feat: Added UI functions related to data-flow knowledge base #3221 (#10038)

### What problem does this PR solve?

feat: Added UI functions related to data-flow knowledge base #3221

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
chanx
2025-09-11 09:51:18 +08:00
committed by GitHub
parent df8d31451b
commit 8a09f07186
64 changed files with 5079 additions and 81 deletions

8
web/package-lock.json generated
View File

@ -66,7 +66,7 @@
"jsencrypt": "^3.3.2",
"lexical": "^0.23.1",
"lodash": "^4.17.21",
"lucide-react": "^0.508.0",
"lucide-react": "^0.542.0",
"mammoth": "^1.7.2",
"next-themes": "^0.4.6",
"openai-speech-stream-player": "^1.0.8",
@ -25113,9 +25113,9 @@
}
},
"node_modules/lucide-react": {
"version": "0.508.0",
"resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.508.0.tgz",
"integrity": "sha512-gcP16PnexqtOFrTtv98kVsGzTfnbPekzZiQfByi2S89xfk7E/4uKE1USZqccIp58v42LqkO7MuwpCqshwSrJCg==",
"version": "0.542.0",
"resolved": "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.542.0.tgz",
"integrity": "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"

View File

@ -79,7 +79,7 @@
"jsencrypt": "^3.3.2",
"lexical": "^0.23.1",
"lodash": "^4.17.21",
"lucide-react": "^0.508.0",
"lucide-react": "^0.542.0",
"mammoth": "^1.7.2",
"next-themes": "^0.4.6",
"openai-speech-stream-player": "^1.0.8",

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1756884949583" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="11332" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M190.464 489.472h327.68v40.96h-327.68z" fill="#C7DCFE" p-id="11333"></path><path d="M482.34496 516.5056l111.26784-308.20352 38.54336 13.9264L520.86784 530.432z" fill="#C7DCFE" p-id="11334"></path><path d="M620.544 196.608m-122.88 0a122.88 122.88 0 1 0 245.76 0 122.88 122.88 0 1 0-245.76 0Z" fill="#8FB8FC" p-id="11335"></path><path d="M182.272 509.952m-122.88 0a122.88 122.88 0 1 0 245.76 0 122.88 122.88 0 1 0-245.76 0Z" fill="#C7DCFE" p-id="11336"></path><path d="M558.65344 520.9088l283.77088 163.84-20.48 35.47136-283.77088-163.84z" fill="#C7DCFE" p-id="11337"></path><path d="M841.728 686.08m-122.88 0a122.88 122.88 0 1 0 245.76 0 122.88 122.88 0 1 0-245.76 0Z" fill="#B3CEFE" p-id="11338"></path><path d="M448.67584 803.77856l49.60256-323.91168 40.48896 6.20544-49.60256 323.91168z" fill="#C7DCFE" p-id="11339"></path><path d="M512 530.432m-143.36 0a143.36 143.36 0 1 0 286.72 0 143.36 143.36 0 1 0-286.72 0Z" fill="#4185FF" p-id="11340"></path><path d="M462.848 843.776m-102.4 0a102.4 102.4 0 1 0 204.8 0 102.4 102.4 0 1 0-204.8 0Z" fill="#8FB8FC" p-id="11341"></path></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1757483419289" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="22299" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M998.765714 523.629714c13.824 0 25.014857 11.190857 25.014857 25.014857a475.282286 475.282286 0 0 1-875.593142 256.219429l-27.574858 55.149714a25.014857 25.014857 0 1 1-44.763428-22.454857l44.178286-88.283428a24.868571 24.868571 0 0 1 26.550857-25.526858 25.014857 25.014857 0 0 1 8.265143 0.804572l99.474285 24.868571a25.014857 25.014857 0 0 1-12.068571 48.566857l-46.372572-11.556571A425.252571 425.252571 0 0 0 973.750857 548.571429c0-13.897143 11.190857-25.014857 25.014857-25.014858zM430.957714 365.714286l6.729143 0.658285c2.633143 0.438857 285.549714 160.109714 285.549714 160.109715 20.114286 17.846857 7.314286 34.523429-6.582857 45.933714-1.828571 1.462857-194.779429 113.078857-249.929143 144.969143l-10.678857 6.217143-3.876571 2.194285c-16.676571 8.923429-39.497143 8.923429-47.250286-11.995428-0.877714-2.194286-2.267429-250.221714-2.56-303.396572L402.285714 400.457143l0.731429-0.512c0.731429-18.651429 8.265143-38.034286 34.669714-33.645714z m-15.945143-273.408a475.282286 475.282286 0 0 1 533.869715 200.045714l27.501714-55.149714a25.014857 25.014857 0 1 1 44.690286 22.454857l-44.105143 88.283428a24.868571 24.868571 0 0 1-26.624 25.526858 24.868571 24.868571 0 0 1-8.192-0.804572l-99.547429-24.868571a25.014857 25.014857 0 0 1 12.068572-48.566857l46.445714 11.629714A425.252571 425.252571 0 0 0 123.245714 548.571429a25.014857 25.014857 0 0 1-50.029714 0 475.282286 475.282286 0 0 1 341.796571-456.265143z" fill="#3BA05C" p-id="22300"></path></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,57 @@
// src/pages/dataset/file-logs/file-status-badge.tsx
import { FC } from 'react';
interface StatusBadgeProps {
status: 'Success' | 'Failed' | 'Running' | 'Pending';
}
const FileStatusBadge: FC<StatusBadgeProps> = ({ status }) => {
const getStatusColor = () => {
// #3ba05c → rgb(59, 160, 92) // state-success
// #d8494b → rgb(216, 73, 75) // state-error
// #00beb4 → rgb(0, 190, 180) // accent-primary
// #faad14 → rgb(250, 173, 20) // state-warning
switch (status) {
case 'Success':
return `bg-[rgba(59,160,92,0.1)] text-state-success`;
case 'Failed':
return `bg-[rgba(216,73,75,0.1)] text-state-error`;
case 'Running':
return `bg-[rgba(0,190,180,0.1)] text-accent-primary`;
case 'Pending':
return `bg-[rgba(250,173,20,0.1)] text-state-warning`;
default:
return 'bg-gray-500/10 text-white';
}
};
const getBgStatusColor = () => {
// #3ba05c → rgb(59, 160, 92) // state-success
// #d8494b → rgb(216, 73, 75) // state-error
// #00beb4 → rgb(0, 190, 180) // accent-primary
// #faad14 → rgb(250, 173, 20) // state-warning
switch (status) {
case 'Success':
return `bg-[rgba(59,160,92,1)] text-state-success`;
case 'Failed':
return `bg-[rgba(216,73,75,1)] text-state-error`;
case 'Running':
return `bg-[rgba(0,190,180,1)] text-accent-primary`;
case 'Pending':
return `bg-[rgba(250,173,20,1)] text-state-warning`;
default:
return 'bg-gray-500/10 text-white';
}
};
return (
<span
className={`inline-flex items-center w-[75px] px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(0.1)}`}
>
<div className={`w-1 h-1 mr-1 rounded-full ${getBgStatusColor()}`}></div>
{status}
</span>
);
};
export default FileStatusBadge;

View File

@ -1,6 +1,7 @@
'use client';
import { cn } from '@/lib/utils';
import { parseColorToRGBA } from '@/utils/common-util';
import { Slot } from '@radix-ui/react-slot';
import * as React from 'react';
@ -197,7 +198,208 @@ function TimelineTitle({
);
}
interface TimelineIndicatorNodeProps {
nodeSize?: string | number;
iconColor?: string;
lineColor?: string;
textColor?: string;
indicatorBgColor?: string;
indicatorBorderColor?: string;
}
interface TimelineNode
extends Omit<
React.HTMLAttributes<HTMLDivElement>,
'id' | 'title' | 'content'
>,
TimelineIndicatorNodeProps {
id: string | number;
title?: React.ReactNode;
content?: React.ReactNode;
date?: React.ReactNode;
icon?: React.ReactNode;
completed?: boolean;
clickable?: boolean;
activeStyle?: TimelineIndicatorNodeProps;
}
interface CustomTimelineProps extends React.HTMLAttributes<HTMLDivElement> {
nodes: TimelineNode[];
activeStep?: number;
nodeSize?: string | number;
onStepChange?: (step: number, id: string | number) => void;
orientation?: 'horizontal' | 'vertical';
lineStyle?: 'solid' | 'dashed';
lineColor?: string;
indicatorColor?: string;
defaultValue?: number;
activeStyle?: TimelineIndicatorNodeProps;
}
const CustomTimeline = ({
nodes,
activeStep,
nodeSize = 12,
onStepChange,
orientation = 'horizontal',
lineStyle = 'solid',
lineColor = 'var(--text-secondary)',
indicatorColor = 'var(--accent-primary)',
defaultValue = 1,
className,
activeStyle,
...props
}: CustomTimelineProps) => {
const [internalActiveStep, setInternalActiveStep] =
React.useState(defaultValue);
const _lineColor = `rgb(${parseColorToRGBA(lineColor)})`;
console.log(lineColor, _lineColor);
const currentActiveStep = activeStep ?? internalActiveStep;
const handleStepChange = (step: number, id: string | number) => {
if (activeStep === undefined) {
setInternalActiveStep(step);
}
onStepChange?.(step, id);
};
const [r, g, b] = parseColorToRGBA(indicatorColor);
return (
<Timeline
value={currentActiveStep}
onValueChange={(step) => handleStepChange(step, nodes[step - 1]?.id)}
orientation={orientation}
className={className}
{...props}
>
{nodes.map((node, index) => {
const step = index + 1;
const isCompleted = node.completed ?? step <= currentActiveStep;
const isActive = step === currentActiveStep;
const isClickable = node.clickable ?? true;
const _activeStyle = node.activeStyle ?? (activeStyle || {});
const _nodeSizeTemp =
isActive && _activeStyle?.nodeSize
? _activeStyle?.nodeSize
: node.nodeSize ?? nodeSize;
const _nodeSize =
typeof _nodeSizeTemp === 'number'
? `${_nodeSizeTemp}px`
: _nodeSizeTemp;
console.log('icon-size', nodeSize, node.nodeSize, _nodeSize);
// const activeStyle = _activeStyle || {};
return (
<TimelineItem
key={node.id}
step={step}
className={cn(
node.className,
isClickable &&
'cursor-pointer hover:opacity-80 transition-opacity',
isCompleted && 'data-[completed]:data-completed/timeline-item',
isActive && 'relative z-10',
)}
onClick={() => isClickable && handleStepChange(step, node.id)}
>
<TimelineSeparator
className={cn(
'group-data-[orientation=horizontal]/timeline:-top-6 group-data-[orientation=horizontal]/timeline:h-0.1 group-data-[orientation=horizontal]/timeline:-translate-y-1/2',
'group-data-[orientation=vertical]/timeline:-left-6 group-data-[orientation=vertical]/timeline:w-0.1 group-data-[orientation=vertical]/timeline:-translate-x-1/2 ',
// `group-data-[orientation=horizontal]/timeline:w-[calc(100%-0.5rem-1rem)] group-data-[orientation=vertical]/timeline:h-[calc(100%-1rem-1rem)] group-data-[orientation=vertical]/timeline:translate-y-7 group-data-[orientation=horizontal]/timeline:translate-x-7`,
)}
style={{
border:
lineStyle === 'dashed'
? `1px dashed ${isActive ? _activeStyle.lineColor || _lineColor : _lineColor}`
: lineStyle === 'solid'
? `1px solid ${isActive ? _activeStyle.lineColor || _lineColor : _lineColor}`
: 'none',
backgroundColor: 'transparent',
width:
orientation === 'horizontal'
? `calc(100% - ${_nodeSize} - 2px - 0.1rem)`
: '1px',
height:
orientation === 'vertical'
? `calc(100% - ${_nodeSize} - 2px - 0.1rem)`
: '1px',
transform: `translate(${
orientation === 'horizontal' ? `${_nodeSize}` : '0'
}, ${orientation === 'vertical' ? `${_nodeSize}` : '0'})`,
}}
/>
<TimelineIndicator
className={cn(
'flex items-center justify-center p-1',
isCompleted && 'bg-primary border-primary',
!isCompleted && 'border-text-secondary bg-bg-base',
)}
style={{
width: _nodeSize,
height: _nodeSize,
borderColor: isActive
? _activeStyle.indicatorBorderColor || indicatorColor
: isCompleted
? indicatorColor
: '',
// backgroundColor: isActive
// ? _activeStyle.indicatorBgColor || indicatorColor
// : isCompleted
// ? indicatorColor
// : '',
backgroundColor: isActive
? _activeStyle.indicatorBgColor ||
`rgba(${r}, ${g}, ${b}, 0.1)`
: isCompleted
? `rgba(${r}, ${g}, ${b}, 0.1)`
: '',
}}
>
{node.icon && (
<div
className={cn(
'text-current',
`w-[${_nodeSize}] h-[${_nodeSize}]`,
isActive &&
`text-primary w-[${_activeStyle.nodeSize || _nodeSize}] h-[${_activeStyle.nodeSize || _nodeSize}]`,
)}
style={{
color: isActive ? _activeStyle.iconColor : undefined,
}}
>
{node.icon}
</div>
)}
</TimelineIndicator>
<TimelineHeader>
{node.date && <TimelineDate>{node.date}</TimelineDate>}
<TimelineTitle
className={cn(
'text-sm font-medium',
isActive && _activeStyle.textColor
? `text-${_activeStyle.textColor}`
: '',
)}
style={{
color: isActive ? _activeStyle.textColor : undefined,
}}
>
{node.title}
</TimelineTitle>
</TimelineHeader>
{node.content && <TimelineContent>{node.content}</TimelineContent>}
</TimelineItem>
);
})}
</Timeline>
);
};
CustomTimeline.displayName = 'CustomTimeline';
export {
CustomTimeline,
Timeline,
TimelineContent,
TimelineDate,
@ -206,4 +408,5 @@ export {
TimelineItem,
TimelineSeparator,
TimelineTitle,
type TimelineNode,
};

View File

@ -3,10 +3,22 @@ import React from 'react';
interface SpotlightProps {
className?: string;
opcity?: number;
coverage?: number;
}
const Spotlight: React.FC<SpotlightProps> = ({ className }) => {
/**
*
* @param opcity 0~1 default 0.5
* @param coverage 0~100 default 60
* @returns
*/
const Spotlight: React.FC<SpotlightProps> = ({
className,
opcity = 0.5,
coverage = 60,
}) => {
const isDark = useIsDarkTheme();
const rgb = isDark ? '255, 255, 255' : '194, 221, 243';
return (
<div
className={`absolute inset-0 opacity-80 ${className} rounded-lg`}
@ -18,9 +30,7 @@ const Spotlight: React.FC<SpotlightProps> = ({ className }) => {
<div
className="absolute inset-0"
style={{
background: isDark
? 'radial-gradient(circle at 50% 190%, #fff4 0%, #fff0 60%)'
: 'radial-gradient(circle at 50% 190%, #E4F3FF 0%, #E4F3FF00 60%)',
background: `radial-gradient(circle at 50% 190%, rgba(${rgb},${opcity}) 0%, rgba(${rgb},0) ${coverage}%)`,
pointerEvents: 'none',
}}
></div>

View File

@ -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,
};
};

View File

@ -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: 'Parserdeepdoc',
rerunFromCurrentStep: 'Rerun From Current Step',
rerunFromCurrentStepTip: 'Changes detected. Click to re-run.',
},
},
};

View File

@ -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: '已修改,点击重新运行。',
},
},
};

