= ({ className }) => {
diff --git a/web/src/hooks/logic-hooks/navigate-hooks.ts b/web/src/hooks/logic-hooks/navigate-hooks.ts
index b31e9fb5f..b403b37ec 100644
--- a/web/src/hooks/logic-hooks/navigate-hooks.ts
+++ b/web/src/hooks/logic-hooks/navigate-hooks.ts
@@ -125,6 +125,16 @@ export const useNavigatePage = () => {
[navigate],
);
+ const navigateToDataflowResult = useCallback(
+ (id: string, knowledgeId?: string) => () => {
+ navigate(
+ // `${Routes.ParsedResult}/${id}?${QueryStringMap.KnowledgeId}=${knowledgeId}`,
+ `${Routes.DataflowResult}/${id}`,
+ );
+ },
+ [navigate],
+ );
+
return {
navigateToDatasetList,
navigateToDataset,
@@ -144,5 +154,6 @@ export const useNavigatePage = () => {
navigateToFiles,
navigateToAgentList,
navigateToOldProfile,
+ navigateToDataflowResult,
};
};
diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts
index c42e2b693..997eb1b76 100644
--- a/web/src/locales/en.ts
+++ b/web/src/locales/en.ts
@@ -102,6 +102,28 @@ export default {
noMoreData: `That's all. Nothing more.`,
},
knowledgeDetails: {
+ generateKnowledgeGraph:
+ 'This will extract entities and relationships from all your documents in this dataset. The process may take a while to complete.',
+ generateRaptor:
+ 'This will extract entities and relationships from all your documents in this dataset. The process may take a while to complete.',
+ generate: 'Generate',
+ raptor: 'Raptor',
+ knowledgeGraph: 'Knowledge Graph',
+ processingType: 'Processing Type',
+ dataPipeline: 'Data Pipeline',
+ operations: 'Operations',
+ status: 'Status',
+ task: 'Task',
+ startDate: 'Start Date',
+ source: 'Source',
+ fileName: 'File Name',
+ datasetLogs: 'Dataset Logs',
+ fileLogs: 'File Logs',
+ overview: 'Overview',
+ success: 'Success',
+ failed: 'Failed',
+ completed: 'Completed',
+ processLog: 'Process Log',
created: 'Created',
learnMore: 'Learn More',
general: 'General',
@@ -195,6 +217,7 @@ export default {
chunk: 'Chunk',
bulk: 'Bulk',
cancel: 'Cancel',
+ close: 'Close',
rerankModel: 'Rerank model',
rerankPlaceholder: 'Please select',
rerankTip: `Optional. If left empty, RAGFlow will use a combination of weighted keyword similarity and weighted vector cosine similarity; if a rerank model is selected, a weighted reranking score will replace the weighted vector cosine similarity. Please be aware that using a rerank model will significantly increase the system's response time. If you wish to use a rerank model, ensure you use a SaaS reranker; if you prefer a locally deployed rerank model, ensure you start RAGFlow with docker-compose-gpu.yml.`,
@@ -238,6 +261,16 @@ export default {
reRankModelWaring: 'Re-rank model is very time consuming.',
},
knowledgeConfiguration: {
+ enableAutoGenerate: 'Enable Auto Generate',
+ teamPlaceholder: 'Please select a team.',
+ dataFlowPlaceholder: 'Please select a data flow.',
+ buildItFromScratch: 'Build it from scratch',
+ useRAPTORToEnhanceRetrieval: 'Use RAPTOR to Enhance Retrieval',
+ extractKnowledgeGraph: 'Extract Knowledge Graph',
+ dataFlow: 'Data Flow',
+ parseType: 'Parse Type',
+ manualSetup: 'Manual Setup',
+ builtIn: 'Built-in',
titleDescription:
'Update your knowledge base configuration here, particularly the chunking method.',
name: 'Knowledge base name',
@@ -1589,5 +1622,11 @@ This delimiter is used to split the input text into several text pieces echo of
total: 'Total {{total}}',
page: '{{page}} /Page',
},
+ dataflowParser: {
+ parseSummary: 'Parse Summary',
+ parseSummaryTip: 'Parser:deepdoc',
+ rerunFromCurrentStep: 'Rerun From Current Step',
+ rerunFromCurrentStepTip: 'Changes detected. Click to re-run.',
+ },
},
};
diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts
index 1f8d9d9ee..403ff6b45 100644
--- a/web/src/locales/zh.ts
+++ b/web/src/locales/zh.ts
@@ -94,6 +94,24 @@ export default {
noMoreData: '没有更多数据了',
},
knowledgeDetails: {
+ generate: '生成',
+ raptor: 'Raptor',
+ knowledgeGraph: '知识图谱',
+ processingType: '处理类型',
+ dataPipeline: '数据管道',
+ operations: '操作',
+ status: '状态',
+ task: '任务',
+ startDate: '开始时间',
+ source: '来源',
+ fileName: '文件名',
+ datasetLogs: '数据集日志',
+ fileLogs: '文件日志',
+ overview: '概览',
+ success: '成功',
+ failed: '失败',
+ completed: '已完成',
+ processLog: '处理进度日志',
created: '创建于',
learnMore: '了解更多',
general: '通用',
@@ -183,6 +201,7 @@ export default {
chunk: '解析块',
bulk: '批量',
cancel: '取消',
+ close: '关闭',
rerankModel: 'Rerank模型',
rerankPlaceholder: '请选择',
rerankTip: `非必选项:若不选择 rerank 模型,系统将默认采用关键词相似度与向量余弦相似度相结合的混合查询方式;如果设置了 rerank 模型,则混合查询中的向量相似度部分将被 rerank 打分替代。请注意:采用 rerank 模型会非常耗时。如需选用 rerank 模型,建议使用 SaaS 的 rerank 模型服务;如果你倾向使用本地部署的 rerank 模型,请务必确保你使用 docker-compose-gpu.yml 启动 RAGFlow。`,
@@ -227,6 +246,16 @@ export default {
theDocumentBeingParsedCannotBeDeleted: '正在解析的文档不能被删除',
},
knowledgeConfiguration: {
+ enableAutoGenerate: '是否启用自动生成',
+ teamPlaceholder: '请选择团队',
+ dataFlowPlaceholder: '请选择数据流',
+ buildItFromScratch: '去Scratch构建',
+ useRAPTORToEnhanceRetrieval: '使用 RAPTOR 提升检索效果',
+ extractKnowledgeGraph: '知识图谱提取',
+ dataFlow: '数据流',
+ parseType: '切片方法',
+ manualSetup: '手动设置',
+ builtIn: '内置',
titleDescription: '在这里更新您的知识库详细信息,尤其是切片方法。',
name: '知识库名称',
photo: '知识库图片',
@@ -1501,5 +1530,11 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
total: '总共 {{total}} 条',
page: '{{page}}条/页',
},
+ dataflowParser: {
+ parseSummary: '解析摘要',
+ parseSummaryTip: '解析器: deepdoc',
+ rerunFromCurrentStep: '从当前步骤重新运行',
+ rerunFromCurrentStepTip: '已修改,点击重新运行。',
+ },
},
};
diff --git a/web/src/pages/agent/canvas/index.tsx b/web/src/pages/agent/canvas/index.tsx
index b7c929a52..74e6d146a 100644
--- a/web/src/pages/agent/canvas/index.tsx
+++ b/web/src/pages/agent/canvas/index.tsx
@@ -40,6 +40,7 @@ import { useCacheChatLog } from '../hooks/use-cache-chat-log';
import { useMoveNote } from '../hooks/use-move-note';
import { useDropdownManager } from './context';
+import Spotlight from '@/components/spotlight';
import {
useHideFormSheetOnNodeDeletion,
useShowDrawer,
@@ -309,6 +310,7 @@ function AgentCanvas({ drawerVisible, hideDrawer }: IProps) {
onBeforeDelete={handleBeforeDelete}
>
+
diff --git a/web/src/pages/dataflow-result/chunker.tsx b/web/src/pages/dataflow-result/chunker.tsx
new file mode 100644
index 000000000..ff869809d
--- /dev/null
+++ b/web/src/pages/dataflow-result/chunker.tsx
@@ -0,0 +1,234 @@
+import message from '@/components/ui/message';
+import {
+ RAGFlowPagination,
+ RAGFlowPaginationType,
+} from '@/components/ui/ragflow-pagination';
+import { Spin } from '@/components/ui/spin';
+import {
+ useFetchNextChunkList,
+ useSwitchChunk,
+} from '@/hooks/use-chunk-request';
+import classNames from 'classnames';
+import { useCallback, useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import ChunkCard from './components/chunk-card';
+import CreatingModal from './components/chunk-creating-modal';
+import ChunkResultBar from './components/chunk-result-bar';
+import CheckboxSets from './components/chunk-result-bar/checkbox-sets';
+import RerunButton from './components/rerun-button';
+import {
+ useChangeChunkTextMode,
+ useDeleteChunkByIds,
+ useHandleChunkCardClick,
+ useUpdateChunk,
+} from './hooks';
+import styles from './index.less';
+const ChunkerContainer = () => {
+ const [selectedChunkIds, setSelectedChunkIds] = useState([]);
+ const [isChange, setIsChange] = useState(false);
+ const { t } = useTranslation();
+ const {
+ data: { documentInfo, data = [], total },
+ pagination,
+ loading,
+ searchString,
+ handleInputChange,
+ available,
+ handleSetAvailable,
+ } = useFetchNextChunkList();
+ const { handleChunkCardClick, selectedChunkId } = useHandleChunkCardClick();
+ const isPdf = documentInfo?.type === 'pdf';
+ const {
+ chunkUpdatingLoading,
+ onChunkUpdatingOk,
+ showChunkUpdatingModal,
+ hideChunkUpdatingModal,
+ chunkId,
+ chunkUpdatingVisible,
+ documentId,
+ } = useUpdateChunk();
+ const { removeChunk } = useDeleteChunkByIds();
+ const { changeChunkTextMode, textMode } = useChangeChunkTextMode();
+ const selectAllChunk = useCallback(
+ (checked: boolean) => {
+ setSelectedChunkIds(checked ? data.map((x) => x.chunk_id) : []);
+ },
+ [data],
+ );
+ const showSelectedChunkWarning = useCallback(() => {
+ message.warning(t('message.pleaseSelectChunk'));
+ }, [t]);
+ const { switchChunk } = useSwitchChunk();
+
+ const [chunkList, setChunkList] = useState(data);
+ useEffect(() => {
+ setChunkList(data);
+ }, [data]);
+ const onPaginationChange: RAGFlowPaginationType['onChange'] = (
+ page,
+ size,
+ ) => {
+ setSelectedChunkIds([]);
+ pagination.onChange?.(page, size);
+ };
+
+ const handleSwitchChunk = useCallback(
+ async (available?: number, chunkIds?: string[]) => {
+ let ids = chunkIds;
+ if (!chunkIds) {
+ ids = selectedChunkIds;
+ if (selectedChunkIds.length === 0) {
+ showSelectedChunkWarning();
+ return;
+ }
+ }
+
+ const resCode: number = await switchChunk({
+ chunk_ids: ids,
+ available_int: available,
+ doc_id: documentId,
+ });
+ if (ids?.length && resCode === 0) {
+ chunkList.forEach((x: any) => {
+ if (ids.indexOf(x['chunk_id']) > -1) {
+ x['available_int'] = available;
+ }
+ });
+ setChunkList(chunkList);
+ }
+ },
+ [
+ switchChunk,
+ documentId,
+ selectedChunkIds,
+ showSelectedChunkWarning,
+ chunkList,
+ ],
+ );
+ const handleSingleCheckboxClick = useCallback(
+ (chunkId: string, checked: boolean) => {
+ setSelectedChunkIds((previousIds) => {
+ const idx = previousIds.findIndex((x) => x === chunkId);
+ const nextIds = [...previousIds];
+ if (checked && idx === -1) {
+ nextIds.push(chunkId);
+ } else if (!checked && idx !== -1) {
+ nextIds.splice(idx, 1);
+ }
+ return nextIds;
+ });
+ },
+ [],
+ );
+ const handleRemoveChunk = useCallback(async () => {
+ if (selectedChunkIds.length > 0) {
+ const resCode: number = await removeChunk(selectedChunkIds, documentId);
+ if (resCode === 0) {
+ setSelectedChunkIds([]);
+ }
+ } else {
+ showSelectedChunkWarning();
+ }
+ }, [selectedChunkIds, documentId, removeChunk, showSelectedChunkWarning]);
+
+ const handleChunkEditSave = (e: any) => {
+ setIsChange(true);
+ onChunkUpdatingOk(e);
+ };
+ return (
+ <>
+ {isChange && (
+
+
+
+ )}
+
+
+
+
+
{t('chunk.chunkResult')}
+
+ {t('chunk.chunkResultTip')}
+
+
+
+
+
+
+
+
+
+
+ {chunkList.map((item) => (
+ x === item.chunk_id)}
+ handleCheckboxClick={handleSingleCheckboxClick}
+ switchChunk={handleSwitchChunk}
+ clickChunkCard={handleChunkCardClick}
+ selected={item.chunk_id === selectedChunkId}
+ textMode={textMode}
+ >
+ ))}
+
+
+
+ {
+ onPaginationChange(page, pageSize);
+ }}
+ >
+
+
+
+
+ {chunkUpdatingVisible && (
+ {
+ handleChunkEditSave(e);
+ }}
+ parserId={documentInfo.parser_id}
+ />
+ )}
+ >
+ );
+};
+
+export { ChunkerContainer };
diff --git a/web/src/pages/dataflow-result/components/chunk-card/index.less b/web/src/pages/dataflow-result/components/chunk-card/index.less
new file mode 100644
index 000000000..aac7724af
--- /dev/null
+++ b/web/src/pages/dataflow-result/components/chunk-card/index.less
@@ -0,0 +1,36 @@
+.image {
+ width: 100px !important;
+ object-fit: contain;
+}
+
+.imagePreview {
+ max-width: 50vw;
+ max-height: 50vh;
+ object-fit: contain;
+}
+
+.content {
+ flex: 1;
+ .chunkText;
+}
+
+.contentEllipsis {
+ .multipleLineEllipsis(3);
+}
+
+.contentText {
+ word-break: break-all !important;
+}
+
+.chunkCard {
+ width: 100%;
+ padding: 18px 10px;
+}
+
+.cardSelected {
+ background-color: @selectedBackgroundColor;
+}
+
+.cardSelectedDark {
+ background-color: #ffffff2f;
+}
diff --git a/web/src/pages/dataflow-result/components/chunk-card/index.tsx b/web/src/pages/dataflow-result/components/chunk-card/index.tsx
new file mode 100644
index 000000000..198746aef
--- /dev/null
+++ b/web/src/pages/dataflow-result/components/chunk-card/index.tsx
@@ -0,0 +1,127 @@
+import Image from '@/components/image';
+import { useTheme } from '@/components/theme-provider';
+import { Card } from '@/components/ui/card';
+import { Checkbox } from '@/components/ui/checkbox';
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from '@/components/ui/popover';
+import { Switch } from '@/components/ui/switch';
+import { IChunk } from '@/interfaces/database/knowledge';
+import { CheckedState } from '@radix-ui/react-checkbox';
+import classNames from 'classnames';
+import DOMPurify from 'dompurify';
+import { useEffect, useState } from 'react';
+import { ChunkTextMode } from '../../constant';
+import styles from './index.less';
+
+interface IProps {
+ item: IChunk;
+ checked: boolean;
+ switchChunk: (available?: number, chunkIds?: string[]) => void;
+ editChunk: (chunkId: string) => void;
+ handleCheckboxClick: (chunkId: string, checked: boolean) => void;
+ selected: boolean;
+ clickChunkCard: (chunkId: string) => void;
+ textMode: ChunkTextMode;
+}
+
+const ChunkCard = ({
+ item,
+ checked,
+ handleCheckboxClick,
+ editChunk,
+ switchChunk,
+ selected,
+ clickChunkCard,
+ textMode,
+}: IProps) => {
+ const available = Number(item.available_int);
+ const [enabled, setEnabled] = useState(false);
+ const { theme } = useTheme();
+
+ const onChange = (checked: boolean) => {
+ setEnabled(checked);
+ switchChunk(available === 0 ? 1 : 0, [item.chunk_id]);
+ };
+
+ const handleCheck = (e: CheckedState) => {
+ handleCheckboxClick(item.chunk_id, e === 'indeterminate' ? false : e);
+ };
+
+ const handleContentDoubleClick = () => {
+ editChunk(item.chunk_id);
+ };
+
+ const handleContentClick = () => {
+ clickChunkCard(item.chunk_id);
+ };
+
+ useEffect(() => {
+ setEnabled(available === 1);
+ }, [available]);
+ const [open, setOpen] = useState(false);
+ return (
+
+
+
+ {item.image_id && (
+
+ setOpen(true)}
+ onMouseLeave={() => setOpen(false)}
+ >
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+ );
+};
+
+export default ChunkCard;
diff --git a/web/src/pages/dataflow-result/components/chunk-creating-modal/index.tsx b/web/src/pages/dataflow-result/components/chunk-creating-modal/index.tsx
new file mode 100644
index 000000000..66cf4d619
--- /dev/null
+++ b/web/src/pages/dataflow-result/components/chunk-creating-modal/index.tsx
@@ -0,0 +1,206 @@
+import EditTag from '@/components/edit-tag';
+import Divider from '@/components/ui/divider';
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@/components/ui/form';
+import {
+ HoverCard,
+ HoverCardContent,
+ HoverCardTrigger,
+} from '@/components/ui/hover-card';
+import { Modal } from '@/components/ui/modal/modal';
+import Space from '@/components/ui/space';
+import { Switch } from '@/components/ui/switch';
+import { Textarea } from '@/components/ui/textarea';
+import { useFetchChunk } from '@/hooks/chunk-hooks';
+import { IModalProps } from '@/interfaces/common';
+import { Trash2 } from 'lucide-react';
+import React, { useCallback, useEffect, useState } from 'react';
+import { FieldValues, FormProvider, useForm } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+import { useDeleteChunkByIds } from '../../hooks';
+import {
+ transformTagFeaturesArrayToObject,
+ transformTagFeaturesObjectToArray,
+} from '../../utils';
+import { TagFeatureItem } from './tag-feature-item';
+
+interface kFProps {
+ doc_id: string;
+ chunkId: string | undefined;
+ parserId: string;
+}
+
+const ChunkCreatingModal: React.FC & kFProps> = ({
+ doc_id,
+ chunkId,
+ hideModal,
+ onOk,
+ loading,
+ parserId,
+}) => {
+ // const [form] = Form.useForm();
+ // const form = useFormContext();
+ const form = useForm({
+ defaultValues: {
+ content_with_weight: '',
+ tag_kwd: [],
+ question_kwd: [],
+ important_kwd: [],
+ tag_feas: [],
+ },
+ });
+ const [checked, setChecked] = useState(false);
+ const { removeChunk } = useDeleteChunkByIds();
+ const { data } = useFetchChunk(chunkId);
+ const { t } = useTranslation();
+
+ const isTagParser = parserId === 'tag';
+ const onSubmit = useCallback(
+ (values: FieldValues) => {
+ onOk?.({
+ ...values,
+ tag_feas: transformTagFeaturesArrayToObject(values.tag_feas),
+ available_int: checked ? 1 : 0,
+ });
+ },
+ [checked, onOk],
+ );
+
+ const handleOk = form.handleSubmit(onSubmit);
+
+ const handleRemove = useCallback(() => {
+ if (chunkId) {
+ return removeChunk([chunkId], doc_id);
+ }
+ }, [chunkId, doc_id, removeChunk]);
+
+ const handleCheck = useCallback(() => {
+ setChecked(!checked);
+ }, [checked]);
+
+ useEffect(() => {
+ if (data?.code === 0) {
+ const { available_int, tag_feas } = data.data;
+ form.reset({
+ ...data.data,
+ tag_feas: transformTagFeaturesObjectToArray(tag_feas),
+ });
+
+ setChecked(available_int !== 0);
+ }
+ }, [data, form, chunkId]);
+
+ return (
+
+
+
+ {chunkId && (
+
+
+
+
+ {t('chunk.enabled')}
+
+
+
+ {t('common.delete')}
+
+
+
+ )}
+
+ );
+};
+export default ChunkCreatingModal;
diff --git a/web/src/pages/dataflow-result/components/chunk-creating-modal/tag-feature-item.tsx b/web/src/pages/dataflow-result/components/chunk-creating-modal/tag-feature-item.tsx
new file mode 100644
index 000000000..3c9f92c78
--- /dev/null
+++ b/web/src/pages/dataflow-result/components/chunk-creating-modal/tag-feature-item.tsx
@@ -0,0 +1,136 @@
+import { SelectWithSearch } from '@/components/originui/select-with-search';
+import { Button } from '@/components/ui/button';
+import {
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from '@/components/ui/form';
+import { NumberInput } from '@/components/ui/input';
+import { useFetchTagListByKnowledgeIds } from '@/hooks/knowledge-hooks';
+import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
+import { CircleMinus, Plus } from 'lucide-react';
+import { useCallback, useEffect, useMemo } from 'react';
+import { useFieldArray, useFormContext } from 'react-hook-form';
+import { useTranslation } from 'react-i18next';
+import { FormListItem } from '../../utils';
+
+const FieldKey = 'tag_feas';
+
+export const TagFeatureItem = () => {
+ const { t } = useTranslation();
+ const { setKnowledgeIds, list } = useFetchTagListByKnowledgeIds();
+ const { data: knowledgeConfiguration } = useFetchKnowledgeBaseConfiguration();
+ const form = useFormContext();
+ const tagKnowledgeIds = useMemo(() => {
+ return knowledgeConfiguration?.parser_config?.tag_kb_ids ?? [];
+ }, [knowledgeConfiguration?.parser_config?.tag_kb_ids]);
+
+ const options = useMemo(() => {
+ return list.map((x) => ({
+ value: x[0],
+ label: x[0],
+ }));
+ }, [list]);
+
+ const filterOptions = useCallback(
+ (index: number) => {
+ const tags: FormListItem[] = form.getValues(FieldKey) ?? [];
+
+ // Exclude it's own current data
+ const list = tags
+ .filter((x, idx) => x && index !== idx)
+ .map((x) => x.tag);
+ // Exclude the selected data from other options from one's own options.
+ const resultList = options.filter(
+ (x) => !list.some((y) => x.value === y),
+ );
+ return resultList;
+ },
+ [form, options],
+ );
+
+ useEffect(() => {
+ setKnowledgeIds(tagKnowledgeIds);
+ }, [setKnowledgeIds, tagKnowledgeIds]);
+
+ const { fields, append, remove } = useFieldArray({
+ control: form.control,
+ name: FieldKey,
+ });
+ return (
+ (
+
+ {t('knowledgeConfiguration.tags')}
+
+ {fields.map((item, name) => {
+ return (
+
+
+
(
+
+
+
+
+
+
+
+
+ )}
+ />
+ (
+
+
+
+
+
+
+ )}
+ />
+
+
remove(name)}
+ className="text-red-500"
+ />
+
+ );
+ })}
+
+
+
+ )}
+ />
+ );
+};
diff --git a/web/src/pages/dataflow-result/components/chunk-result-bar/checkbox-sets.tsx b/web/src/pages/dataflow-result/components/chunk-result-bar/checkbox-sets.tsx
new file mode 100644
index 000000000..68bafeeb7
--- /dev/null
+++ b/web/src/pages/dataflow-result/components/chunk-result-bar/checkbox-sets.tsx
@@ -0,0 +1,85 @@
+import { Checkbox } from '@/components/ui/checkbox';
+import { Label } from '@/components/ui/label';
+import { Ban, CircleCheck, Trash2 } from 'lucide-react';
+import { useCallback, useMemo } from 'react';
+import { useTranslation } from 'react-i18next';
+
+type ICheckboxSetProps = {
+ selectAllChunk: (e: any) => void;
+ removeChunk: (e?: any) => void;
+ switchChunk: (available: number) => void;
+ checked: boolean;
+ selectedChunkIds: string[];
+};
+export default (props: ICheckboxSetProps) => {
+ const {
+ selectAllChunk,
+ removeChunk,
+ switchChunk,
+ checked,
+ selectedChunkIds,
+ } = props;
+ const { t } = useTranslation();
+ const handleSelectAllCheck = useCallback(
+ (e: any) => {
+ console.log('eee=', e);
+ selectAllChunk(e);
+ },
+ [selectAllChunk],
+ );
+
+ const handleDeleteClick = useCallback(() => {
+ removeChunk();
+ }, [removeChunk]);
+
+ const handleEnabledClick = useCallback(() => {
+ switchChunk(1);
+ }, [switchChunk]);
+
+ const handleDisabledClick = useCallback(() => {
+ switchChunk(0);
+ }, [switchChunk]);
+
+ const isSelected = useMemo(() => {
+ return selectedChunkIds?.length > 0;
+ }, [selectedChunkIds]);
+
+ return (
+
+
+
+
+
+ {isSelected && (
+ <>
+
+
+ {t('chunk.enable')}
+
+
+
+ {t('chunk.disable')}
+
+
+
+ {t('chunk.delete')}
+
+ >
+ )}
+
+ );
+};
diff --git a/web/src/pages/dataflow-result/components/chunk-result-bar/index.tsx b/web/src/pages/dataflow-result/components/chunk-result-bar/index.tsx
new file mode 100644
index 000000000..368317441
--- /dev/null
+++ b/web/src/pages/dataflow-result/components/chunk-result-bar/index.tsx
@@ -0,0 +1,108 @@
+import { Input } from '@/components/originui/input';
+import { Button } from '@/components/ui/button';
+import {
+ Popover,
+ 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>;
+ available: number | undefined;
+ selectAllChunk: (value: boolean) => void;
+ handleSetAvailable: (value: number | undefined) => void;
+ createChunk: () => void;
+ handleInputChange: (e: React.ChangeEvent) => void;
+ searchString: string;
+}
+export default ({
+ changeChunkTextMode,
+ available,
+ selectAllChunk,
+ handleSetAvailable,
+ createChunk,
+ handleInputChange,
+ searchString,
+}: ChunkResultBarProps) => {
+ const { t } = useTranslate('chunk');
+ const [textSelectValue, setTextSelectValue] = useState(
+ ChunkTextMode.Full,
+ );
+ const handleFilterChange = (e: string | number) => {
+ const value = e === -1 ? undefined : (e as number);
+ selectAllChunk(false);
+ handleSetAvailable(value);
+ };
+ const filterContent = (
+
+
+
+ {t('all')}
+ {t('enabled')}
+ {t('disabled')}
+
+
+
+ );
+ const textSelectOptions = [
+ { label: t(ChunkTextMode.Full), value: ChunkTextMode.Full },
+ { label: t(ChunkTextMode.Ellipse), value: ChunkTextMode.Ellipse },
+ ];
+
+ const changeTextSelectValue = (value: string | number) => {
+ setTextSelectValue(value);
+ changeChunkTextMode(value);
+ };
+ return (
+
+
+ {textSelectOptions.map((option) => (
+
changeTextSelectValue(option.value)}
+ >
+ {option.label}
+
+ ))}
+
+
}
+ onChange={handleInputChange}
+ value={searchString}
+ />
+
+
+
+
+
+ {filterContent}
+
+
+
+
+ );
+};
diff --git a/web/src/pages/dataflow-result/components/chunk-toolbar/index.tsx b/web/src/pages/dataflow-result/components/chunk-toolbar/index.tsx
new file mode 100644
index 000000000..6a513ba78
--- /dev/null
+++ b/web/src/pages/dataflow-result/components/chunk-toolbar/index.tsx
@@ -0,0 +1,221 @@
+import { ReactComponent as FilterIcon } from '@/assets/filter.svg';
+import { KnowledgeRouteKey } from '@/constants/knowledge';
+import { IChunkListResult, useSelectChunkList } from '@/hooks/chunk-hooks';
+import { useTranslate } from '@/hooks/common-hooks';
+import { useKnowledgeBaseId } from '@/hooks/knowledge-hooks';
+import {
+ ArrowLeftOutlined,
+ CheckCircleOutlined,
+ CloseCircleOutlined,
+ DeleteOutlined,
+ DownOutlined,
+ FilePdfOutlined,
+ PlusOutlined,
+ SearchOutlined,
+} from '@ant-design/icons';
+import {
+ Button,
+ Checkbox,
+ Flex,
+ Input,
+ Menu,
+ MenuProps,
+ Popover,
+ Radio,
+ RadioChangeEvent,
+ Segmented,
+ SegmentedProps,
+ Space,
+ Typography,
+} from 'antd';
+import { useCallback, useMemo, useState } from 'react';
+import { Link } from 'umi';
+import { ChunkTextMode } from '../../constant';
+
+const { Text } = Typography;
+
+interface IProps
+ extends Pick<
+ IChunkListResult,
+ 'searchString' | 'handleInputChange' | 'available' | 'handleSetAvailable'
+ > {
+ checked: boolean;
+ selectAllChunk: (checked: boolean) => void;
+ createChunk: () => void;
+ removeChunk: () => void;
+ switchChunk: (available: number) => void;
+ changeChunkTextMode(mode: ChunkTextMode): void;
+}
+
+const ChunkToolBar = ({
+ selectAllChunk,
+ checked,
+ createChunk,
+ removeChunk,
+ switchChunk,
+ changeChunkTextMode,
+ available,
+ handleSetAvailable,
+ searchString,
+ handleInputChange,
+}: IProps) => {
+ const data = useSelectChunkList();
+ const documentInfo = data?.documentInfo;
+ const knowledgeBaseId = useKnowledgeBaseId();
+ const [isShowSearchBox, setIsShowSearchBox] = useState(false);
+ const { t } = useTranslate('chunk');
+
+ const handleSelectAllCheck = useCallback(
+ (e: any) => {
+ selectAllChunk(e.target.checked);
+ },
+ [selectAllChunk],
+ );
+
+ const handleSearchIconClick = () => {
+ setIsShowSearchBox(true);
+ };
+
+ const handleSearchBlur = () => {
+ if (!searchString?.trim()) {
+ setIsShowSearchBox(false);
+ }
+ };
+
+ const handleDelete = useCallback(() => {
+ removeChunk();
+ }, [removeChunk]);
+
+ const handleEnabledClick = useCallback(() => {
+ switchChunk(1);
+ }, [switchChunk]);
+
+ const handleDisabledClick = useCallback(() => {
+ switchChunk(0);
+ }, [switchChunk]);
+
+ const items: MenuProps['items'] = useMemo(() => {
+ return [
+ {
+ key: '1',
+ label: (
+ <>
+
+ {t('selectAll')}
+
+ >
+ ),
+ },
+ { type: 'divider' },
+ {
+ key: '2',
+ label: (
+
+
+ {t('enabledSelected')}
+
+ ),
+ },
+ {
+ key: '3',
+ label: (
+
+
+ {t('disabledSelected')}
+
+ ),
+ },
+ { type: 'divider' },
+ {
+ key: '4',
+ label: (
+
+
+ {t('deleteSelected')}
+
+ ),
+ },
+ ];
+ }, [
+ checked,
+ handleSelectAllCheck,
+ handleDelete,
+ handleEnabledClick,
+ handleDisabledClick,
+ t,
+ ]);
+
+ const content = (
+
+ );
+
+ const handleFilterChange = (e: RadioChangeEvent) => {
+ selectAllChunk(false);
+ handleSetAvailable(e.target.value);
+ };
+
+ const filterContent = (
+
+
+ {t('all')}
+ {t('enabled')}
+ {t('disabled')}
+
+
+ );
+
+ return (
+
+
+
+
+
+
+
+ {documentInfo?.name}
+
+
+
+
+
+
+
+ {isShowSearchBox ? (
+ }
+ allowClear
+ onChange={handleInputChange}
+ onBlur={handleSearchBlur}
+ value={searchString}
+ />
+ ) : (
+ } onClick={handleSearchIconClick} />
+ )}
+
+
+ } />
+
+ }
+ type="primary"
+ onClick={() => createChunk()}
+ />
+
+
+ );
+};
+
+export default ChunkToolBar;
diff --git a/web/src/pages/dataflow-result/components/document-preview/csv-preview.tsx b/web/src/pages/dataflow-result/components/document-preview/csv-preview.tsx
new file mode 100644
index 000000000..45b05454e
--- /dev/null
+++ b/web/src/pages/dataflow-result/components/document-preview/csv-preview.tsx
@@ -0,0 +1,114 @@
+import message from '@/components/ui/message';
+import { Spin } from '@/components/ui/spin';
+import request from '@/utils/request';
+import classNames from 'classnames';
+import React, { useEffect, useRef, useState } from 'react';
+
+interface CSVData {
+ rows: string[][];
+ headers: string[];
+}
+
+interface FileViewerProps {
+ className?: string;
+ url: string;
+}
+
+const CSVFileViewer: React.FC = ({ url }) => {
+ const [data, setData] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const containerRef = useRef(null);
+ // const url = useGetDocumentUrl();
+ const parseCSV = (csvText: string): CSVData => {
+ console.log('Parsing CSV data:', csvText);
+ const lines = csvText.split('\n');
+ const headers = lines[0].split(',').map((header) => header.trim());
+ const rows = lines
+ .slice(1)
+ .map((line) => line.split(',').map((cell) => cell.trim()));
+
+ return { headers, rows };
+ };
+
+ useEffect(() => {
+ const loadCSV = async () => {
+ try {
+ const res = await request(url, {
+ method: 'GET',
+ responseType: 'blob',
+ onError: () => {
+ message.error('file load failed');
+ setIsLoading(false);
+ },
+ });
+
+ // parse CSV file
+ const reader = new FileReader();
+ reader.readAsText(res.data);
+ reader.onload = () => {
+ const parsedData = parseCSV(reader.result as string);
+ console.log('file loaded successfully', reader.result);
+ setData(parsedData);
+ };
+ } catch (error) {
+ message.error('CSV file parse failed');
+ console.error('Error loading CSV file:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ loadCSV();
+
+ return () => {
+ setData(null);
+ };
+ }, [url]);
+
+ return (
+
+ {isLoading ? (
+
+
+
+ ) : data ? (
+
+
+
+ {data.headers.map((header, index) => (
+ |
+ {header}
+ |
+ ))}
+
+
+
+ {data.rows.map((row, rowIndex) => (
+
+ {row.map((cell, cellIndex) => (
+ |
+ {cell || '-'}
+ |
+ ))}
+
+ ))}
+
+
+ ) : null}
+
+ );
+};
+
+export default CSVFileViewer;
diff --git a/web/src/pages/dataflow-result/components/document-preview/doc-preview.tsx b/web/src/pages/dataflow-result/components/document-preview/doc-preview.tsx
new file mode 100644
index 000000000..845f0374e
--- /dev/null
+++ b/web/src/pages/dataflow-result/components/document-preview/doc-preview.tsx
@@ -0,0 +1,70 @@
+import message from '@/components/ui/message';
+import { Spin } from '@/components/ui/spin';
+import request from '@/utils/request';
+import classNames from 'classnames';
+import mammoth from 'mammoth';
+import { useEffect, useState } from 'react';
+
+interface DocPreviewerProps {
+ className?: string;
+ url: string;
+}
+
+export const DocPreviewer: React.FC = ({
+ className,
+ url,
+}) => {
+ // const url = useGetDocumentUrl();
+ const [htmlContent, setHtmlContent] = useState('');
+ const [loading, setLoading] = useState(false);
+ const fetchDocument = async () => {
+ setLoading(true);
+ const res = await request(url, {
+ method: 'GET',
+ responseType: 'blob',
+ onError: () => {
+ message.error('Document parsing failed');
+ console.error('Error loading document:', url);
+ },
+ });
+ try {
+ const arrayBuffer = await res.data.arrayBuffer();
+ const result = await mammoth.convertToHtml(
+ { arrayBuffer },
+ { includeDefaultStyleMap: true },
+ );
+
+ const styledContent = result.value
+ .replace(//g, '
')
+ .replace(//g, '');
+
+ setHtmlContent(styledContent);
+ } catch (err) {
+ message.error('Document parsing failed');
+ console.error('Error parsing document:', err);
+ }
+ setLoading(false);
+ };
+
+ useEffect(() => {
+ if (url) {
+ fetchDocument();
+ }
+ }, [url]);
+ return (
+
+ {loading && (
+
+
+
+ )}
+
+ {!loading &&
}
+
+ );
+};
diff --git a/web/src/pages/dataflow-result/components/document-preview/document-header.tsx b/web/src/pages/dataflow-result/components/document-preview/document-header.tsx
new file mode 100644
index 000000000..5ff971b3a
--- /dev/null
+++ b/web/src/pages/dataflow-result/components/document-preview/document-header.tsx
@@ -0,0 +1,21 @@
+import { formatDate } from '@/utils/date';
+import { formatBytes } from '@/utils/file-util';
+
+type Props = {
+ size: number;
+ name: string;
+ create_date: string;
+};
+
+export default ({ size, name, create_date }: Props) => {
+ const sizeName = formatBytes(size);
+ const dateStr = formatDate(create_date);
+ return (
+
+
{name}
+
+ Size:{sizeName} Uploaded Time:{dateStr}
+
+
+ );
+};
diff --git a/web/src/pages/dataflow-result/components/document-preview/excel-preview.tsx b/web/src/pages/dataflow-result/components/document-preview/excel-preview.tsx
new file mode 100644
index 000000000..c86e0462c
--- /dev/null
+++ b/web/src/pages/dataflow-result/components/document-preview/excel-preview.tsx
@@ -0,0 +1,25 @@
+import { useFetchExcel } from '@/pages/document-viewer/hooks';
+import classNames from 'classnames';
+
+interface ExcelCsvPreviewerProps {
+ className?: string;
+ url: string;
+}
+
+export const ExcelCsvPreviewer: React.FC = ({
+ className,
+ url,
+}) => {
+ // const url = useGetDocumentUrl();
+ const { containerRef } = useFetchExcel(url);
+
+ return (
+
+ );
+};
diff --git a/web/src/pages/dataflow-result/components/document-preview/hooks.ts b/web/src/pages/dataflow-result/components/document-preview/hooks.ts
new file mode 100644
index 000000000..fcf6a01ba
--- /dev/null
+++ b/web/src/pages/dataflow-result/components/document-preview/hooks.ts
@@ -0,0 +1,55 @@
+import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
+import { api_host } from '@/utils/api';
+import { useSize } from 'ahooks';
+import { CustomTextRenderer } from 'node_modules/react-pdf/dist/esm/shared/types';
+import { useCallback, useEffect, useMemo, useState } from 'react';
+
+export const useDocumentResizeObserver = () => {
+ const [containerWidth, setContainerWidth] = useState();
+ const [containerRef, setContainerRef] = useState(null);
+ const size = useSize(containerRef);
+
+ const onResize = useCallback((width?: number) => {
+ if (width) {
+ setContainerWidth(width);
+ }
+ }, []);
+
+ useEffect(() => {
+ onResize(size?.width);
+ }, [size?.width, onResize]);
+
+ return { containerWidth, setContainerRef };
+};
+
+function highlightPattern(text: string, pattern: string, pageNumber: number) {
+ if (pageNumber === 2) {
+ return `${text}`;
+ }
+ if (text.trim() !== '' && pattern.match(text)) {
+ // return pattern.replace(text, (value) => `${value}`);
+ return `${text}`;
+ }
+ return text.replace(pattern, (value) => `${value}`);
+}
+
+export const useHighlightText = (searchText: string = '') => {
+ const textRenderer: CustomTextRenderer = useCallback(
+ (textItem) => {
+ return highlightPattern(textItem.str, searchText, textItem.pageNumber);
+ },
+ [searchText],
+ );
+
+ return textRenderer;
+};
+
+export const useGetDocumentUrl = () => {
+ const { documentId } = useGetKnowledgeSearchParams();
+
+ const url = useMemo(() => {
+ return `${api_host}/document/get/${documentId}`;
+ }, [documentId]);
+
+ return url;
+};
diff --git a/web/src/pages/dataflow-result/components/document-preview/image-preview.tsx b/web/src/pages/dataflow-result/components/document-preview/image-preview.tsx
new file mode 100644
index 000000000..80e796a54
--- /dev/null
+++ b/web/src/pages/dataflow-result/components/document-preview/image-preview.tsx
@@ -0,0 +1,73 @@
+import message from '@/components/ui/message';
+import { Spin } from '@/components/ui/spin';
+import request from '@/utils/request';
+import classNames from 'classnames';
+import { useEffect, useState } from 'react';
+
+interface ImagePreviewerProps {
+ className?: string;
+ url: string;
+}
+
+export const ImagePreviewer: React.FC = ({
+ className,
+ url,
+}) => {
+ // const url = useGetDocumentUrl();
+ const [imageSrc, setImageSrc] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+
+ const fetchImage = async () => {
+ setIsLoading(true);
+ const res = await request(url, {
+ method: 'GET',
+ responseType: 'blob',
+ onError: () => {
+ message.error('Failed to load image');
+ setIsLoading(false);
+ },
+ });
+ const objectUrl = URL.createObjectURL(res.data);
+ setImageSrc(objectUrl);
+ setIsLoading(false);
+ };
+ useEffect(() => {
+ if (url) {
+ fetchImage();
+ }
+ }, [url]);
+
+ useEffect(() => {
+ return () => {
+ if (imageSrc) {
+ URL.revokeObjectURL(imageSrc);
+ }
+ };
+ }, [imageSrc]);
+
+ return (
+
+ {isLoading && (
+
+
+
+ )}
+
+ {!isLoading && imageSrc && (
+
+

URL.revokeObjectURL(imageSrc!)}
+ />
+
+ )}
+
+ );
+};
diff --git a/web/src/pages/dataflow-result/components/document-preview/index.less b/web/src/pages/dataflow-result/components/document-preview/index.less
new file mode 100644
index 000000000..bbba51f09
--- /dev/null
+++ b/web/src/pages/dataflow-result/components/document-preview/index.less
@@ -0,0 +1,13 @@
+.documentContainer {
+ width: 100%;
+ // height: calc(100vh - 284px);
+ height: calc(100vh - 170px);
+ position: relative;
+ :global(.PdfHighlighter) {
+ overflow-x: hidden;
+ }
+ :global(.Highlight--scrolledTo .Highlight__part) {
+ overflow-x: hidden;
+ background-color: rgba(255, 226, 143, 1);
+ }
+}
diff --git a/web/src/pages/dataflow-result/components/document-preview/index.tsx b/web/src/pages/dataflow-result/components/document-preview/index.tsx
new file mode 100644
index 000000000..dd7b35348
--- /dev/null
+++ b/web/src/pages/dataflow-result/components/document-preview/index.tsx
@@ -0,0 +1,68 @@
+import { memo } from 'react';
+
+import CSVFileViewer from './csv-preview';
+import { DocPreviewer } from './doc-preview';
+import { ExcelCsvPreviewer } from './excel-preview';
+import { ImagePreviewer } from './image-preview';
+import styles from './index.less';
+import PdfPreviewer, { IProps } from './pdf-preview';
+import { PptPreviewer } from './ppt-preview';
+import { TxtPreviewer } from './txt-preview';
+
+type PreviewProps = {
+ fileType: string;
+ className?: string;
+ url: string;
+};
+const Preview = ({
+ fileType,
+ className,
+ highlights,
+ setWidthAndHeight,
+ url,
+}: PreviewProps & Partial) => {
+ return (
+ <>
+ {fileType === 'pdf' && highlights && setWidthAndHeight && (
+
+ )}
+ {['doc', 'docx'].indexOf(fileType) > -1 && (
+
+ )}
+ {['txt', 'md'].indexOf(fileType) > -1 && (
+
+ )}
+ {['visual'].indexOf(fileType) > -1 && (
+
+ )}
+ {['pptx'].indexOf(fileType) > -1 && (
+
+ )}
+ {['xlsx'].indexOf(fileType) > -1 && (
+
+ )}
+ {['csv'].indexOf(fileType) > -1 && (
+
+ )}
+ >
+ );
+};
+export default memo(Preview);
diff --git a/web/src/pages/dataflow-result/components/document-preview/pdf-preview.tsx b/web/src/pages/dataflow-result/components/document-preview/pdf-preview.tsx
new file mode 100644
index 000000000..79b1c54ae
--- /dev/null
+++ b/web/src/pages/dataflow-result/components/document-preview/pdf-preview.tsx
@@ -0,0 +1,127 @@
+import { memo, useEffect, useRef } from 'react';
+import {
+ AreaHighlight,
+ Highlight,
+ IHighlight,
+ PdfHighlighter,
+ PdfLoader,
+ Popup,
+} from 'react-pdf-highlighter';
+
+import { useCatchDocumentError } from '@/components/pdf-previewer/hooks';
+import { Spin } from '@/components/ui/spin';
+import FileError from '@/pages/document-viewer/file-error';
+import styles from './index.less';
+
+export interface IProps {
+ highlights: IHighlight[];
+ setWidthAndHeight: (width: number, height: number) => void;
+ url: string;
+}
+const HighlightPopup = ({
+ comment,
+}: {
+ comment: { text: string; emoji: string };
+}) =>
+ comment.text ? (
+
+ {comment.emoji} {comment.text}
+
+ ) : null;
+
+// TODO: merge with DocumentPreviewer
+const PdfPreview = ({ highlights: state, setWidthAndHeight, url }: IProps) => {
+ // const url = useGetDocumentUrl();
+
+ const ref = useRef<(highlight: IHighlight) => void>(() => {});
+ const error = useCatchDocumentError(url);
+
+ const resetHash = () => {};
+
+ useEffect(() => {
+ if (state.length > 0) {
+ ref?.current(state[0]);
+ }
+ }, [state]);
+
+ return (
+
+ }
+ workerSrc="/pdfjs-dist/pdf.worker.min.js"
+ errorMessage={{error}}
+ >
+ {(pdfDocument) => {
+ pdfDocument.getPage(1).then((page) => {
+ const viewport = page.getViewport({ scale: 1 });
+ const width = viewport.width;
+ const height = viewport.height;
+ setWidthAndHeight(width, height);
+ });
+
+ return (
+ event.altKey}
+ onScrollChange={resetHash}
+ scrollRef={(scrollTo) => {
+ ref.current = scrollTo;
+ }}
+ onSelectionFinished={() => null}
+ highlightTransform={(
+ highlight,
+ index,
+ setTip,
+ hideTip,
+ viewportToScaled,
+ screenshot,
+ isScrolledTo,
+ ) => {
+ const isTextHighlight = !Boolean(
+ highlight.content && highlight.content.image,
+ );
+
+ const component = isTextHighlight ? (
+
+ ) : (
+ {}}
+ />
+ );
+
+ return (
+ }
+ onMouseOver={(popupContent) =>
+ setTip(highlight, () => popupContent)
+ }
+ onMouseOut={hideTip}
+ key={index}
+ >
+ {component}
+
+ );
+ }}
+ highlights={state}
+ />
+ );
+ }}
+
+
+ );
+};
+
+export default memo(PdfPreview);
diff --git a/web/src/pages/dataflow-result/components/document-preview/ppt-preview.tsx b/web/src/pages/dataflow-result/components/document-preview/ppt-preview.tsx
new file mode 100644
index 000000000..7786c48c3
--- /dev/null
+++ b/web/src/pages/dataflow-result/components/document-preview/ppt-preview.tsx
@@ -0,0 +1,70 @@
+import message from '@/components/ui/message';
+import request from '@/utils/request';
+import classNames from 'classnames';
+import { init } from 'pptx-preview';
+import { useEffect, useRef } from 'react';
+interface PptPreviewerProps {
+ className?: string;
+ url: string;
+}
+
+export const PptPreviewer: React.FC