View File

@ -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}
>
<AgentBackground></AgentBackground>
<Spotlight className="z-0" opcity={0.7} coverage={70} />
<Controls position={'bottom-center'} orientation="horizontal">
<ControlButton>
<Tooltip>

View File

@ -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<string[]>([]);
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 && (
<div className=" absolute top-2 right-6">
<RerunButton />
</div>
)}
<div
className={classNames(
{ [styles.pagePdfWrapper]: isPdf },
'flex flex-col w-3/5',
)}
>
<Spin spinning={loading} className={styles.spin} size="large">
<div className="h-[50px] flex flex-row justify-between items-end pb-[5px]">
<div>
<h2 className="text-[16px]">{t('chunk.chunkResult')}</h2>
<div className="text-[12px] text-text-secondary italic">
{t('chunk.chunkResultTip')}
</div>
</div>
<ChunkResultBar
handleInputChange={handleInputChange}
searchString={searchString}
changeChunkTextMode={changeChunkTextMode}
createChunk={showChunkUpdatingModal}
available={available}
selectAllChunk={selectAllChunk}
handleSetAvailable={handleSetAvailable}
/>
</div>
<div className=" rounded-[16px] box-border mb-2">
<div className="pt-[5px] pb-[5px]">
<CheckboxSets
selectAllChunk={selectAllChunk}
switchChunk={handleSwitchChunk}
removeChunk={handleRemoveChunk}
checked={selectedChunkIds.length === data.length}
selectedChunkIds={selectedChunkIds}
/>
</div>
<div className="h-[calc(100vh-280px)] overflow-y-auto pr-2 scrollbar-thin">
<div
className={classNames(
styles.chunkContainer,
{
[styles.chunkOtherContainer]: !isPdf,
},
'flex flex-col gap-4',
)}
>
{chunkList.map((item) => (
<ChunkCard
item={item}
key={item.chunk_id}
editChunk={showChunkUpdatingModal}
checked={selectedChunkIds.some((x) => x === item.chunk_id)}
handleCheckboxClick={handleSingleCheckboxClick}
switchChunk={handleSwitchChunk}
clickChunkCard={handleChunkCardClick}
selected={item.chunk_id === selectedChunkId}
textMode={textMode}
></ChunkCard>
))}
</div>
</div>
<div className={styles.pageFooter}>
<RAGFlowPagination
pageSize={pagination.pageSize}
current={pagination.current}
total={total}
onChange={(page, pageSize) => {
onPaginationChange(page, pageSize);
}}
></RAGFlowPagination>
</div>
</div>
</Spin>
</div>
{chunkUpdatingVisible && (
<CreatingModal
doc_id={documentId}
chunkId={chunkId}
hideModal={hideChunkUpdatingModal}
visible={chunkUpdatingVisible}
loading={chunkUpdatingLoading}
onOk={(e) => {
handleChunkEditSave(e);
}}
parserId={documentInfo.parser_id}
/>
)}
</>
);
};
export { ChunkerContainer };

View File

@ -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;
}

View File

@ -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<boolean>(false);
return (
<Card
className={classNames('rounded-lg w-full py-3 px-3', {
'bg-bg-title': selected,
'bg-bg-input': !selected,
})}
>
<div className="flex items-start justify-between gap-2">
<Checkbox onCheckedChange={handleCheck} checked={checked}></Checkbox>
{item.image_id && (
<Popover open={open}>
<PopoverTrigger
asChild
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
>
<div>
<Image id={item.image_id} className={styles.image}></Image>
</div>
</PopoverTrigger>
<PopoverContent
className="p-0"
align={'start'}
side={'right'}
sideOffset={-20}
>
<div>
<Image
id={item.image_id}
className={styles.imagePreview}
></Image>
</div>
</PopoverContent>
</Popover>
)}
<section
onDoubleClick={handleContentDoubleClick}
onClick={handleContentClick}
className={styles.content}
>
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(item.content_with_weight),
}}
className={classNames(styles.contentText, {
[styles.contentEllipsis]: textMode === ChunkTextMode.Ellipse,
})}
></div>
</section>
<div>
<Switch
checked={enabled}
onCheckedChange={onChange}
aria-readonly
className="!m-0"
/>
</div>
</div>
</Card>
);
};
export default ChunkCard;

View File

@ -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<IModalProps<any> & kFProps> = ({
doc_id,
chunkId,
hideModal,
onOk,
loading,
parserId,
}) => {
// const [form] = Form.useForm();
// const form = useFormContext();
const form = useForm<FieldValues>({
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 (
<Modal
title={`${chunkId ? t('common.edit') : t('common.create')} ${t('chunk.chunk')}`}
open={true}
onOk={handleOk}
onCancel={hideModal}
confirmLoading={loading}
destroyOnClose
>
<Form {...form}>
<div className="flex flex-col gap-4">
<FormField
control={form.control}
name="content_with_weight"
render={({ field }) => (
<FormItem>
<FormLabel>{t('chunk.chunk')}</FormLabel>
<FormControl>
<Textarea {...field} autoSize={{ minRows: 4, maxRows: 10 }} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="important_kwd"
render={({ field }) => (
<FormItem>
<FormLabel>{t('chunk.keyword')}</FormLabel>
<FormControl>
<EditTag {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="question_kwd"
render={({ field }) => (
<FormItem>
<FormLabel className="flex justify-start items-start">
<div className="flex items-center gap-0">
<span>{t('chunk.question')}</span>
<HoverCard>
<HoverCardTrigger asChild>
<span className="text-xs mt-[-3px] text-center scale-[90%] font-thin text-primary cursor-pointer rounded-full w-[16px] h-[16px] border-muted-foreground/50 border">
?
</span>
</HoverCardTrigger>
<HoverCardContent className="w-80" side="top">
{t('chunk.questionTip')}
</HoverCardContent>
</HoverCard>
</div>
</FormLabel>
<FormControl>
<EditTag {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{isTagParser && (
<FormField
control={form.control}
name="tag_kwd"
render={({ field }) => (
<FormItem>
<FormLabel>{t('knowledgeConfiguration.tagName')}</FormLabel>
<FormControl>
<EditTag {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
{!isTagParser && (
<FormProvider {...form}>
<TagFeatureItem />
</FormProvider>
)}
</div>
</Form>
{chunkId && (
<section>
<Divider />
<Space size={'large'}>
<div className="flex items-center gap-2">
{t('chunk.enabled')}
<Switch checked={checked} onCheckedChange={handleCheck} />
</div>
<div className="flex items-center gap-1" onClick={handleRemove}>
<Trash2 size={16} /> {t('common.delete')}
</div>
</Space>
</section>
)}
</Modal>
);
};
export default ChunkCreatingModal;

View File

@ -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 (
<FormField
control={form.control}
name={FieldKey as any}
render={() => (
<FormItem>
<FormLabel>{t('knowledgeConfiguration.tags')}</FormLabel>
<div>
{fields.map((item, name) => {
return (
<div key={item.id} className="flex gap-3 items-center mb-4">
<div className="flex flex-1 gap-8">
<FormField
control={form.control}
name={`${FieldKey}.${name}.tag` as any}
render={({ field }) => (
<FormItem className="w-2/3">
<FormControl className="w-full">
<div>
<SelectWithSearch
options={filterOptions(name)}
placeholder={t(
'knowledgeConfiguration.tagName',
)}
value={field.value}
onChange={field.onChange}
/>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`${FieldKey}.${name}.frequency`}
render={({ field }) => (
<FormItem>
<FormControl>
<NumberInput
value={field.value}
onChange={field.onChange}
placeholder={t(
'knowledgeConfiguration.frequency',
)}
max={10}
min={0}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<CircleMinus
onClick={() => remove(name)}
className="text-red-500"
/>
</div>
);
})}
<Button
variant="dashed"
className="w-full flex items-center justify-center gap-2"
onClick={() => append({ tag: '', frequency: 0 })}
>
<Plus size={16} />
{t('knowledgeConfiguration.addTag')}
</Button>
</div>
</FormItem>
)}
/>
);
};

View File

@ -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 (
<div className="flex gap-[40px] py-4 px-2">
<div className="flex items-center gap-3 cursor-pointer text-muted-foreground hover:text-text-primary">
<Checkbox
id="all_chunks_checkbox"
onCheckedChange={handleSelectAllCheck}
checked={checked}
className=" data-[state=checked]:bg-text-primary data-[state=checked]:border-text-primary data-[state=checked]:text-bg-base border-muted-foreground text-muted-foreground hover:text-bg-base hover:border-text-primary "
/>
<Label htmlFor="all_chunks_checkbox">{t('chunk.selectAll')}</Label>
</div>
{isSelected && (
<>
<div
className="flex items-center cursor-pointer text-muted-foreground hover:text-text-primary"
onClick={handleEnabledClick}
>
<CircleCheck size={16} />
<span className="block ml-1">{t('chunk.enable')}</span>
</div>
<div
className="flex items-center cursor-pointer text-muted-foreground hover:text-text-primary"
onClick={handleDisabledClick}
>
<Ban size={16} />
<span className="block ml-1">{t('chunk.disable')}</span>
</div>
<div
className="flex items-center cursor-pointer text-red-400 hover:text-red-500"
onClick={handleDeleteClick}
>
<Trash2 size={16} />
<span className="block ml-1">{t('chunk.delete')}</span>
</div>
</>
)}
</div>
);
};

View File

@ -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<React.SetStateAction<string | number>>;
available: number | undefined;
selectAllChunk: (value: boolean) => void;
handleSetAvailable: (value: number | undefined) => void;
createChunk: () => void;
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
searchString: string;
}
export default ({
changeChunkTextMode,
available,
selectAllChunk,
handleSetAvailable,
createChunk,
handleInputChange,
searchString,
}: ChunkResultBarProps) => {
const { t } = useTranslate('chunk');
const [textSelectValue, setTextSelectValue] = useState<string | number>(
ChunkTextMode.Full,
);
const handleFilterChange = (e: string | number) => {
const value = e === -1 ? undefined : (e as number);
selectAllChunk(false);
handleSetAvailable(value);
};
const filterContent = (
<div className="w-[200px]">
<Radio.Group onChange={handleFilterChange} value={available}>
<div className="flex flex-col gap-2 p-4">
<Radio value={-1}>{t('all')}</Radio>
<Radio value={1}>{t('enabled')}</Radio>
<Radio value={0}>{t('disabled')}</Radio>
</div>
</Radio.Group>
</div>
);
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 (
<div className="flex gap-2">
<div className="flex items-center gap-1 bg-bg-card text-muted-foreground w-fit h-[35px] rounded-md p-1">
{textSelectOptions.map((option) => (
<div
key={option.value}
className={cn(
'flex items-center cursor-pointer px-4 py-1 rounded-md',
{
'text-primary bg-bg-base': option.value === textSelectValue,
'text-text-primary': option.value !== textSelectValue,
},
)}
onClick={() => changeTextSelectValue(option.value)}
>
{option.label}
</div>
))}
</div>
<Input
className="bg-bg-card text-muted-foreground"
style={{ width: 200 }}
placeholder={t('search')}
icon={<SearchOutlined />}
onChange={handleInputChange}
value={searchString}
/>
<Popover>
<PopoverTrigger asChild>
<Button className="bg-bg-card text-muted-foreground hover:bg-card">
<ListFilter />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 w-[200px]">
{filterContent}
</PopoverContent>
</Popover>
<Button
onClick={() => createChunk()}
variant={'secondary'}
className="bg-bg-card text-muted-foreground hover:bg-card"
>
<Plus size={44} />
</Button>
</div>
);
};

View File

@ -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: (
<>
<Checkbox onChange={handleSelectAllCheck} checked={checked}>
<b>{t('selectAll')}</b>
</Checkbox>
</>
),
},
{ type: 'divider' },
{
key: '2',
label: (
<Space onClick={handleEnabledClick}>
<CheckCircleOutlined />
<b>{t('enabledSelected')}</b>
</Space>
),
},
{
key: '3',
label: (
<Space onClick={handleDisabledClick}>
<CloseCircleOutlined />
<b>{t('disabledSelected')}</b>
</Space>
),
},
{ type: 'divider' },
{
key: '4',
label: (
<Space onClick={handleDelete}>
<DeleteOutlined />
<b>{t('deleteSelected')}</b>
</Space>
),
},
];
}, [
checked,
handleSelectAllCheck,
handleDelete,
handleEnabledClick,
handleDisabledClick,
t,
]);
const content = (
<Menu style={{ width: 200 }} items={items} selectable={false} />
);
const handleFilterChange = (e: RadioChangeEvent) => {
selectAllChunk(false);
handleSetAvailable(e.target.value);
};
const filterContent = (
<Radio.Group onChange={handleFilterChange} value={available}>
<Space direction="vertical">
<Radio value={undefined}>{t('all')}</Radio>
<Radio value={1}>{t('enabled')}</Radio>
<Radio value={0}>{t('disabled')}</Radio>
</Space>
</Radio.Group>
);
return (
<Flex justify="space-between" align="center">
<Space size={'middle'}>
<Link
to={`/knowledge/${KnowledgeRouteKey.Dataset}?id=${knowledgeBaseId}`}
>
<ArrowLeftOutlined />
</Link>
<FilePdfOutlined />
<Text ellipsis={{ tooltip: documentInfo?.name }} style={{ width: 150 }}>
{documentInfo?.name}
</Text>
</Space>
<Space>
<Segmented
options={[
{ label: t(ChunkTextMode.Full), value: ChunkTextMode.Full },
{ label: t(ChunkTextMode.Ellipse), value: ChunkTextMode.Ellipse },
]}
onChange={changeChunkTextMode as SegmentedProps['onChange']}
/>
<Popover content={content} placement="bottom" arrow={false}>
<Button>
{t('bulk')}
<DownOutlined />
</Button>
</Popover>
{isShowSearchBox ? (
<Input
size="middle"
placeholder={t('search')}
prefix={<SearchOutlined />}
allowClear
onChange={handleInputChange}
onBlur={handleSearchBlur}
value={searchString}
/>
) : (
<Button icon={<SearchOutlined />} onClick={handleSearchIconClick} />
)}
<Popover content={filterContent} placement="bottom" arrow={false}>
<Button icon={<FilterIcon />} />
</Popover>
<Button
icon={<PlusOutlined />}
type="primary"
onClick={() => createChunk()}
/>
</Space>
</Flex>
);
};
export default ChunkToolBar;

View File

@ -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<FileViewerProps> = ({ url }) => {
const [data, setData] = useState<CSVData | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const containerRef = useRef<HTMLDivElement>(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 (
<div
ref={containerRef}
className={classNames(
'relative w-full h-full p-4 bg-background-paper border border-border-normal rounded-md',
'overflow-auto max-h-[80vh] p-2',
)}
>
{isLoading ? (
<div className="absolute inset-0 flex items-center justify-center">
<Spin />
</div>
) : data ? (
<table className="min-w-full divide-y divide-border-normal">
<thead className="bg-background-header-bar">
<tr>
{data.headers.map((header, index) => (
<th
key={`header-${index}`}
className="px-6 py-3 text-left text-sm font-medium text-text-primary"
>
{header}
</th>
))}
</tr>
</thead>
<tbody className="bg-background-paper divide-y divide-border-normal">
{data.rows.map((row, rowIndex) => (
<tr key={`row-${rowIndex}`}>
{row.map((cell, cellIndex) => (
<td
key={`cell-${rowIndex}-${cellIndex}`}
className="px-6 py-4 whitespace-nowrap text-sm text-text-secondary"
>
{cell || '-'}
</td>
))}
</tr>
))}
</tbody>
</table>
) : null}
</div>
);
};
export default CSVFileViewer;

View File

@ -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<DocPreviewerProps> = ({
className,
url,
}) => {
// const url = useGetDocumentUrl();
const [htmlContent, setHtmlContent] = useState<string>('');
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(/<p>/g, '<p class="mb-2">')
.replace(/<h(\d)>/g, '<h$1 class="font-semibold mt-4 mb-2">');
setHtmlContent(styledContent);
} catch (err) {
message.error('Document parsing failed');
console.error('Error parsing document:', err);
}
setLoading(false);
};
useEffect(() => {
if (url) {
fetchDocument();
}
}, [url]);
return (
<div
className={classNames(
'relative w-full h-full p-4 bg-background-paper border border-border-normal rounded-md',
className,
)}
>
{loading && (
<div className="absolute inset-0 flex items-center justify-center">
<Spin />
</div>
)}
{!loading && <div dangerouslySetInnerHTML={{ __html: htmlContent }} />}
</div>
);
};

View File

@ -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 (
<div>
<h2 className="text-[16px]">{name}</h2>
<div className="text-text-secondary text-[12px] pt-[5px]">
Size{sizeName} Uploaded Time{dateStr}
</div>
</div>
);
};

View File

@ -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<ExcelCsvPreviewerProps> = ({
className,
url,
}) => {
// const url = useGetDocumentUrl();
const { containerRef } = useFetchExcel(url);
return (
<div
ref={containerRef}
className={classNames(
'relative w-full h-full p-4 bg-background-paper border border-border-normal rounded-md excel-csv-previewer',
className,
)}
></div>
);
};

View File

@ -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<number>();
const [containerRef, setContainerRef] = useState<HTMLElement | null>(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 `<mark>${text}</mark>`;
}
if (text.trim() !== '' && pattern.match(text)) {
// return pattern.replace(text, (value) => `<mark>${value}</mark>`);
return `<mark>${text}</mark>`;
}
return text.replace(pattern, (value) => `<mark>${value}</mark>`);
}
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;
};

View File

@ -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<ImagePreviewerProps> = ({
className,
url,
}) => {
// const url = useGetDocumentUrl();
const [imageSrc, setImageSrc] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(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 (
<div
className={classNames(
'relative w-full h-full p-4 bg-background-paper border border-border-normal rounded-md image-previewer',
className,
)}
>
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center">
<Spin />
</div>
)}
{!isLoading && imageSrc && (
<div className="max-h-[80vh] overflow-auto p-2">
<img
src={imageSrc}
alt={'image'}
className="w-full h-auto max-w-full object-contain"
onLoad={() => URL.revokeObjectURL(imageSrc!)}
/>
</div>
)}
</div>
);
};

View File

@ -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);
}
}

View File

@ -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<IProps>) => {
return (
<>
{fileType === 'pdf' && highlights && setWidthAndHeight && (
<section className={styles.documentPreview}>
<PdfPreviewer
highlights={highlights}
setWidthAndHeight={setWidthAndHeight}
url={url}
></PdfPreviewer>
</section>
)}
{['doc', 'docx'].indexOf(fileType) > -1 && (
<section>
<DocPreviewer className={className} url={url} />
</section>
)}
{['txt', 'md'].indexOf(fileType) > -1 && (
<section>
<TxtPreviewer className={className} url={url} />
</section>
)}
{['visual'].indexOf(fileType) > -1 && (
<section>
<ImagePreviewer className={className} url={url} />
</section>
)}
{['pptx'].indexOf(fileType) > -1 && (
<section>
<PptPreviewer className={className} url={url} />
</section>
)}
{['xlsx'].indexOf(fileType) > -1 && (
<section>
<ExcelCsvPreviewer className={className} url={url} />
</section>
)}
{['csv'].indexOf(fileType) > -1 && (
<section>
<CSVFileViewer className={className} url={url} />
</section>
)}
</>
);
};
export default memo(Preview);

View File

@ -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 ? (
<div className="Highlight__popup">
{comment.emoji} {comment.text}
</div>
) : 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 (
<div
className={`${styles.documentContainer} rounded-[10px] overflow-hidden `}
>
<PdfLoader
url={url}
beforeLoad={
<div className="absolute inset-0 flex items-center justify-center">
<Spin />
</div>
}
workerSrc="/pdfjs-dist/pdf.worker.min.js"
errorMessage={<FileError>{error}</FileError>}
>
{(pdfDocument) => {
pdfDocument.getPage(1).then((page) => {
const viewport = page.getViewport({ scale: 1 });
const width = viewport.width;
const height = viewport.height;
setWidthAndHeight(width, height);
});
return (
<PdfHighlighter
pdfDocument={pdfDocument}
enableAreaSelection={(event) => 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 ? (
<Highlight
isScrolledTo={isScrolledTo}
position={highlight.position}
comment={highlight.comment}
/>
) : (
<AreaHighlight
isScrolledTo={isScrolledTo}
highlight={highlight}
onChange={() => {}}
/>
);
return (
<Popup
popupContent={<HighlightPopup {...highlight} />}
onMouseOver={(popupContent) =>
setTip(highlight, () => popupContent)
}
onMouseOut={hideTip}
key={index}
>
{component}
</Popup>
);
}}
highlights={state}
/>
);
}}
</PdfLoader>
</div>
);
};
export default memo(PdfPreview);

View File

@ -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<PptPreviewerProps> = ({
className,
url,
}) => {
// const url = useGetDocumentUrl();
const wrapper = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const fetchDocument = async () => {
const res = await request(url, {
method: 'GET',
responseType: 'blob',
onError: () => {
message.error('Document parsing failed');
console.error('Error loading document:', url);
},
});
console.log(res);
try {
const arrayBuffer = await res.data.arrayBuffer();
if (containerRef.current) {
let width = 500;
let height = 900;
if (containerRef.current) {
width = containerRef.current.clientWidth - 50;
height = containerRef.current.clientHeight - 50;
}
let pptxPrviewer = init(containerRef.current, {
width: width,
height: height,
});
pptxPrviewer.preview(arrayBuffer);
}
} catch (err) {
message.error('ppt parse failed');
}
};
useEffect(() => {
if (url) {
fetchDocument();
}
}, [url]);
return (
<div
ref={containerRef}
className={classNames(
'relative w-full h-full p-4 bg-background-paper border border-border-normal rounded-md ppt-previewer',
className,
)}
>
<div className="overflow-auto p-2">
<div className="flex flex-col gap-4">
<div ref={wrapper} />
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,56 @@
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';
type TxtPreviewerProps = { className?: string; url: string };
export const TxtPreviewer = ({ className, url }: TxtPreviewerProps) => {
// const url = useGetDocumentUrl();
const [loading, setLoading] = useState(false);
const [data, setData] = useState<string>('');
const fetchTxt = async () => {
setLoading(true);
const res = await request(url, {
method: 'GET',
responseType: 'blob',
onError: (err: any) => {
message.error('Failed to load file');
console.error('Error loading file:', err);
},
});
// blob to string
const reader = new FileReader();
reader.readAsText(res.data);
reader.onload = () => {
setData(reader.result as string);
setLoading(false);
console.log('file loaded successfully', reader.result);
};
console.log('file data:', res);
};
useEffect(() => {
if (url) {
fetchTxt();
} else {
setLoading(false);
setData('');
}
}, [url]);
return (
<div
className={classNames(
'relative w-full h-full p-4 bg-background-paper border border-border-normal rounded-md',
className,
)}
>
{loading && (
<div className="absolute inset-0 flex items-center justify-center">
<Spin />
</div>
)}
{!loading && <pre className="whitespace-pre-wrap p-2 ">{data}</pre>}
</div>
);
};

View File

@ -0,0 +1,48 @@
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import { useState } from 'react';
interface FormatPreserveEditorProps {
initialValue: string;
onSave: (value: string) => void;
className?: string;
}
const FormatPreserveEditor = ({
initialValue,
onSave,
className,
}: FormatPreserveEditorProps) => {
const [content, setContent] = useState(initialValue);
const [isEditing, setIsEditing] = useState(false);
const handleEdit = () => setIsEditing(true);
const handleSave = () => {
onSave(content);
setIsEditing(false);
};
return (
<div className="editor-container">
{isEditing ? (
<Textarea
className={cn(
'w-full h-full bg-transparent text-text-secondary',
className,
)}
value={content}
onChange={(e) => setContent(e.target.value)}
onBlur={handleSave}
autoSize={{ maxRows: 100 }}
autoFocus
/>
) : (
<pre className="text-text-secondary" onClick={handleEdit}>
{content}
</pre>
)}
</div>
);
};
export default FormatPreserveEditor;

View File

@ -0,0 +1,29 @@
import SvgIcon from '@/components/svg-icon';
import { Button } from '@/components/ui/button';
import { CircleAlert } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { useRerunDataflow } from '../../hooks';
interface RerunButtonProps {
className?: string;
}
const RerunButton = (props: RerunButtonProps) => {
const { t } = useTranslation();
const { loading } = useRerunDataflow();
const clickFunc = () => {
console.log('click rerun button');
};
return (
<div className="flex flex-col gap-2">
<div className="text-xs text-text-primary flex items-center gap-1">
<CircleAlert color="#d29e2d" strokeWidth={1} size={12} />
{t('dataflowParser.rerunFromCurrentStepTip')}
</div>
<Button onClick={clickFunc} disabled={loading}>
<SvgIcon name="rerun" width={16} />
{t('dataflowParser.rerunFromCurrentStep')}
</Button>
</div>
);
};
export default RerunButton;

View File

@ -0,0 +1,82 @@
import { CustomTimeline, TimelineNode } from '@/components/originui/timeline';
import {
CheckLine,
FilePlayIcon,
Grid3x2,
ListPlus,
PlayIcon,
} from 'lucide-react';
import { useMemo } from 'react';
export const TimelineNodeObj = {
begin: {
id: 1,
title: 'Begin',
icon: <PlayIcon size={13} />,
clickable: false,
},
parser: { id: 2, title: 'Parser', icon: <FilePlayIcon size={13} /> },
chunker: { id: 3, title: 'Chunker', icon: <Grid3x2 size={13} /> },
indexer: {
id: 4,
title: 'Indexer',
icon: <ListPlus size={13} />,
clickable: false,
},
complete: {
id: 5,
title: 'Complete',
icon: <CheckLine size={13} />,
clickable: false,
},
};
export interface TimelineDataFlowProps {
activeId: number | string;
activeFunc: (id: number | string) => void;
}
const TimelineDataFlow = ({ activeFunc, activeId }: TimelineDataFlowProps) => {
// const [activeStep, setActiveStep] = useState(2);
const timelineNodes: TimelineNode[] = useMemo(() => {
const nodes: TimelineNode[] = [];
Object.keys(TimelineNodeObj).forEach((key) => {
nodes.push({
...TimelineNodeObj[key as keyof typeof TimelineNodeObj],
className: 'w-32',
completed: false,
});
});
return nodes;
}, []);
const activeStep = useMemo(() => {
const index = timelineNodes.findIndex((node) => node.id === activeId);
return index > -1 ? index + 1 : 0;
}, [activeId, timelineNodes]);
const handleStepChange = (step: number, id: string | number) => {
// setActiveStep(step);
activeFunc?.(id);
console.log(step, id);
};
return (
<div className="">
<div>
<CustomTimeline
nodes={timelineNodes as TimelineNode[]}
activeStep={activeStep}
onStepChange={handleStepChange}
orientation="horizontal"
lineStyle="solid"
nodeSize={24}
activeStyle={{
nodeSize: 30,
iconColor: 'var(--accent-primary)',
textColor: 'var(--accent-primary)',
}}
/>
</div>
</div>
);
};
export default TimelineDataFlow;

View File

@ -0,0 +1,4 @@
export enum ChunkTextMode {
Full = 'full',
Ellipse = 'ellipse',
}

View File

@ -0,0 +1,185 @@
import message from '@/components/ui/message';
import {
useCreateChunk,
useDeleteChunk,
useSelectChunkList,
} from '@/hooks/chunk-hooks';
import { useSetModalState, useShowDeleteConfirm } from '@/hooks/common-hooks';
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
import { IChunk } from '@/interfaces/database/knowledge';
import { buildChunkHighlights } from '@/utils/document-util';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { IHighlight } from 'react-pdf-highlighter';
import { ChunkTextMode } from './constant';
export const useHandleChunkCardClick = () => {
const [selectedChunkId, setSelectedChunkId] = useState<string>('');
const handleChunkCardClick = useCallback((chunkId: string) => {
setSelectedChunkId(chunkId);
}, []);
return { handleChunkCardClick, selectedChunkId };
};
export const useGetSelectedChunk = (selectedChunkId: string) => {
const data = useSelectChunkList();
return (
data?.data?.find((x) => x.chunk_id === selectedChunkId) ?? ({} as IChunk)
);
};
export const useGetChunkHighlights = (selectedChunkId: string) => {
const [size, setSize] = useState({ width: 849, height: 1200 });
const selectedChunk: IChunk = useGetSelectedChunk(selectedChunkId);
const highlights: IHighlight[] = useMemo(() => {
return buildChunkHighlights(selectedChunk, size);
}, [selectedChunk, size]);
const setWidthAndHeight = useCallback((width: number, height: number) => {
setSize((pre) => {
if (pre.height !== height || pre.width !== width) {
return { height, width };
}
return pre;
});
}, []);
return { highlights, setWidthAndHeight };
};
// Switch chunk text to be fully displayed or ellipse
export const useChangeChunkTextMode = () => {
const [textMode, setTextMode] = useState<ChunkTextMode>(ChunkTextMode.Full);
const changeChunkTextMode = useCallback((mode: ChunkTextMode) => {
setTextMode(mode);
}, []);
return { textMode, changeChunkTextMode };
};
export const useDeleteChunkByIds = (): {
removeChunk: (chunkIds: string[], documentId: string) => Promise<number>;
} => {
const { deleteChunk } = useDeleteChunk();
const showDeleteConfirm = useShowDeleteConfirm();
const removeChunk = useCallback(
(chunkIds: string[], documentId: string) => () => {
return deleteChunk({ chunkIds, doc_id: documentId });
},
[deleteChunk],
);
const onRemoveChunk = useCallback(
(chunkIds: string[], documentId: string): Promise<number> => {
return showDeleteConfirm({ onOk: removeChunk(chunkIds, documentId) });
},
[removeChunk, showDeleteConfirm],
);
return {
removeChunk: onRemoveChunk,
};
};
export const useUpdateChunk = () => {
const [chunkId, setChunkId] = useState<string | undefined>('');
const {
visible: chunkUpdatingVisible,
hideModal: hideChunkUpdatingModal,
showModal,
} = useSetModalState();
const { createChunk, loading } = useCreateChunk();
const { documentId } = useGetKnowledgeSearchParams();
const onChunkUpdatingOk = useCallback(
async (params: IChunk) => {
const code = await createChunk({
...params,
doc_id: documentId,
chunk_id: chunkId,
});
if (code === 0) {
hideChunkUpdatingModal();
}
},
[createChunk, hideChunkUpdatingModal, chunkId, documentId],
);
const handleShowChunkUpdatingModal = useCallback(
async (id?: string) => {
setChunkId(id);
showModal();
},
[showModal],
);
return {
chunkUpdatingLoading: loading,
onChunkUpdatingOk,
chunkUpdatingVisible,
hideChunkUpdatingModal,
showChunkUpdatingModal: handleShowChunkUpdatingModal,
chunkId,
documentId,
};
};
export const useFetchParserList = () => {
const [loading, setLoading] = useState(false);
return {
loading,
};
};
export const useRerunDataflow = () => {
const [loading, setLoading] = useState(false);
return {
loading,
};
};
export const useFetchPaserText = () => {
const initialText =
'第一行文本\n\t第二行缩进文本\n第三行 多个空格 第一行文本\n\t第二行缩进文本\n第三行 ' +
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' +
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' +
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' +
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' +
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 ' +
'多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格第一行文本\n\t第二行缩进文本\n第三行 多个空格';
const [loading, setLoading] = useState(false);
const [data, setData] = useState<string>(initialText);
const { t } = useTranslation();
const queryClient = useQueryClient();
const {
// data,
// isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: ['createChunk'],
mutationFn: async (payload: any) => {
// let service = kbService.create_chunk;
// if (payload.chunk_id) {
// service = kbService.set_chunk;
// }
// const { data } = await service(payload);
// if (data.code === 0) {
message.success(t('message.created'));
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: ['fetchChunkList'] });
}, 1000); // Delay to ensure the list is updated
// }
// return data?.code;
},
});
return { data, loading, rerun: mutateAsync };
};

View File

@ -0,0 +1,96 @@
.chunkPage {
padding: 24px;
padding-top: 2px;
display: flex;
// height: calc(100vh - 112px);
height: 100vh;
flex-direction: column;
.filter {
margin: 10px 0;
display: flex;
height: 32px;
justify-content: space-between;
}
.pagePdfWrapper {
width: 60%;
}
.pageWrapper {
width: 100%;
}
.pageContent {
flex: 1;
width: 100%;
padding-right: 12px;
overflow-y: auto;
.spin {
min-height: 400px;
}
}
.documentPreview {
// width: 40%;
height: calc(100vh - 180px);
overflow: auto;
}
.chunkContainer {
display: flex;
height: calc(100vh - 332px);
}
.chunkOtherContainer {
width: 100%;
}
.pageFooter {
padding-top: 10px;
padding-right: 10px;
height: 32px;
}
}
.container {
height: 100px;
display: flex;
flex-direction: column;
justify-content: space-between;
.content {
display: flex;
justify-content: space-between;
.context {
flex: 1;
// width: 207px;
height: 88px;
overflow: hidden;
}
}
.footer {
height: 20px;
.text {
margin-left: 10px;
}
}
}
.card {
:global {
.ant-card-body {
padding: 10px;
margin: 0;
}
margin-bottom: 10px;
}
cursor: pointer;
}

View File

@ -0,0 +1,121 @@
import { useFetchNextChunkList } from '@/hooks/use-chunk-request';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import DocumentPreview from './components/document-preview';
import { useGetChunkHighlights, useHandleChunkCardClick } from './hooks';
import DocumentHeader from './components/document-preview/document-header';
import { PageHeader } from '@/components/page-header';
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb';
import {
QueryStringMap,
useNavigatePage,
} from '@/hooks/logic-hooks/navigate-hooks';
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
import { ChunkerContainer } from './chunker';
import { useGetDocumentUrl } from './components/document-preview/hooks';
import TimelineDataFlow, { TimelineNodeObj } from './components/time-line';
import styles from './index.less';
import ParserContainer from './parser';
const Chunk = () => {
const {
data: { documentInfo },
} = useFetchNextChunkList();
const { selectedChunkId } = useHandleChunkCardClick();
const [activeStepId, setActiveStepId] = useState<number | string>(0);
const { data: dataset } = useFetchKnowledgeBaseConfiguration();
const { t } = useTranslation();
const { navigateToDataset, getQueryString, navigateToDatasetList } =
useNavigatePage();
const fileUrl = useGetDocumentUrl();
const { highlights, setWidthAndHeight } =
useGetChunkHighlights(selectedChunkId);
const fileType = useMemo(() => {
switch (documentInfo?.type) {
case 'doc':
return documentInfo?.name.split('.').pop() || 'doc';
case 'visual':
case 'docx':
case 'txt':
case 'md':
case 'pdf':
return documentInfo?.type;
}
return 'unknown';
}, [documentInfo]);
const handleStepChange = (id: number | string) => {
setActiveStepId(id);
};
return (
<>
<PageHeader>
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink onClick={navigateToDatasetList}>
{t('knowledgeDetails.dataset')}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink
onClick={navigateToDataset(
getQueryString(QueryStringMap.id) as string,
)}
>
{dataset.name}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>{documentInfo?.name}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
</PageHeader>
<div className=" absolute ml-[50%] translate-x-[-50%] top-4 flex justify-center">
<TimelineDataFlow
activeFunc={handleStepChange}
activeId={activeStepId}
/>
</div>
<div className={styles.chunkPage}>
<div className="flex flex-none gap-8 border border-border mt-[26px] p-3 rounded-lg h-[calc(100vh-100px)]">
<div className="w-2/5">
<div className="h-[50px] flex flex-col justify-end pb-[5px]">
<DocumentHeader {...documentInfo} />
</div>
<section className={styles.documentPreview}>
<DocumentPreview
className={styles.documentPreview}
fileType={fileType}
highlights={highlights}
setWidthAndHeight={setWidthAndHeight}
url={fileUrl}
></DocumentPreview>
</section>
</div>
<div className="h-dvh border-r -mt-3"></div>
{activeStepId === TimelineNodeObj.chunker.id && <ChunkerContainer />}
{activeStepId === TimelineNodeObj.parser.id && <ParserContainer />}
</div>
</div>
</>
);
};
export default Chunk;

View File

@ -0,0 +1,58 @@
import Spotlight from '@/components/spotlight';
import { Spin } from '@/components/ui/spin';
import classNames from 'classnames';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import FormatPreserveEditor from './components/parse-editer';
import RerunButton from './components/rerun-button';
import { useFetchParserList, useFetchPaserText } from './hooks';
const ParserContainer = () => {
const { data: initialValue, rerun: onSave } = useFetchPaserText();
const { t } = useTranslation();
const { loading } = useFetchParserList();
const [initialText, setInitialText] = useState(initialValue);
const [isChange, setIsChange] = useState(false);
const handleSave = (newContent: string) => {
console.log('保存内容:', newContent);
if (newContent !== initialText) {
setIsChange(true);
onSave(newContent);
} else {
setIsChange(false);
}
// Here, the API is called to send newContent to the backend
};
return (
<>
{isChange && (
<div className=" absolute top-2 right-6">
<RerunButton />
</div>
)}
<div className={classNames('flex flex-col w-3/5')}>
<Spin spinning={loading} className="" size="large">
<div className="h-[50px] flex flex-col justify-end pb-[5px]">
<div>
<h2 className="text-[16px]">
{t('dataflowParser.parseSummary')}
</h2>
<div className="text-[12px] text-text-secondary italic ">
{t('dataflowParser.parseSummaryTip')}
</div>
</div>
</div>
<div className=" border rounded-lg p-[20px] box-border h-[calc(100vh-180px)] overflow-auto scrollbar-none">
<FormatPreserveEditor
initialValue={initialText}
onSave={handleSave}
className="!h-[calc(100vh-220px)]"
/>
<Spotlight opcity={0.6} coverage={60} />
</div>
</Spin>
</div>
</>
);
};
export default ParserContainer;

View File

@ -0,0 +1,24 @@
export type FormListItem = {
frequency: number;
tag: string;
};
export function transformTagFeaturesArrayToObject(
list: Array<FormListItem> = [],
) {
return list.reduce<Record<string, number>>((pre, cur) => {
pre[cur.tag] = cur.frequency;
return pre;
}, {});
}
export function transformTagFeaturesObjectToArray(
object: Record<string, number> = {},
) {
return Object.keys(object).reduce<Array<FormListItem>>((pre, key) => {
pre.push({ frequency: object[key], tag: key });
return pre;
}, []);
}

View File

@ -0,0 +1,9 @@
export enum LogTabs {
FILE_LOGS = 'fileLogs',
DATASET_LOGS = 'datasetLogs',
}
export enum processingType {
knowledgeGraph = 'knowledgeGraph',
raptor = 'raptor',
}

View File

@ -0,0 +1,91 @@
import { FilterButton } from '@/components/list-filter-bar';
import {
CheckboxFormMultipleProps,
FilterPopover,
} from '@/components/list-filter-bar/filter-popover';
import { Button } from '@/components/ui/button';
import { SearchInput } from '@/components/ui/input';
import { cn } from '@/lib/utils';
import { ChangeEventHandler, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { LogTabs } from './dataset-common';
interface IProps {
searchString?: string;
onSearchChange?: ChangeEventHandler<HTMLInputElement>;
active?: (typeof LogTabs)[keyof typeof LogTabs];
setActive?: (active: (typeof LogTabs)[keyof typeof LogTabs]) => void;
}
const DatasetFilter = (
props: IProps & Omit<CheckboxFormMultipleProps, 'setOpen'>,
) => {
const {
searchString,
onSearchChange,
value,
onChange,
filters,
onOpenChange,
active = LogTabs.FILE_LOGS,
setActive,
...rest
} = props;
const { t } = useTranslation();
const filterCount = useMemo(() => {
return typeof value === 'object' && value !== null
? Object.values(value).reduce((pre, cur) => {
return pre + cur.length;
}, 0)
: 0;
}, [value]);
return (
<div className="flex items-center justify-between mb-4">
<div className="flex space-x-2 bg-bg-card p-1 rounded-md">
<Button
className={cn(
'px-4 py-2 rounded-md hover:text-text-primary hover:bg-bg-base',
{
'bg-bg-base text-text-primary': active === LogTabs.FILE_LOGS,
'bg-transparent text-text-secondary ':
active !== LogTabs.FILE_LOGS,
},
)}
onClick={() => setActive?.(LogTabs.FILE_LOGS)}
>
{t('knowledgeDetails.fileLogs')}
</Button>
<Button
className={cn(
'px-4 py-2 rounded-md hover:text-text-primary hover:bg-bg-base',
{
'bg-bg-base text-text-primary': active === LogTabs.DATASET_LOGS,
'bg-transparent text-text-secondary ':
active !== LogTabs.DATASET_LOGS,
},
)}
onClick={() => setActive?.(LogTabs.DATASET_LOGS)}
>
{t('knowledgeDetails.datasetLogs')}
</Button>
</div>
<div className="flex items-center space-x-2">
<FilterPopover
value={value}
onChange={onChange}
filters={filters}
onOpenChange={onOpenChange}
>
<FilterButton count={filterCount}></FilterButton>
</FilterPopover>
<SearchInput
value={searchString}
onChange={onSearchChange}
className="w-32"
></SearchInput>
</div>
</div>
);
};
export { DatasetFilter };

View File

@ -0,0 +1,156 @@
import {
CircleQuestionMark,
Cpu,
FileChartLine,
HardDriveDownload,
} from 'lucide-react';
import { FC, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { LogTabs } from './dataset-common';
import { DatasetFilter } from './dataset-filter';
import FileLogsTable from './overview-table';
interface StatCardProps {
title: string;
value: number;
icon: JSX.Element;
children?: JSX.Element;
}
const StatCard: FC<StatCardProps> = ({ title, value, children, icon }) => {
return (
<div className="bg-bg-card p-4 rounded-lg border border-border flex flex-col gap-2">
<div className="flex items-center justify-between">
<h3 className="flex items-center gap-1 text-sm font-medium text-text-secondary">
{title}
<CircleQuestionMark size={12} />
</h3>
{icon}
</div>
<div className="text-2xl font-bold text-text-primary">{value}</div>
<div className="h-12 w-full flex items-center">
<div className="flex-1">{children}</div>
</div>
</div>
);
};
interface CardFooterProcessProps {
total: number;
completed: number;
success: number;
failed: number;
}
const CardFooterProcess: FC<CardFooterProcessProps> = ({
total,
completed,
success,
failed,
}) => {
const { t } = useTranslation();
const successPrecentage = (success / total) * 100;
const failedPrecentage = (failed / total) * 100;
return (
<div className="flex items-center flex-col gap-2">
<div className="flex justify-between w-full text-sm text-text-secondary">
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
{success}
<span>{t('knowledgeDetails.success')}</span>
</div>
<div className="flex items-center gap-1">
{failed}
<span>{t('knowledgeDetails.failed')}</span>
</div>
</div>
<div className="flex items-center gap-1">
{completed}
<span>{t('knowledgeDetails.completed')}</span>
</div>
</div>
<div className="w-full flex rounded-full h-3 bg-bg-card text-sm font-bold text-text-primary">
<div
className=" rounded-full h-3 bg-accent-primary"
style={{ width: successPrecentage + '%' }}
></div>
<div
className=" rounded-full h-3 bg-state-error"
style={{ width: failedPrecentage + '%' }}
></div>
</div>
</div>
);
};
const FileLogsPage: FC = () => {
const { t } = useTranslation();
const [active, setActive] = useState<(typeof LogTabs)[keyof typeof LogTabs]>(
LogTabs.FILE_LOGS,
);
const mockData = Array(30)
.fill(0)
.map((_, i) => ({
id: i === 0 ? '#952734' : `14`,
fileName: 'PRD for DealBees 1.2 (1).txt',
source: 'GitHub',
pipeline: i === 0 ? 'data demo for...' : i === 1 ? 'test' : 'kikis demo',
startDate: '14/03/2025 14:53:39',
task: i === 0 ? 'Parse' : 'Parser',
status:
i === 0
? 'Success'
: i === 1
? 'Failed'
: i === 2
? 'Running'
: 'Pending',
}));
const pagination = {
current: 1,
pageSize: 30,
total: 100,
};
const changeActiveLogs = (active: (typeof LogTabs)[keyof typeof LogTabs]) => {
setActive(active);
};
const handlePaginationChange = (page: number, pageSize: number) => {
console.log('Pagination changed:', { page, pageSize });
};
return (
<div className="p-5 min-w-[880px] border-border border rounded-lg mr-5">
{/* Stats Cards */}
<div className="grid grid-cols-3 md:grid-cols-3 gap-4 mb-6">
<StatCard title="Total Files" value={2827} icon={<FileChartLine />}>
<div>+7% from last week</div>
</StatCard>
<StatCard title="Downloading" value={28} icon={<HardDriveDownload />}>
<CardFooterProcess
total={100}
success={8}
failed={2}
completed={15}
/>
</StatCard>
<StatCard title="Processing" value={156} icon={<Cpu />}>
<CardFooterProcess total={20} success={8} failed={2} completed={15} />
</StatCard>
</div>
{/* Tabs & Search */}
<DatasetFilter active={active} setActive={changeActiveLogs} />
{/* Table */}
<FileLogsTable
data={mockData}
pagination={pagination}
setPagination={handlePaginationChange}
pageCount={10}
active={active}
/>
</div>
);
};
export default FileLogsPage;

View File

@ -0,0 +1,384 @@
import FileStatusBadge from '@/components/file-status-badge';
import { FileIcon } from '@/components/icon-font';
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import SvgIcon from '@/components/svg-icon';
import { Button } from '@/components/ui/button';
import { RAGFlowPagination } from '@/components/ui/ragflow-pagination';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { useTranslate } from '@/hooks/common-hooks';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import ProcessLogModal from '@/pages/datasets/process-log-modal';
import {
ColumnDef,
ColumnFiltersState,
SortingState,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
import { TFunction } from 'i18next';
import { ClipboardList, Eye } from 'lucide-react';
import { Dispatch, FC, SetStateAction, useMemo, useState } from 'react';
import { LogTabs, processingType } from './dataset-common';
interface DocumentLog {
id: string;
fileName: string;
source: string;
pipeline: string;
startDate: string;
task: string;
status: 'Success' | 'Failed' | 'Running' | 'Pending';
}
interface FileLogsTableProps {
data: DocumentLog[];
pageCount: number;
pagination: {
current: number;
pageSize: number;
total: number;
};
setPagination: (pagination: { page: number; pageSize: number }) => void;
loading?: boolean;
active: (typeof LogTabs)[keyof typeof LogTabs];
}
export const getFileLogsTableColumns = (
t: TFunction<'translation', string>,
setIsModalVisible: Dispatch<SetStateAction<boolean>>,
navigateToDataflowResult: (
id: string,
knowledgeId?: string | undefined,
) => () => void,
) => {
// const { t } = useTranslate('knowledgeDetails');
const columns: ColumnDef<DocumentLog>[] = [
{
id: 'select',
header: ({ table }) => (
<input
type="checkbox"
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
className="rounded bg-gray-900 text-blue-500 focus:ring-blue-500"
/>
),
cell: ({ row }) => (
<input
type="checkbox"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
className="rounded border-gray-600 bg-gray-900 text-blue-500 focus:ring-blue-500"
/>
),
},
{
accessorKey: 'id',
header: 'ID',
cell: ({ row }) => (
<div className="text-text-primary">{row.original.id}</div>
),
},
{
accessorKey: 'fileName',
header: t('fileName'),
cell: ({ row }) => (
<div
className="flex items-center gap-2 text-text-primary"
onClick={navigateToDataflowResult(
row.original.id,
row.original.kb_id,
)}
>
<FileIcon name={row.original.fileName}></FileIcon>
{row.original.fileName}
</div>
),
},
{
accessorKey: 'source',
header: t('source'),
cell: ({ row }) => (
<div className="text-text-primary">{row.original.source}</div>
),
},
{
accessorKey: 'pipeline',
header: t('dataPipeline'),
cell: ({ row }) => (
<div className="flex items-center gap-2 text-text-primary">
<RAGFlowAvatar
avatar={null}
name={row.original.pipeline}
className="size-4"
/>
{row.original.pipeline}
</div>
),
},
{
accessorKey: 'startDate',
header: t('startDate'),
cell: ({ row }) => (
<div className="text-text-primary">{row.original.startDate}</div>
),
},
{
accessorKey: 'task',
header: t('task'),
cell: ({ row }) => (
<div className="text-text-primary">{row.original.task}</div>
),
},
{
accessorKey: 'status',
header: t('status'),
cell: ({ row }) => <FileStatusBadge status={row.original.status} />,
},
{
id: 'operations',
header: t('operations'),
cell: ({ row }) => (
<div className="flex justify-start space-x-2">
<Button
variant="ghost"
size="sm"
className="p-1"
onClick={() => {
setIsModalVisible(true);
}}
>
<Eye />
</Button>
<Button
variant="ghost"
size="sm"
className="p-1"
onClick={navigateToDataflowResult(row.original.id)}
>
<ClipboardList />
</Button>
</div>
),
},
];
return columns;
};
export const getDatasetLogsTableColumns = (
t: TFunction<'translation', string>,
setIsModalVisible: Dispatch<SetStateAction<boolean>>,
) => {
// const { t } = useTranslate('knowledgeDetails');
const columns: ColumnDef<DocumentLog>[] = [
{
id: 'select',
header: ({ table }) => (
<input
type="checkbox"
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
className="rounded bg-gray-900 text-blue-500 focus:ring-blue-500"
/>
),
cell: ({ row }) => (
<input
type="checkbox"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
className="rounded border-gray-600 bg-gray-900 text-blue-500 focus:ring-blue-500"
/>
),
},
{
accessorKey: 'id',
header: 'ID',
cell: ({ row }) => (
<div className="text-text-primary">{row.original.id}</div>
),
},
{
accessorKey: 'startDate',
header: t('startDate'),
cell: ({ row }) => (
<div className="text-text-primary">{row.original.startDate}</div>
),
},
{
accessorKey: 'processingType',
header: t('processingType'),
cell: ({ row }) => (
<div className="flex items-center gap-2 text-text-primary">
{processingType.knowledgeGraph === row.original.processingType && (
<SvgIcon name={`data-flow/knowledgegraph`} width={24}></SvgIcon>
)}
{processingType.raptor === row.original.processingType && (
<SvgIcon name={`data-flow/raptor`} width={24}></SvgIcon>
)}
{row.original.processingType}
</div>
),
},
{
accessorKey: 'status',
header: t('status'),
cell: ({ row }) => <FileStatusBadge status={row.original.status} />,
},
{
id: 'operations',
header: t('operations'),
cell: ({ row }) => (
<div className="flex justify-start space-x-2">
<Button
variant="ghost"
size="sm"
className="p-1"
onClick={() => {
setIsModalVisible(true);
}}
>
<Eye />
</Button>
</div>
),
},
];
return columns;
};
const FileLogsTable: FC<FileLogsTableProps> = ({
data,
pagination,
setPagination,
loading,
active = LogTabs.FILE_LOGS,
}) => {
const [sorting, setSorting] = useState<SortingState>([]);
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
const [rowSelection, setRowSelection] = useState({});
const { t } = useTranslate('knowledgeDetails');
const [isModalVisible, setIsModalVisible] = useState(false);
const { navigateToDataflowResult } = useNavigatePage();
const columns = useMemo(() => {
console.log('columns', active);
return active === LogTabs.FILE_LOGS
? getFileLogsTableColumns(t, setIsModalVisible, navigateToDataflowResult)
: getDatasetLogsTableColumns(t, setIsModalVisible);
}, [active, t]);
const currentPagination = useMemo(
() => ({
pageIndex: (pagination.current || 1) - 1,
pageSize: pagination.pageSize || 10,
}),
[pagination],
);
const table = useReactTable({
data,
columns,
manualPagination: true,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
onSortingChange: setSorting,
onColumnFiltersChange: setColumnFilters,
onRowSelectionChange: setRowSelection,
state: {
sorting,
columnFilters,
rowSelection,
pagination: currentPagination,
},
pageCount: pagination.total
? Math.ceil(pagination.total / pagination.pageSize)
: 0,
});
const taskInfo = {
taskId: '#9527',
fileName: 'PRD for DealBees 1.2 (1).text',
fileSize: '2.4G',
source: 'Github',
task: 'Parse',
state: 'Running',
startTime: '14/03/2025 14:53:39',
duration: '800',
details: 'PRD for DealBees 1.2 (1).text',
};
return (
<div className="w-full h-[calc(100vh-350px)]">
<Table rootClassName="max-h-[calc(100vh-380px)]">
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody className="relative">
{table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && 'selected'}
className="group"
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
className={cell.column.columnDef.meta?.cellClassName}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
<div className="flex items-center justify-end py-4 absolute bottom-3 right-12">
<div className="space-x-2">
<RAGFlowPagination
{...{ current: pagination.current, pageSize: pagination.pageSize }}
total={pagination.total}
onChange={(page, pageSize) => setPagination({ page, pageSize })}
/>
</div>
</div>
<ProcessLogModal
visible={isModalVisible}
onCancel={() => setIsModalVisible(false)}
taskInfo={taskInfo}
/>
</div>
);
};
export default FileLogsTable;

View File

@ -0,0 +1,31 @@
import { Button } from '@/components/ui/button';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { NaiveConfiguration } from './naive';
import { SavingButton } from './saving-button';
export function ChunkMethodForm() {
const form = useFormContext();
const { t } = useTranslation();
return (
<section className="h-full flex flex-col">
<div className="overflow-auto flex-1 min-h-0">
<NaiveConfiguration></NaiveConfiguration>
</div>
<div className="text-right pt-4 flex justify-end gap-3">
<Button
type="reset"
className="bg-transparent text-color-white hover:bg-transparent border-gray-500 border-[1px]"
onClick={() => {
form.reset();
}}
>
{t('knowledgeConfiguration.cancel')}
</Button>
<SavingButton></SavingButton>
</div>
</section>
);
}

View File

@ -0,0 +1,155 @@
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
import { SliderInputFormField } from '@/components/slider-input-form-field';
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { MultiSelect } from '@/components/ui/multi-select';
import { useFetchKnowledgeList } from '@/hooks/knowledge-hooks';
import { Flex, Form, InputNumber, Select, Slider, Space } from 'antd';
import DOMPurify from 'dompurify';
import { useFormContext, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
export const TagSetItem = () => {
const { t } = useTranslation();
const form = useFormContext();
const { list: knowledgeList } = useFetchKnowledgeList(true);
const knowledgeOptions = knowledgeList
.filter((x) => x.parser_id === 'tag')
.map((x) => ({
label: x.name,
value: x.id,
icon: () => (
<Space>
<RAGFlowAvatar
name={x.name}
avatar={x.avatar}
className="size-4"
></RAGFlowAvatar>
</Space>
),
}));
return (
<FormField
control={form.control}
name="parser_config.tag_kb_ids"
render={({ field }) => (
<FormItem className=" items-center space-y-0 ">
<div className="flex items-center">
<FormLabel
className="text-sm text-muted-foreground whitespace-nowrap w-1/4"
tooltip={
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(
t('knowledgeConfiguration.tagSetTip'),
),
}}
></div>
}
>
{t('knowledgeConfiguration.tagSet')}
</FormLabel>
<div className="w-3/4">
<FormControl>
<MultiSelect
options={knowledgeOptions}
onValueChange={field.onChange}
placeholder={t('chat.knowledgeBasesMessage')}
variant="inverted"
maxCount={10}
{...field}
/>
</FormControl>
</div>
</div>
<div className="flex pt-1">
<div className="w-1/4"></div>
<FormMessage />
</div>
</FormItem>
)}
/>
);
return (
<Form.Item
label={t('knowledgeConfiguration.tagSet')}
name={['parser_config', 'tag_kb_ids']}
tooltip={
<div
dangerouslySetInnerHTML={{
__html: DOMPurify.sanitize(t('knowledgeConfiguration.tagSetTip')),
}}
></div>
}
rules={[
{
message: t('chat.knowledgeBasesMessage'),
type: 'array',
},
]}
>
<Select
mode="multiple"
options={knowledgeOptions}
placeholder={t('chat.knowledgeBasesMessage')}
></Select>
</Form.Item>
);
};
export const TopNTagsItem = () => {
const { t } = useTranslation();
return (
<SliderInputFormField
name={'parser_config.topn_tags'}
label={t('knowledgeConfiguration.topnTags')}
max={10}
min={1}
defaultValue={3}
></SliderInputFormField>
);
return (
<Form.Item label={t('knowledgeConfiguration.topnTags')}>
<Flex gap={20} align="center">
<Flex flex={1}>
<Form.Item
name={['parser_config', 'topn_tags']}
noStyle
initialValue={3}
>
<Slider max={10} min={1} style={{ width: '100%' }} />
</Form.Item>
</Flex>
<Form.Item name={['parser_config', 'topn_tags']} noStyle>
<InputNumber max={10} min={1} />
</Form.Item>
</Flex>
</Form.Item>
);
};
export function TagItems() {
const form = useFormContext();
const ids: string[] = useWatch({
control: form.control,
name: 'parser_config.tag_kb_ids',
});
return (
<>
<TagSetItem></TagSetItem>
{Array.isArray(ids) && ids.length > 0 && <TopNTagsItem></TopNTagsItem>}
</>
);
}

View File

@ -0,0 +1,14 @@
import { FormContainerProps } from '@/components/form-container';
import { cn } from '@/lib/utils';
import { PropsWithChildren } from 'react';
export function ConfigurationFormContainer({
children,
className,
}: FormContainerProps) {
return <section className={cn('space-y-4', className)}>{children}</section>;
}
export function MainContainer({ children }: PropsWithChildren) {
return <section className="space-y-5">{children}</section>;
}

View File

@ -0,0 +1,328 @@
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Radio } from '@/components/ui/radio';
import { RAGFlowSelect } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { useTranslate } from '@/hooks/common-hooks';
import { cn } from '@/lib/utils';
import { ArrowUpRight } from 'lucide-react';
import { useFormContext } from 'react-hook-form';
import {
useHasParsedDocument,
useSelectChunkMethodList,
useSelectEmbeddingModelOptions,
} from '../hooks';
export function ChunkMethodItem() {
const { t } = useTranslate('knowledgeConfiguration');
const form = useFormContext();
// const handleChunkMethodSelectChange = useHandleChunkMethodSelectChange(form);
const parserList = useSelectChunkMethodList();
return (
<FormField
control={form.control}
name={'parser_id'}
render={({ field }) => (
<FormItem className=" items-center space-y-0 ">
<div className="flex items-center">
<FormLabel
required
tooltip={t('chunkMethodTip')}
className="text-sm text-muted-foreground whitespace-wrap w-1/4"
>
{t('chunkMethod')}
</FormLabel>
<div className="w-3/4 ">
<FormControl>
<RAGFlowSelect
{...field}
options={parserList}
placeholder={t('chunkMethodPlaceholder')}
// onChange={handleChunkMethodSelectChange}
/>
</FormControl>
</div>
</div>
<div className="flex pt-1">
<div className="w-1/4"></div>
<FormMessage />
</div>
</FormItem>
)}
/>
);
}
export function EmbeddingModelItem({ line = 1 }: { line?: 1 | 2 }) {
const { t } = useTranslate('knowledgeConfiguration');
const form = useFormContext();
const embeddingModelOptions = useSelectEmbeddingModelOptions();
const disabled = useHasParsedDocument();
return (
<FormField
control={form.control}
name={'embd_id'}
render={({ field }) => (
<FormItem className=" items-center space-y-0 ">
<div className={cn({ 'flex items-center': line === 1 })}>
<FormLabel
required
tooltip={t('embeddingModelTip')}
className={cn('text-sm whitespace-wrap ', {
'w-1/4': line === 1,
})}
>
{t('embeddingModel')}
</FormLabel>
<div
className={cn('text-muted-foreground', { 'w-3/4': line === 1 })}
>
<FormControl>
<RAGFlowSelect
{...field}
options={embeddingModelOptions}
disabled={disabled}
placeholder={t('embeddingModelPlaceholder')}
/>
</FormControl>
</div>
</div>
<div className="flex pt-1">
<div className="w-1/4"></div>
<FormMessage />
</div>
</FormItem>
)}
/>
);
}
export function ParseTypeItem() {
const { t } = useTranslate('knowledgeConfiguration');
const form = useFormContext();
return (
<FormField
control={form.control}
name={'parseType'}
render={({ field }) => (
<FormItem className=" items-center space-y-0 ">
<div className="">
<FormLabel
tooltip={t('parseTypeTip')}
className="text-sm whitespace-wrap "
>
{t('parseType')}
</FormLabel>
<div className="text-muted-foreground">
<FormControl>
<Radio.Group {...field}>
<div className="w-3/4 flex gap-2 justify-between text-muted-foreground">
<Radio value={1}>{t('builtIn')}</Radio>
<Radio value={2}>{t('manualSetup')}</Radio>
</div>
</Radio.Group>
</FormControl>
</div>
</div>
<div className="flex pt-1">
<div className="w-1/4"></div>
<FormMessage />
</div>
</FormItem>
)}
/>
);
}
export function DataFlowItem() {
const { t } = useTranslate('knowledgeConfiguration');
const form = useFormContext();
return (
<FormField
control={form.control}
name={'data_flow'}
render={({ field }) => (
<FormItem className=" items-center space-y-0 ">
<div className="">
<div className="flex gap-2 justify-between ">
<FormLabel
tooltip={t('dataFlowTip')}
className="text-sm text-text-primary whitespace-wrap "
>
{t('dataFlow')}
</FormLabel>
<div className="text-sm flex text-text-primary">
{t('buildItFromScratch')}
<ArrowUpRight size={14} />
</div>
</div>
<div className="text-muted-foreground">
<FormControl>
<RAGFlowSelect
{...field}
placeholder={t('dataFlowPlaceholder')}
options={[{ value: '0', label: t('dataFlowDefault') }]}
/>
</FormControl>
</div>
</div>
<div className="flex pt-1">
<div className="w-1/4"></div>
<FormMessage />
</div>
</FormItem>
)}
/>
);
}
export function DataExtractKnowledgeItem() {
const { t } = useTranslate('knowledgeConfiguration');
const form = useFormContext();
return (
<>
{' '}
<FormField
control={form.control}
name={'extractKnowledgeGraph'}
render={({ field }) => (
<FormItem className=" items-center space-y-0 ">
<div className="">
<FormLabel
tooltip={t('extractKnowledgeGraphTip')}
className="text-sm whitespace-wrap "
>
{t('extractKnowledgeGraph')}
</FormLabel>
<div className="text-muted-foreground">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</div>
</div>
<div className="flex pt-1">
<div className="w-1/4"></div>
<FormMessage />
</div>
</FormItem>
)}
/>{' '}
<FormField
control={form.control}
name={'useRAPTORToEnhanceRetrieval'}
render={({ field }) => (
<FormItem className=" items-center space-y-0 ">
<div className="">
<FormLabel
tooltip={t('useRAPTORToEnhanceRetrievalTip')}
className="text-sm whitespace-wrap "
>
{t('useRAPTORToEnhanceRetrieval')}
</FormLabel>
<div className="text-muted-foreground">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</div>
</div>
<div className="flex pt-1">
<div className="w-1/4"></div>
<FormMessage />
</div>
</FormItem>
)}
/>
</>
);
}
export function TeamItem() {
const { t } = useTranslate('knowledgeConfiguration');
const form = useFormContext();
return (
<FormField
control={form.control}
name={'team'}
render={({ field }) => (
<FormItem className=" items-center space-y-0 ">
<div className="">
<FormLabel
tooltip={t('teamTip')}
className="text-sm whitespace-wrap "
>
<span className="text-destructive mr-1"> *</span>
{t('team')}
</FormLabel>
<div className="text-muted-foreground">
<FormControl>
<RAGFlowSelect
{...field}
placeholder={t('teamPlaceholder')}
options={[{ value: '0', label: t('teamDefault') }]}
/>
</FormControl>
</div>
</div>
<div className="flex pt-1">
<div className="w-1/4"></div>
<FormMessage />
</div>
</FormItem>
)}
/>
);
}
export function EnableAutoGenerateItem() {
const { t } = useTranslate('knowledgeConfiguration');
const form = useFormContext();
return (
<FormField
control={form.control}
name={'enableAutoGenerate'}
render={({ field }) => (
<FormItem className=" items-center space-y-0 ">
<div className="flex items-center">
<FormLabel
tooltip={t('enableAutoGenerateTip')}
className="text-sm whitespace-wrap w-1/4"
>
{t('enableAutoGenerate')}
</FormLabel>
<div className="text-muted-foreground w-3/4">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</div>
</div>
<div className="flex pt-1">
<div className="w-1/4"></div>
<FormMessage />
</div>
</FormItem>
)}
/>
);
}

View File

@ -0,0 +1,73 @@
import { z } from 'zod';
export const formSchema = z.object({
name: z.string().min(1, {
message: 'Username must be at least 2 characters.',
}),
description: z.string().min(2, {
message: 'Username must be at least 2 characters.',
}),
// avatar: z.instanceof(File),
avatar: z.any().nullish(),
permission: z.string().optional(),
parser_id: z.string(),
embd_id: z.string(),
parser_config: z
.object({
layout_recognize: z.string(),
chunk_token_num: z.number(),
delimiter: z.string(),
auto_keywords: z.number().optional(),
auto_questions: z.number().optional(),
html4excel: z.boolean(),
tag_kb_ids: z.array(z.string()).nullish(),
topn_tags: z.number().optional(),
raptor: z
.object({
use_raptor: z.boolean().optional(),
prompt: z.string().optional(),
max_token: z.number().optional(),
threshold: z.number().optional(),
max_cluster: z.number().optional(),
random_seed: z.number().optional(),
})
.refine(
(data) => {
if (data.use_raptor && !data.prompt) {
return false;
}
return true;
},
{
message: 'Prompt is required',
path: ['prompt'],
},
),
graphrag: z
.object({
use_graphrag: z.boolean().optional(),
entity_types: z.array(z.string()).optional(),
method: z.string().optional(),
resolution: z.boolean().optional(),
community: z.boolean().optional(),
})
.refine(
(data) => {
if (
data.use_graphrag &&
(!data.entity_types || data.entity_types.length === 0)
) {
return false;
}
return true;
},
{
message: 'Please enter Entity types',
path: ['entity_types'],
},
),
})
.optional(),
pagerank: z.number(),
// icon: z.array(z.instanceof(File)),
});

View File

@ -0,0 +1,92 @@
import { AvatarUpload } from '@/components/avatar-upload';
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { EmbeddingModelItem } from './configuration/common-item';
import { PermissionFormField } from './permission-form-field';
export function GeneralForm() {
const form = useFormContext();
const { t } = useTranslation();
return (
<section className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem className="items-center space-y-0">
<div className="flex">
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4">
<span className="text-red-600">*</span>
{t('common.name')}
</FormLabel>
<FormControl className="w-3/4">
<Input {...field}></Input>
</FormControl>
</div>
<div className="flex pt-1">
<div className="w-1/4"></div>
<FormMessage />
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="avatar"
render={({ field }) => (
<FormItem className="items-center space-y-0">
<div className="flex">
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4">
{t('setting.avatar')}
</FormLabel>
<FormControl className="w-3/4">
<AvatarUpload {...field}></AvatarUpload>
</FormControl>
</div>
<div className="flex pt-1">
<div className="w-1/4"></div>
<FormMessage />
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => {
// null initialize empty string
if (typeof field.value === 'object' && !field.value) {
form.setValue('description', ' ');
}
return (
<FormItem className="items-center space-y-0">
<div className="flex">
<FormLabel className="text-sm text-muted-foreground whitespace-nowrap w-1/4">
{t('flow.description')}
</FormLabel>
<FormControl className="w-3/4">
<Input {...field}></Input>
</FormControl>
</div>
<div className="flex pt-1">
<div className="w-1/4"></div>
<FormMessage />
</div>
</FormItem>
);
}}
/>
<PermissionFormField></PermissionFormField>
<EmbeddingModelItem></EmbeddingModelItem>
</section>
);
}

View File

@ -0,0 +1,87 @@
import { LlmModelType } from '@/constants/knowledge';
import { useSetModalState } from '@/hooks/common-hooks';
import { useSelectLlmOptionsByModelType } from '@/hooks/llm-hooks';
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
import { useSelectParserList } from '@/hooks/user-setting-hooks';
import { useIsFetching } from '@tanstack/react-query';
import { pick } from 'lodash';
import { useCallback, useEffect, useState } from 'react';
import { UseFormReturn } from 'react-hook-form';
import { z } from 'zod';
import { formSchema } from './form-schema';
// The value that does not need to be displayed in the analysis method Select
const HiddenFields = ['email', 'picture', 'audio'];
export function useSelectChunkMethodList() {
const parserList = useSelectParserList();
return parserList.filter((x) => !HiddenFields.some((y) => y === x.value));
}
export function useSelectEmbeddingModelOptions() {
const allOptions = useSelectLlmOptionsByModelType();
return allOptions[LlmModelType.Embedding];
}
export function useHasParsedDocument() {
const { data: knowledgeDetails } = useFetchKnowledgeBaseConfiguration();
return knowledgeDetails.chunk_num > 0;
}
export const useFetchKnowledgeConfigurationOnMount = (
form: UseFormReturn<z.infer<typeof formSchema>, any, undefined>,
) => {
const { data: knowledgeDetails } = useFetchKnowledgeBaseConfiguration();
useEffect(() => {
const parser_config = {
...form.formState?.defaultValues?.parser_config,
...knowledgeDetails.parser_config,
};
const formValues = {
...pick({ ...knowledgeDetails, parser_config: parser_config }, [
'description',
'name',
'permission',
'embd_id',
'parser_id',
'language',
'parser_config',
'pagerank',
'avatar',
]),
};
form.reset(formValues);
}, [form, knowledgeDetails]);
return knowledgeDetails;
};
export const useSelectKnowledgeDetailsLoading = () =>
useIsFetching({ queryKey: ['fetchKnowledgeDetail'] }) > 0;
export const useRenameKnowledgeTag = () => {
const [tag, setTag] = useState<string>('');
const {
visible: tagRenameVisible,
hideModal: hideTagRenameModal,
showModal: showFileRenameModal,
} = useSetModalState();
const handleShowTagRenameModal = useCallback(
(record: string) => {
setTag(record);
showFileRenameModal();
},
[showFileRenameModal],
);
return {
initialName: tag,
tagRenameVisible,
hideTagRenameModal,
showTagRenameModal: handleShowTagRenameModal,
};
};

View File

@ -0,0 +1,88 @@
import { Form } from '@/components/ui/form';
import { DocumentParserType } from '@/constants/knowledge';
import { PermissionRole } from '@/constants/permission';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { TopTitle } from '../dataset-title';
import { ChunkMethodForm } from './chunk-method-form';
import { formSchema } from './form-schema';
import { GeneralForm } from './general-form';
import { useFetchKnowledgeConfigurationOnMount } from './hooks';
const enum DocumentType {
DeepDOC = 'DeepDOC',
PlainText = 'Plain Text',
}
const initialEntityTypes = [
'organization',
'person',
'geo',
'event',
'category',
];
const enum MethodValue {
General = 'general',
Light = 'light',
}
export default function DatasetSettings() {
const { t } = useTranslation();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: '',
parser_id: DocumentParserType.Naive,
permission: PermissionRole.Me,
parser_config: {
layout_recognize: DocumentType.DeepDOC,
chunk_token_num: 512,
delimiter: `\n`,
auto_keywords: 0,
auto_questions: 0,
html4excel: false,
topn_tags: 3,
raptor: {
use_raptor: false,
},
graphrag: {
use_graphrag: false,
entity_types: initialEntityTypes,
method: MethodValue.Light,
},
},
pagerank: 0,
},
});
useFetchKnowledgeConfigurationOnMount(form);
async function onSubmit(data: z.infer<typeof formSchema>) {
console.log('🚀 ~ DatasetSettings ~ data:', data);
}
return (
<section className="p-5 h-full flex flex-col">
<TopTitle
title={t('knowledgeDetails.configuration')}
description={t('knowledgeConfiguration.titleDescription')}
></TopTitle>
<div className="flex gap-14 flex-1 min-h-0">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6 flex-1"
>
<div className="w-[768px]">
<GeneralForm></GeneralForm>
<ChunkMethodForm></ChunkMethodForm>
</div>
</form>
</Form>
</div>
</section>
);
}

View File

@ -0,0 +1,33 @@
import GraphRagItems from '@/components/parse-configuration/graph-rag-form-fields';
import RaptorFormFields from '@/components/parse-configuration/raptor-form-fields';
import {
ConfigurationFormContainer,
MainContainer,
} from './configuration-form-container';
import { EnableAutoGenerateItem } from './configuration/common-item';
export function NaiveConfiguration() {
return (
<MainContainer>
<GraphRagItems className="border-none p-0"></GraphRagItems>
<ConfigurationFormContainer>
<RaptorFormFields></RaptorFormFields>
</ConfigurationFormContainer>
<EnableAutoGenerateItem />
{/* <ConfigurationFormContainer>
<ChunkMethodItem></ChunkMethodItem>
<LayoutRecognizeFormField></LayoutRecognizeFormField>
<MaxTokenNumberFormField initialValue={512}></MaxTokenNumberFormField>
<DelimiterFormField></DelimiterFormField>
</ConfigurationFormContainer>
<ConfigurationFormContainer>
<PageRankFormField></PageRankFormField>
<AutoKeywordsFormField></AutoKeywordsFormField>
<AutoQuestionsFormField></AutoQuestionsFormField>
<ExcelToHtmlFormField></ExcelToHtmlFormField>
<TagItems></TagItems>
</ConfigurationFormContainer> */}
</MainContainer>
);
}

View File

@ -0,0 +1,29 @@
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { PermissionRole } from '@/constants/permission';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
export function PermissionFormField() {
const { t } = useTranslation();
const teamOptions = useMemo(() => {
return Object.values(PermissionRole).map((x) => ({
label: t('knowledgeConfiguration.' + x),
value: x,
}));
}, [t]);
return (
<RAGFlowFormItem
name="permission"
label={t('knowledgeConfiguration.permissions')}
tooltip={t('knowledgeConfiguration.permissionsTip')}
horizontal
>
<SelectWithSearch
options={teamOptions}
triggerClassName="w-3/4"
></SelectWithSearch>
</RAGFlowFormItem>
);
}

View File

@ -0,0 +1,82 @@
import { ButtonLoading } from '@/components/ui/button';
import { useUpdateKnowledge } from '@/hooks/use-knowledge-request';
import { useMemo } from 'react';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useParams } from 'umi';
export function GeneralSavingButton() {
const form = useFormContext();
const { saveKnowledgeConfiguration, loading: submitLoading } =
useUpdateKnowledge();
const { id: kb_id } = useParams();
const { t } = useTranslation();
const defaultValues = useMemo(
() => form.formState.defaultValues ?? {},
[form.formState.defaultValues],
);
const parser_id = defaultValues['parser_id'];
return (
<ButtonLoading
type="button"
loading={submitLoading}
onClick={() => {
(async () => {
let isValidate = await form.trigger('name');
const { name, description, permission, avatar } = form.getValues();
if (isValidate) {
saveKnowledgeConfiguration({
kb_id,
parser_id,
name,
description,
avatar,
permission,
});
}
})();
}}
>
{t('knowledgeConfiguration.save')}
</ButtonLoading>
);
}
export function SavingButton() {
const { saveKnowledgeConfiguration, loading: submitLoading } =
useUpdateKnowledge();
const form = useFormContext();
const { id: kb_id } = useParams();
const { t } = useTranslation();
return (
<ButtonLoading
loading={submitLoading}
onClick={() => {
(async () => {
try {
let beValid = await form.formControl.trigger();
if (beValid) {
form.handleSubmit(async (values) => {
console.log('saveKnowledgeConfiguration: ', values);
delete values['avatar'];
await saveKnowledgeConfiguration({
kb_id,
...values,
});
})();
}
} catch (e) {
console.log(e);
} finally {
}
})();
}}
>
{t('knowledgeConfiguration.save')}
</ButtonLoading>
);
}

View File

@ -0,0 +1,68 @@
import SvgIcon from '@/components/svg-icon';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { t } from 'i18next';
import { lowerFirst, toLower } from 'lodash';
import { WandSparkles } from 'lucide-react';
const MenuItem: React.FC<{ name: 'KnowledgeGraph' | 'Raptor' }> = ({
name,
}) => {
console.log(name, 'pppp');
return (
<div className="flex items-start gap-2 flex-col">
<div className="flex justify-start text-text-primary items-center gap-2">
<SvgIcon name={`data-flow/${toLower(name)}`} width={24}></SvgIcon>
{t(`knowledgeDetails.${lowerFirst(name)}`)}
</div>
<div className="text-text-secondary text-sm">
{t(`knowledgeDetails.generate${name}`)}
</div>
</div>
);
};
const Generate: React.FC = () => {
return (
<div className="generate">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant={'transparent'}>
<WandSparkles className="mr-2" />
{t('knowledgeDetails.generate')}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-[380px] p-2 ">
<DropdownMenuItem className="border cursor-pointer p-2 rounded-md hover:border-accent-primary hover:bg-[rgba(59,160,92,0.1)]">
<MenuItem name="KnowledgeGraph" />
</DropdownMenuItem>
<DropdownMenuItem
className="border cursor-pointer p-2 rounded-md mt-3 hover:border-accent-primary hover:bg-[rgba(59,160,92,0.1)]"
onSelect={(e) => {
e.preventDefault();
}}
onClick={(e) => {
e.stopPropagation();
}}
>
<MenuItem name="Raptor" />
{/* <div className="flex items-start gap-2 flex-col">
<div className="flex items-center gap-2">
<SvgIcon name={`data-flow/raptor`} width={24}></SvgIcon>
{t('knowledgeDetails.raptor')}
</div>
<div>{t('knowledgeDetails.generateRaptor')}</div>
</div> */}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};
export default Generate;

View File

@ -15,6 +15,7 @@ import { useFetchDocumentList } from '@/hooks/use-document-request';
import { Upload } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { DatasetTable } from './dataset-table';
import Generate from './generate';
import { useBulkOperateDataset } from './use-bulk-operate-dataset';
import { useCreateEmptyDocument } from './use-create-empty-document';
import { useSelectDatasetFilters } from './use-select-filters';
@ -59,6 +60,10 @@ export default function Dataset() {
setRowSelection,
});
return (
<>
<div className="absolute top-4 right-5">
<Generate />
</div>
<section className="p-5 min-w-[880px]">
<ListFilterBar
title="Dataset"
@ -123,5 +128,6 @@ export default function Dataset() {
></RenameDialog>
)}
</section>
</>
);
}

View File

@ -5,8 +5,11 @@ import {
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Radio } from '@/components/ui/radio';
import { RAGFlowSelect } from '@/components/ui/select';
import { Switch } from '@/components/ui/switch';
import { useTranslate } from '@/hooks/common-hooks';
import { ArrowUpRight } from 'lucide-react';
import { useFormContext } from 'react-hook-form';
import {
useHasParsedDocument,
@ -67,15 +70,16 @@ export function EmbeddingModelItem() {
name={'embd_id'}
render={({ field }) => (
<FormItem className=" items-center space-y-0 ">
<div className="flex items-center">
<div className="">
<FormLabel
required
tooltip={t('embeddingModelTip')}
className="text-sm text-muted-foreground whitespace-wrap w-1/4"
className="text-sm whitespace-wrap "
>
<span className="text-destructive mr-1"> *</span>
{t('embeddingModel')}
</FormLabel>
<div className="w-3/4">
<div className="text-muted-foreground">
<FormControl>
<RAGFlowSelect
{...field}
@ -95,3 +99,190 @@ export function EmbeddingModelItem() {
/>
);
}
export function ParseTypeItem() {
const { t } = useTranslate('knowledgeConfiguration');
const form = useFormContext();
return (
<FormField
control={form.control}
name={'parseType'}
render={({ field }) => (
<FormItem className=" items-center space-y-0 ">
<div className="">
<FormLabel
tooltip={t('parseTypeTip')}
className="text-sm whitespace-wrap "
>
{t('parseType')}
</FormLabel>
<div className="text-muted-foreground">
<FormControl>
<Radio.Group {...field}>
<div className="w-3/4 flex gap-2 justify-between text-muted-foreground">
<Radio value={1}>{t('builtIn')}</Radio>
<Radio value={2}>{t('manualSetup')}</Radio>
</div>
</Radio.Group>
</FormControl>
</div>
</div>
<div className="flex pt-1">
<div className="w-1/4"></div>
<FormMessage />
</div>
</FormItem>
)}
/>
);
}
export function DataFlowItem() {
const { t } = useTranslate('knowledgeConfiguration');
const form = useFormContext();
return (
<FormField
control={form.control}
name={'data_flow'}
render={({ field }) => (
<FormItem className=" items-center space-y-0 ">
<div className="">
<div className="flex gap-2 justify-between ">
<FormLabel
tooltip={t('dataFlowTip')}
className="text-sm text-text-primary whitespace-wrap "
>
{t('dataFlow')}
</FormLabel>
<div className="text-sm flex text-text-primary">
{t('buildItFromScratch')}
<ArrowUpRight size={14} />
</div>
</div>
<div className="text-muted-foreground">
<FormControl>
<RAGFlowSelect
{...field}
placeholder={t('dataFlowPlaceholder')}
options={[{ value: '0', label: t('dataFlowDefault') }]}
/>
</FormControl>
</div>
</div>
<div className="flex pt-1">
<div className="w-1/4"></div>
<FormMessage />
</div>
</FormItem>
)}
/>
);
}
export function DataExtractKnowledgeItem() {
const { t } = useTranslate('knowledgeConfiguration');
const form = useFormContext();
return (
<>
{' '}
<FormField
control={form.control}
name={'extractKnowledgeGraph'}
render={({ field }) => (
<FormItem className=" items-center space-y-0 ">
<div className="">
<FormLabel
tooltip={t('extractKnowledgeGraphTip')}
className="text-sm whitespace-wrap "
>
{t('extractKnowledgeGraph')}
</FormLabel>
<div className="text-muted-foreground">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</div>
</div>
<div className="flex pt-1">
<div className="w-1/4"></div>
<FormMessage />
</div>
</FormItem>
)}
/>{' '}
<FormField
control={form.control}
name={'useRAPTORToEnhanceRetrieval'}
render={({ field }) => (
<FormItem className=" items-center space-y-0 ">
<div className="">
<FormLabel
tooltip={t('useRAPTORToEnhanceRetrievalTip')}
className="text-sm whitespace-wrap "
>
{t('useRAPTORToEnhanceRetrieval')}
</FormLabel>
<div className="text-muted-foreground">
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</div>
</div>
<div className="flex pt-1">
<div className="w-1/4"></div>
<FormMessage />
</div>
</FormItem>
)}
/>
</>
);
}
export function TeamItem() {
const { t } = useTranslate('knowledgeConfiguration');
const form = useFormContext();
return (
<FormField
control={form.control}
name={'team'}
render={({ field }) => (
<FormItem className=" items-center space-y-0 ">
<div className="">
<FormLabel
tooltip={t('teamTip')}
className="text-sm whitespace-wrap "
>
<span className="text-destructive mr-1"> *</span>
{t('team')}
</FormLabel>
<div className="text-muted-foreground">
<FormControl>
<RAGFlowSelect
{...field}
placeholder={t('teamPlaceholder')}
options={[{ value: '0', label: t('teamDefault') }]}
/>
</FormControl>
</div>
</div>
<div className="flex pt-1">
<div className="w-1/4"></div>
<FormMessage />
</div>
</FormItem>
)}
/>
);
}

View File

@ -9,7 +9,13 @@ import { cn, formatBytes } from '@/lib/utils';
import { Routes } from '@/routes';
import { formatPureDate } from '@/utils/date';
import { isEmpty } from 'lodash';
import { Banknote, Database, FileSearch2, GitGraph } from 'lucide-react';
import {
Banknote,
Database,
DatabaseZap,
FileSearch2,
GitGraph,
} from 'lucide-react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useHandleMenuClick } from './hooks';
@ -28,6 +34,11 @@ export function SideBar({ refreshCount }: PropType) {
const items = useMemo(() => {
const list = [
{
icon: DatabaseZap,
label: t(`knowledgeDetails.overview`),
key: Routes.DataSetOverview,
},
{
icon: Database,
label: t(`knowledgeDetails.dataset`),

View File

@ -17,9 +17,16 @@ import {
import { Input } from '@/components/ui/input';
import { IModalProps } from '@/interfaces/common';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { useForm, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import {
DataExtractKnowledgeItem,
DataFlowItem,
EmbeddingModelItem,
ParseTypeItem,
TeamItem,
} from '../dataset/dataset-setting/configuration/common-item';
const FormId = 'dataset-creating-form';
@ -33,19 +40,24 @@ export function InputForm({ onOk }: IModalProps<any>) {
message: t('knowledgeList.namePlaceholder'),
})
.trim(),
parseType: z.number().optional(),
});
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
name: '',
parseType: 1,
},
});
function onSubmit(data: z.infer<typeof FormSchema>) {
onOk?.(data.name);
}
const parseType = useWatch({
control: form.control,
name: 'parseType',
});
return (
<Form {...form}>
<form
@ -58,7 +70,10 @@ export function InputForm({ onOk }: IModalProps<any>) {
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t('knowledgeList.name')}</FormLabel>
<FormLabel>
<span className="text-destructive mr-1"> *</span>
{t('knowledgeList.name')}
</FormLabel>
<FormControl>
<Input
placeholder={t('knowledgeList.namePlaceholder')}
@ -69,6 +84,15 @@ export function InputForm({ onOk }: IModalProps<any>) {
</FormItem>
)}
/>
<EmbeddingModelItem line={2} />
<ParseTypeItem />
{parseType === 2 && (
<>
<DataFlowItem />
<DataExtractKnowledgeItem />
<TeamItem />
</>
)}
</form>
</Form>
);

View File

@ -0,0 +1,116 @@
import { Button } from '@/components/ui/button';
import { Modal } from '@/components/ui/modal/modal';
import { useTranslate } from '@/hooks/common-hooks';
import React from 'react';
interface ProcessLogModalProps {
visible: boolean;
onCancel: () => void;
taskInfo: {
taskId: string;
fileName: string;
fileSize: string;
source: string;
task: string;
state: 'Running' | 'Completed' | 'Failed' | 'Pending';
startTime: string;
endTime?: string;
duration?: string;
details: string;
};
}
const StatusTag: React.FC<{ state: string }> = ({ state }) => {
const getTagStyle = () => {
switch (state) {
case 'Running':
return 'bg-green-500 text-green-100';
case 'Completed':
return 'bg-blue-500 text-blue-100';
case 'Failed':
return 'bg-red-500 text-red-100';
case 'Pending':
return 'bg-yellow-500 text-yellow-100';
default:
return 'bg-gray-500 text-gray-100';
}
};
return (
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getTagStyle()}`}
>
<span className="w-1.5 h-1.5 rounded-full mr-1 bg-current"></span>
{state}
</span>
);
};
const InfoItem: React.FC<{
label: string;
value: string | React.ReactNode;
className?: string;
}> = ({ label, value, className = '' }) => {
return (
<div className={`flex flex-col mb-4 ${className}`}>
<span className="text-text-secondary text-sm">{label}</span>
<span className="text-text-primary mt-1">{value}</span>
</div>
);
};
const ProcessLogModal: React.FC<ProcessLogModalProps> = ({
visible,
onCancel,
taskInfo,
}) => {
const { t } = useTranslate('knowledgeDetails');
return (
<Modal
title={t('processLog')}
open={visible}
onCancel={onCancel}
footer={
<div className="flex justify-end">
<Button onClick={onCancel}>{t('close')}</Button>
</div>
}
className="process-log-modal"
>
<div className="p-6 rounded-lg">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Left Column */}
<div className="space-y-4">
<InfoItem label="Task ID" value={taskInfo.taskId} />
<InfoItem label="File Name" value={taskInfo.fileName} />
<InfoItem label="File Size" value={taskInfo.fileSize} />
<InfoItem label="Source" value={taskInfo.source} />
<InfoItem label="Task" value={taskInfo.task} />
<InfoItem label="Details" value={taskInfo.details} />
</div>
{/* Right Column */}
<div className="space-y-4">
<div className="flex flex-col">
<span className="text-text-secondary text-sm">States</span>
<div className="mt-1">
<StatusTag state={taskInfo.state} />
</div>
</div>
<InfoItem label="Start Time" value={taskInfo.startTime} />
<InfoItem label="End Time" value={taskInfo.endTime || '-'} />
<InfoItem
label="Duration"
value={taskInfo.duration ? `${taskInfo.duration}s` : '-'}
/>
</div>
</div>
</div>
</Modal>
);
};
export default ProcessLogModal;

View File

@ -1,4 +1,5 @@
import { Input } from '@/components/originui/input';
import Spotlight from '@/components/spotlight';
import message from '@/components/ui/message';
import { IUserInfo } from '@/interfaces/database/user-setting';
import { cn } from '@/lib/utils';
@ -6,7 +7,6 @@ import { Search } from 'lucide-react';
import { Dispatch, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';
import './index.less';
import Spotlight from './spotlight';
export default function SearchPage({
isSearching,

View File

@ -44,6 +44,9 @@ export enum Routes {
UserSetting = '/user-setting',
DataFlows = '/data-flows',
DataFlow = '/data-flow',
DataSetOverview = '/dataset-overview',
DataSetSetting = '/dataset-setting',
DataflowResult = '/dataflow-result',
}
const routes = [
@ -273,8 +276,21 @@ const routes = [
path: `${Routes.DatasetBase}${Routes.KnowledgeGraph}/:id`,
component: `@/pages${Routes.DatasetBase}${Routes.KnowledgeGraph}`,
},
{
path: `${Routes.DatasetBase}${Routes.DataSetOverview}/:id`,
component: `@/pages${Routes.DatasetBase}${Routes.DataSetOverview}`,
},
{
path: `${Routes.DatasetBase}${Routes.DataSetSetting}/:id`,
component: `@/pages${Routes.DatasetBase}${Routes.DataSetSetting}`,
},
],
},
{
path: `${Routes.DataflowResult}/:id`,
layout: false,
component: `@/pages${Routes.DataflowResult}`,
},
{
path: `${Routes.ParsedResult}/chunks`,
layout: false,

View File

@ -141,3 +141,65 @@ export function formatFileSize(bytes: number, si = true, dp = 1) {
return nextBytes.toFixed(dp) + ' ' + units[u];
}
// Get the actual color value of a CSS variable
function getCSSVariableValue(variableName: string): string {
const computedStyle = getComputedStyle(document.documentElement);
const value = computedStyle.getPropertyValue(variableName).trim();
if (!value) {
throw new Error(`CSS variable ${variableName} is not defined`);
}
return value;
}
// Parse the color and convert to RGBA
export function parseColorToRGBA(color: string): [number, number, number] {
// Handling CSS variables (e.g. var(--accent-primary))
let colorStr = color;
if (colorStr.startsWith('var(')) {
const varMatch = color.match(/var\(([^)]+)\)/);
if (!varMatch) {
console.error(`Invalid CSS variable: ${color}`);
return [0, 0, 0];
}
const varName = varMatch[1];
if (!varName) {
console.error(`Invalid CSS variable: ${colorStr}`);
return [0, 0, 0];
}
colorStr = getCSSVariableValue(varName);
}
// Handles hexadecimal colors (e.g. #FF5733)
if (colorStr.startsWith('#')) {
const cleanedHex = colorStr.replace(/^#/, '');
if (cleanedHex.length === 3) {
return [
parseInt(cleanedHex[0] + cleanedHex[0], 16),
parseInt(cleanedHex[1] + cleanedHex[1], 16),
parseInt(cleanedHex[2] + cleanedHex[2], 16),
];
}
return [
parseInt(cleanedHex.slice(0, 2), 16),
parseInt(cleanedHex.slice(2, 4), 16),
parseInt(cleanedHex.slice(4, 6), 16),
];
}
// Handling RGB colors (e.g., rgb(255, 87, 51))
if (colorStr.startsWith('rgb')) {
const rgbMatch = colorStr.match(/rgb$$(\d+),\s*(\d+),\s*(\d+)$$/);
if (rgbMatch) {
return [
parseInt(rgbMatch[1]),
parseInt(rgbMatch[2]),
parseInt(rgbMatch[3]),
];
}
console.error(`Unsupported RGB format: ${colorStr}`);
return [0, 0, 0];
}
console.error(`Unsupported colorStr format: ${colorStr}`);
return [0, 0, 0];
}