Feature: Added data source functionality #10703 (#11046)

### What problem does this PR solve?

Feature: Added data source functionality

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
chanx
2025-11-06 11:53:46 +08:00
committed by GitHub
parent 15c75bbf15
commit f581a1c4e5
31 changed files with 2526 additions and 16 deletions

View File

@ -0,0 +1,14 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="discord">
<mask id="mask0_826_561469" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
<rect id="&#231;&#159;&#169;&#229;&#189;&#162;" width="24" height="24" fill="white"/>
</mask>
<g mask="url(#mask0_826_561469)">
</g>
<g id="Discord Icon SVG Vector Icon 1">
<g id="Group">
<path id="Vector" d="M18.2477 6.17085C17.0825 5.6257 15.8367 5.2295 14.5342 5.00391C14.3742 5.29311 14.1873 5.68211 14.0585 5.99155C12.6739 5.78332 11.302 5.78332 9.94287 5.99155C9.81404 5.68211 9.62292 5.29311 9.46152 5.00391C8.1576 5.2295 6.91032 5.62716 5.74514 6.17374C3.39498 9.72515 2.75789 13.1883 3.07644 16.6024C4.63519 17.7664 6.14581 18.4735 7.63093 18.9362C7.99762 18.4316 8.32465 17.8951 8.60638 17.3297C8.06981 17.1258 7.5559 16.8742 7.07031 16.5821C7.19913 16.4867 7.32514 16.3869 7.44689 16.2842C10.4086 17.6695 13.6267 17.6695 16.5531 16.2842C16.6762 16.3869 16.8022 16.4867 16.9296 16.5821C16.4426 16.8756 15.9273 17.1273 15.3907 17.3312C15.6724 17.8951 15.9981 18.433 16.3662 18.9377C17.8527 18.4749 19.3647 17.7678 20.9235 16.6024C21.2973 12.6446 20.285 9.21325 18.2477 6.17085ZM9.00988 14.5028C8.12079 14.5028 7.39166 13.6727 7.39166 12.662C7.39166 11.6512 8.10522 10.8198 9.00988 10.8198C9.91457 10.8198 10.6437 11.6498 10.6281 12.662C10.6295 13.6727 9.91457 14.5028 9.00988 14.5028ZM14.9901 14.5028C14.101 14.5028 13.3718 13.6727 13.3718 12.662C13.3718 11.6512 14.0854 10.8198 14.9901 10.8198C15.8947 10.8198 16.6238 11.6498 16.6083 12.662C16.6083 13.6727 15.8947 14.5028 14.9901 14.5028Z" fill="#5865F2"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,19 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="notion">
<rect id="&#231;&#159;&#169;&#229;&#189;&#162;" width="12" height="12" fill="#D8D8D8" fill-opacity="0.01"/>
<mask id="mask0_826_561463" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
<rect id="&#231;&#159;&#169;&#229;&#189;&#162;_2" width="24" height="24" fill="white"/>
</mask>
<g mask="url(#mask0_826_561463)">
</g>
<g id="Log in out &#226;&#128;&#147; Notion Help Center 1" clip-path="url(#clip0_826_561463)">
<path id="Vector" d="M4.6861 3.77652L14.6677 3.04092C15.8935 2.93592 16.2085 3.00672 16.9789 3.56592L20.1655 5.80572C20.6917 6.19092 20.8669 6.29592 20.8669 6.71532V18.9991C20.8669 19.7689 20.5861 20.2243 19.6057 20.2939L8.0143 20.9941C7.2775 21.0289 6.9277 20.9239 6.5419 20.4337L4.1959 17.3893C3.7741 16.8289 3.6001 16.4095 3.6001 15.9193V5.00052C3.6001 4.37112 3.8809 3.84612 4.6861 3.77652Z" fill="white"/>
<path id="Vector_2" fill-rule="evenodd" clip-rule="evenodd" d="M14.6683 3.04092L4.6849 3.77652C3.8809 3.84612 3.6001 4.37112 3.6001 5.00052V15.9193C3.6001 16.4095 3.7747 16.8289 4.1953 17.3893L6.5419 20.4337C6.9277 20.9239 7.2775 21.0289 8.0137 20.9941L19.6063 20.2939C20.5867 20.2243 20.8675 19.7689 20.8675 18.9991V6.71532C20.8675 6.31752 20.7097 6.20292 20.2453 5.86332L20.1661 5.80572L16.9801 3.56592C16.2091 3.00672 15.8941 2.93592 14.6677 3.04092H14.6683ZM8.2759 6.51432C7.3297 6.57792 7.1143 6.59232 6.5773 6.15612L5.2111 5.07132C5.0713 4.93092 5.1415 4.75572 5.4913 4.72092L15.0883 4.02132C15.8935 3.95112 16.3141 4.23132 16.6291 4.47612L18.2755 5.66592C18.3457 5.70132 18.5203 5.91072 18.3103 5.91072L8.3983 6.50592L8.2759 6.51432ZM7.1725 18.8941V8.46612C7.1725 8.01072 7.3129 7.80072 7.7329 7.76532L19.1155 7.10052C19.5013 7.06572 19.6765 7.31052 19.6765 7.76532V18.1237C19.6765 18.5791 19.6063 18.9643 18.9757 18.9991L8.0833 19.6291C7.4533 19.6639 7.1725 19.4545 7.1725 18.8941ZM17.9257 9.02532C17.9953 9.34032 17.9257 9.65532 17.6095 9.69132L17.0851 9.79512V17.4943C16.6291 17.7391 16.2091 17.8789 15.8587 17.8789C15.2983 17.8789 15.1579 17.7037 14.7379 17.1793L11.3053 11.7901V17.0041L12.3913 17.2495C12.3913 17.2495 12.3913 17.8795 11.5153 17.8795L9.0991 18.0193C9.0289 17.8789 9.0991 17.5291 9.3439 17.4595L9.9745 17.2849V10.3909L9.0991 10.3201C9.0289 10.0051 9.2035 9.55032 9.6943 9.51552L12.2863 9.34032L15.8587 14.8003V9.97032L14.9479 9.86592C14.8783 9.48012 15.1579 9.20052 15.5083 9.16572L17.9257 9.02532Z" fill="black"/>
</g>
</g>
<defs>
<clipPath id="clip0_826_561463">
<rect width="17.4" height="18" fill="white" transform="translate(3.6001 3)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,29 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="s3">
<mask id="mask0_826_561478" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
<rect id="&#231;&#159;&#169;&#229;&#189;&#162;" width="24" height="24" fill="white"/>
</mask>
<g mask="url(#mask0_826_561478)">
</g>
<g id="FileAmazon-S3-Logo - Wikimedia Commons 2" clip-path="url(#clip0_826_561478)">
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M6.63563 6.48047L9.35059 12.0352L6.63563 17.5898L5.52348 16.9219V7.14844L6.63563 6.48047Z" fill="#E25444"/>
<path id="Vector_2" fill-rule="evenodd" clip-rule="evenodd" d="M6.63566 6.48047L12.0656 7.14844L14.8296 12.0352L12.0656 16.9219L6.63566 17.5898V6.48047Z" fill="#7B1D13"/>
<path id="Vector_3" fill-rule="evenodd" clip-rule="evenodd" d="M17.5934 6.48047L18.4766 6.90234V17.168L17.5934 17.5898L12.0653 12.0352L17.5934 6.48047Z" fill="#58150D"/>
<path id="Vector_4" fill-rule="evenodd" clip-rule="evenodd" d="M17.6055 6.46875L12.0582 8.125V16.0937L17.6055 17.5937V6.46875Z" fill="#E25444"/>
<path id="Vector_5" fill-rule="evenodd" clip-rule="evenodd" d="M12.0552 9.00002L14.4106 8.59376L12.0552 5.71875L9.70566 8.59376L12.0552 9.00002Z" fill="#58150D"/>
<path id="Vector_6" fill-rule="evenodd" clip-rule="evenodd" d="M9.70566 8.59376L12.0582 9.00625L14.4106 8.59376V5.71875" fill="#58150D"/>
<path id="Vector_7" fill-rule="evenodd" clip-rule="evenodd" d="M12.0552 15.0312L14.4106 15.5L12.0552 17.9688L9.70566 15.5L12.0552 15.0312Z" fill="#58150D"/>
<path id="Vector_8" fill-rule="evenodd" clip-rule="evenodd" d="M12.0653 3.5625L14.4204 4.89844V8.58984L12.0579 7.87501L12.0653 3.5625Z" fill="#7B1D13"/>
<path id="Vector_9" fill-rule="evenodd" clip-rule="evenodd" d="M12.0582 9.90625L14.4106 10.1813V13.867L12.0582 14.1563V9.90625Z" fill="#7B1D13"/>
<path id="Vector_10" fill-rule="evenodd" clip-rule="evenodd" d="M12.0582 16.1249L14.4106 15.491V19.1143L12.0582 20.4375V16.1249Z" fill="#7B1D13"/>
<path id="Vector_11" fill-rule="evenodd" clip-rule="evenodd" d="M9.70561 15.491L12.0581 16.1251V20.4375L9.70561 19.1143V15.491Z" fill="#E25444"/>
<path id="Vector_12" fill-rule="evenodd" clip-rule="evenodd" d="M12.0581 9.90625L9.70561 10.1813V13.867L12.0581 14.1563V9.90625Z" fill="#E25444"/>
<path id="Vector_13" fill-rule="evenodd" clip-rule="evenodd" d="M12.0654 3.5625L9.71029 4.89844V8.58984L12.0654 7.88672V3.5625Z" fill="#E25444"/>
</g>
</g>
<defs>
<clipPath id="clip0_826_561478">
<rect width="14" height="18" fill="white" transform="matrix(-1 0 0 1 19 3)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,41 @@
import { cn } from '@/lib/utils';
import { ArrowBigLeft } from 'lucide-react';
import React from 'react';
import { useNavigate } from 'umi';
import { Button } from '../ui/button';
interface BackButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
to?: string;
}
const BackButton: React.FC<BackButtonProps> = ({
to,
className,
children,
...props
}) => {
const navigate = useNavigate();
const handleClick = () => {
if (to) {
navigate(to);
} else {
navigate(-1);
}
};
return (
<Button
variant="ghost"
className={cn('gap-2 bg-bg-card border border-border-default', className)}
onClick={handleClick}
{...props}
>
<ArrowBigLeft className="h-4 w-4" />
{children || 'Back'}
</Button>
);
};
export default BackButton;

View File

@ -8,9 +8,10 @@ interface StatusBadgeProps {
// status: 'Success' | 'Failed' | 'Running' | 'Pending';
status: RunningStatus;
name?: string;
className?: string;
}
const FileStatusBadge: FC<StatusBadgeProps> = ({ status, name }) => {
const FileStatusBadge: FC<StatusBadgeProps> = ({ status, name, className }) => {
const getStatusColor = () => {
// #3ba05c → rgb(59, 160, 92) // state-success
// #d8494b → rgb(216, 73, 75) // state-error
@ -51,7 +52,7 @@ const FileStatusBadge: FC<StatusBadgeProps> = ({ status, name }) => {
return (
<span
className={`inline-flex items-center w-[75px] px-2 py-1 rounded-full text-xs font-medium ${getStatusColor()}`}
className={`inline-flex items-center w-[75px] px-2 py-1 rounded-full text-xs font-medium ${getStatusColor()} ${className}`}
>
<div className={`w-1 h-1 mr-1 rounded-full ${getBgStatusColor()}`}></div>
{name || ''}

View File

@ -39,7 +39,7 @@ export function RAGFlowFormItem({
<FormItem
className={cn(
{
'flex items-center': horizontal,
'flex items-center w-full': horizontal,
},
className,
)}

View File

@ -13,6 +13,7 @@ export enum RunningStatus {
CANCEL = '2', // need to refresh
DONE = '3', // need to refresh
FAIL = '4', // need to refresh
SCHEDULE = '5',
}
export const RunningStatusMap = {
@ -21,6 +22,7 @@ export const RunningStatusMap = {
[RunningStatus.CANCEL]: 'Cancel',
[RunningStatus.DONE]: 'Success',
[RunningStatus.FAIL]: 'Failed',
[RunningStatus.SCHEDULE]: 'Schedule',
};
export enum ModelVariableType {

View File

@ -141,12 +141,21 @@ export const useNavigatePage = () => {
[navigate],
);
const navigateToDataSourceDetail = useCallback(
(id?: string) => {
navigate(
`${Routes.UserSetting}${Routes.DataSource}${Routes.DataSourceDetailPage}?id=${id}`,
);
},
[navigate],
);
const navigateToDataflowResult = useCallback(
(props: NavigateToDataflowResultProps) => () => {
let params: string[] = [];
Object.keys(props).forEach((key) => {
if (props[key]) {
params.push(`${key}=${props[key]}`);
if (props[key as keyof typeof props]) {
params.push(`${key}=${props[key as keyof typeof props]}`);
}
});
navigate(
@ -179,5 +188,6 @@ export const useNavigatePage = () => {
navigateToOldProfile,
navigateToDataflowResult,
navigateToDataFile,
navigateToDataSourceDetail,
};
};

View File

@ -1,6 +1,12 @@
import { RunningStatus } from '@/constants/knowledge';
import { DataSourceKey } from '@/pages/user-setting/data-source/contant';
import { TreeData } from '@antv/g6/lib/types';
export interface IConnector {
id: string;
name: string;
status: RunningStatus;
source: DataSourceKey;
}
// knowledge base
export interface IKnowledge {
avatar?: any;
@ -35,6 +41,7 @@ export interface IKnowledge {
mindmap_task_id?: string;
graphrag_task_finish_at: string;
graphrag_task_id: string;
connectors: IConnector[];
}
export interface IKnowledgeResult {

View File

@ -274,6 +274,9 @@ export default {
reRankModelWaring: 'Re-rank model is very time consuming.',
},
knowledgeConfiguration: {
dataSource: 'Data Source',
linkSourceSetTip: 'Manage data source linkage with this dataset',
linkDataSource: 'Link Data Source',
tocExtraction: 'TOC Enhance',
tocExtractionTip:
" For existing chunks, generate a hierarchical table of contents (one directory per file). During queries, when Directory Enhancement is activated, the system will use a large model to determine which directory items are relevant to the user's question, thereby identifying the relevant chunks.",
@ -680,6 +683,19 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
tocEnhanceTip: ` During the parsing of the document, table of contents information was generated (see the 'Enable Table of Contents Extraction' option in the General method). This allows the large model to return table of contents items relevant to the user's query, thereby using these items to retrieve related chunks and apply weighting to these chunks during the sorting process. This approach is derived from mimicking the behavioral logic of how humans search for knowledge in books.`,
},
setting: {
errorMsg: 'Error message',
newDocs: 'New Docs',
timeStarted: 'Time started',
log: 'Log',
s3Description:
'Connect to your AWS S3 bucket to import and sync stored files.',
discordDescription:
'Link your Discord server to access and analyze chat data.',
notionDescription:
'Sync pages and databases from Notion for knowledge retrieval.',
availableSourcesDescription: 'Select a data source to add',
availableSources: 'Available Sources',
datasourceDescription: 'Manage your data source and connections',
save: 'Save',
search: 'Search',
availableModels: 'Available models',
@ -697,6 +713,7 @@ This auto-tagging feature enhances retrieval by adding another layer of domain-s
'Please enter your current password to change your password.',
model: 'Model providers',
systemModelDescription: 'Please complete these settings before beginning',
dataSources: 'Data Sources',
team: 'Team',
system: 'System',
logout: 'Log out',
@ -1837,12 +1854,16 @@ Important structured information may include: names, dates, locations, events, k
changeStepModalConfirmText: 'Switch Anyway',
changeStepModalCancelText: 'Cancel',
unlinkPipelineModalTitle: 'Unlink Ingestion pipeline',
unlinkPipelineModalConfirmText: 'Unlink',
unlinkPipelineModalContent: `
<p>Once unlinked, this Dataset will no longer be connected to the current Ingestion pipeline.</p>
<p>Files that are already being parsed will continue until completion</p>
<p>Files that are not yet parsed will no longer be processed</p> <br/>
<p>Are you sure you want to proceed?</p> `,
unlinkPipelineModalConfirmText: 'Unlink',
unlinkSourceModalTitle: 'Unlink data source',
unlinkSourceModalContent: `
<p>Are you sure to unlink this data source </p>`,
unlinkSourceModalConfirmText: 'Unlink',
},
datasetOverview: {
downloadTip: 'Files being downloaded from data sources. ',

View File

@ -260,6 +260,9 @@ export default {
theDocumentBeingParsedCannotBeDeleted: '正在解析的文档不能被删除',
},
knowledgeConfiguration: {
dataSource: '数据源',
linkSourceSetTip: '管理与此数据集的数据源链接',
linkDataSource: '链接数据源',
tocExtractionTip:
'对于已有的chunk生成层级结构的目录信息每个文件一个目录。在查询时激活`目录增强`后系统会用大模型去判断用户问题和哪些目录项相关从而找到相关的chunk。',
deleteGenerateModalContent: `
@ -671,6 +674,16 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
tocEnhanceTip: `解析文档时生成了目录信息见General方法的启用目录抽取让大模型返回和用户问题相关的目录项从而利用目录项拿到相关chunk对这些chunk在排序中进行加权。这种方法来源于模仿人类查询书本中知识的行为逻辑`,
},
setting: {
errorMsg: '错误信息',
newDocs: '新文档',
timeStarted: '开始时间',
log: '日志',
s3Description: ' 连接你的 AWS S3 存储桶以导入和同步文件。',
discordDescription: ' 连接你的 Discord 服务器以访问和分析聊天数据。',
notionDescription: ' 同步 Notion 页面与数据库,用于知识检索。',
availableSourcesDescription: '选择要添加的数据源',
availableSources: '可用数据源',
datasourceDescription: '管理您的数据源和连接',
save: '保存',
search: '搜索',
availableModels: '可选模型',
@ -688,6 +701,7 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
passwordDescription: '请输入您当前的密码以更改您的密码。',
model: '模型提供商',
systemModelDescription: '请在开始之前完成这些设置',
dataSources: '数据源',
team: '团队',
system: '系统',
logout: '登出',
@ -1731,6 +1745,10 @@ Tokenizer 会根据所选方式将内容存储为对应的数据结构。`,
<p>尚未解析的文件将不再被处理。</p> <br/>
<p>你确定要继续吗?</p> `,
unlinkPipelineModalConfirmText: '解绑',
unlinkSourceModalTitle: '取消链接数据源',
unlinkSourceModalContent: `
<p>您确定要取消链接此数据源吗?</p>`,
unlinkSourceModalConfirmText: '取消链接',
},
datasetOverview: {
downloadTip: '正在从数据源下载文件。',

View File

@ -0,0 +1,97 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { cn } from '@/lib/utils';
import {
IDataSorceInfo,
IDataSourceBase,
} from '@/pages/user-setting/data-source/interface';
import { Check } from 'lucide-react';
import { useMemo } from 'react';
export type IAddedSourceCardProps = IDataSorceInfo & {
filterString: string;
list: IDataSourceBase[];
selectedList: IDataSourceBase[];
setSelectedList: (list: IDataSourceBase[]) => void;
};
export const AddedSourceCard = (props: IAddedSourceCardProps) => {
const {
list: originList,
name,
icon,
filterString,
selectedList,
setSelectedList,
} = props;
const list = useMemo(() => {
return originList.map((item) => {
const checked = selectedList?.some((i) => i.id === item.id) || false;
return {
...item,
checked: checked,
};
});
}, [originList, selectedList]);
const filterList = useMemo(
() => list.filter((item) => item.name.indexOf(filterString) > -1),
[filterString, list],
);
// const { navigateToDataSourceDetail } = useNavigatePage();
// const toDetail = (id: string) => {
// navigateToDataSourceDetail(id);
// };
const onCheck = (item: IDataSourceBase & { checked: boolean }) => {
if (item.checked) {
setSelectedList(selectedList.filter((i) => i.id !== item.id));
} else {
setSelectedList([...(selectedList || []), item]);
}
};
return (
<>
{filterList.length > 0 && (
<Card className="bg-transparent border border-border-button px-5 pt-[10px] pb-5 rounded-md">
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-0 pb-3">
{/* <Users className="mr-2 h-5 w-5 text-[#1677ff]" /> */}
<CardTitle className="text-base flex gap-1 font-normal">
{icon}
{name}
</CardTitle>
</CardHeader>
<CardContent className="p-2 flex flex-col gap-2">
{filterList.map((item) => (
<div
key={item.id}
className={cn(
'flex flex-row items-center justify-between rounded-md bg-bg-input px-2 py-1 cursor-pointer',
// { hidden: item.name.indexOf(filterString) <= -1 },
)}
onClick={() => {
console.log('item--->', item);
// toDetail(item.id);
onCheck(item);
}}
>
<div className="text-sm text-text-secondary ">{item.name}</div>
<div className="text-sm text-text-secondary flex gap-2">
{item.checked && (
<Check
className="cursor-pointer"
size={14}
// onClick={() => {
// toDetail(item.id);
// }}
/>
)}
</div>
</div>
))}
</CardContent>
</Card>
)}
</>
);
};

View File

@ -0,0 +1,86 @@
import { Button } from '@/components/ui/button';
import { SearchInput } from '@/components/ui/input';
import { Modal } from '@/components/ui/modal/modal';
import { IConnector } from '@/interfaces/database/knowledge';
import { useListDataSource } from '@/pages/user-setting/data-source/hooks';
import { IDataSourceBase } from '@/pages/user-setting/data-source/interface';
import { t } from 'i18next';
import { useEffect, useState } from 'react';
import { AddedSourceCard } from './added-source-card';
const LinkDataSourceModal = ({
selectedList,
open,
setOpen,
onSubmit,
}: {
selectedList: IConnector[];
open: boolean;
setOpen: (open: boolean) => void;
onSubmit?: (list: IDataSourceBase[] | undefined) => void;
}) => {
const [list, setList] = useState<IDataSourceBase[]>();
const [fileterString, setFileterString] = useState('');
useEffect(() => {
setList(selectedList);
}, [selectedList]);
const { categorizedList } = useListDataSource();
const handleFormSubmit = (values: any) => {
console.log(values, selectedList);
onSubmit?.(list);
};
return (
<Modal
className="!w-[560px]"
title={t('knowledgeConfiguration.linkDataSource')}
open={open}
onCancel={() => {
setList(selectedList);
}}
onOpenChange={setOpen}
showfooter={false}
>
<div className="flex flex-col gap-4 ">
{/* {JSON.stringify(selectedList)} */}
<SearchInput
value={fileterString}
onChange={(e) => setFileterString(e.target.value)}
/>
<div className="flex flex-col gap-3">
{categorizedList.map((item, index) => (
<AddedSourceCard
key={index}
selectedList={list as IDataSourceBase[]}
setSelectedList={(list) => setList(list)}
filterString={fileterString}
{...item}
/>
))}
</div>
<div className="flex justify-end gap-1">
<Button
type="button"
variant={'outline'}
className="btn-primary"
onClick={() => {
setOpen(false);
}}
>
{t('modal.cancelText')}
</Button>
<Button
type="button"
variant={'default'}
className="btn-primary"
onClick={handleFormSubmit}
>
{t('modal.okText')}
</Button>
</div>
</div>
</Modal>
);
};
export default LinkDataSourceModal;

View File

@ -0,0 +1,193 @@
import { Button } from '@/components/ui/button';
import { Modal } from '@/components/ui/modal/modal';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { IConnector } from '@/interfaces/database/knowledge';
import { DataSourceInfo } from '@/pages/user-setting/data-source/contant';
import { IDataSourceBase } from '@/pages/user-setting/data-source/interface';
import { Link, Settings, Unlink } from 'lucide-react';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import LinkDataSourceModal from './link-data-source-modal';
export type IDataSourceNodeProps = IConnector & {
icon: React.ReactNode;
};
export interface ILinkDataSourceProps {
data?: IConnector[];
handleLinkOrEditSubmit?: (data: IDataSourceBase[] | undefined) => void;
unbindFunc?: (item: DataSourceItemProps) => void;
}
interface DataSourceItemProps extends IDataSourceNodeProps {
openLinkModalFunc?: (open: boolean, data?: IDataSourceNodeProps) => void;
unbindFunc?: (item: DataSourceItemProps) => void;
}
const DataSourceItem = (props: DataSourceItemProps) => {
const { t } = useTranslation();
const { id, name, icon, openLinkModalFunc, unbindFunc } = props;
const { navigateToDataSourceDetail } = useNavigatePage();
const toDetail = (id: string) => {
navigateToDataSourceDetail(id);
};
const openUnlinkModal = () => {
Modal.show({
visible: true,
className: '!w-[560px]',
title: t('dataflowParser.unlinkSourceModalTitle'),
children: (
<div
className="text-sm text-text-secondary"
dangerouslySetInnerHTML={{
__html: t('dataflowParser.unlinkSourceModalContent'),
}}
></div>
),
onVisibleChange: () => {
Modal.hide();
},
footer: (
<div className="flex justify-end gap-2">
<Button variant={'outline'} onClick={() => Modal.hide()}>
{t('dataflowParser.changeStepModalCancelText')}
</Button>
<Button
variant={'secondary'}
className="!bg-state-error text-bg-base"
onClick={() => {
unbindFunc?.(props);
Modal.hide();
}}
>
{t('dataflowParser.unlinkSourceModalConfirmText')}
</Button>
</div>
),
});
};
return (
<div className="flex items-center justify-between gap-1 px-2 rounded-md border ">
<div className="flex items-center gap-1">
{icon}
<div>{name}</div>
</div>
<div className="flex gap-1 items-center">
<Button
variant={'transparent'}
className="border-none"
type="button"
onClick={() => {
toDetail(id);
}}
// onClick={() =>
// openLinkModalFunc?.(true, { ...omit(props, ['openLinkModalFunc']) })
// }
>
<Settings />
</Button>
<>
<Button
type="button"
variant={'transparent'}
className="border-none"
onClick={() => {
openUnlinkModal();
}}
>
<Unlink />
</Button>
</>
</div>
</div>
);
};
const LinkDataSource = (props: ILinkDataSourceProps) => {
const { data, handleLinkOrEditSubmit: submit, unbindFunc } = props;
const { t } = useTranslation();
const [openLinkModal, setOpenLinkModal] = useState(false);
const pipelineNode: IDataSourceNodeProps[] = useMemo(() => {
if (data && data.length > 0) {
return data.map((item) => {
return {
...item,
id: item?.id,
name: item?.name,
icon:
DataSourceInfo[item?.source as keyof typeof DataSourceInfo]?.icon ||
'',
} as IDataSourceNodeProps;
});
}
return [];
}, [data]);
const openLinkModalFunc = (open: boolean, data?: IDataSourceNodeProps) => {
console.log('open', open, data);
setOpenLinkModal(open);
// if (data) {
// setCurrentDataSource(data);
// } else {
// setCurrentDataSource(undefined);
// }
};
const handleLinkOrEditSubmit = (data: IDataSourceBase[] | undefined) => {
console.log('handleLinkOrEditSubmit', data);
submit?.(data);
setOpenLinkModal(false);
};
return (
<div className="flex flex-col gap-2">
<section className="flex flex-col">
<div className="flex items-center gap-1 text-text-primary text-sm">
{t('knowledgeConfiguration.dataSource')}
</div>
<div className="flex justify-between items-center">
<div className="text-center text-xs text-text-secondary">
{t('knowledgeConfiguration.linkSourceSetTip')}
</div>
<Button
type="button"
variant={'transparent'}
onClick={() => {
openLinkModalFunc?.(true);
}}
>
<Link />
<span className="text-xs text-text-primary">
{t('knowledgeConfiguration.linkDataSource')}
</span>
</Button>
</div>
</section>
<section className="flex flex-col gap-2">
{pipelineNode.map(
(item) =>
item.id && (
<DataSourceItem
key={item.id}
openLinkModalFunc={openLinkModalFunc}
unbindFunc={unbindFunc}
{...item}
/>
),
)}
</section>
<LinkDataSourceModal
selectedList={data as IConnector[]}
open={openLinkModal}
setOpen={(open: boolean) => {
openLinkModalFunc(open);
}}
onSubmit={handleLinkOrEditSubmit}
/>
</div>
);
};
export default LinkDataSource;

View File

@ -76,6 +76,16 @@ export const formSchema = z
})
.optional(),
pagerank: z.number(),
connectors: z
.array(
z.object({
id: z.string().optional(),
name: z.string().optional(),
source: z.string().optional(),
ststus: z.string().optional(),
}),
)
.optional(),
// icon: z.array(z.instanceof(File)),
})
.superRefine((data, ctx) => {

View File

@ -7,6 +7,8 @@ import { Form } from '@/components/ui/form';
import { FormLayout } from '@/constants/form';
import { DocumentParserType } from '@/constants/knowledge';
import { PermissionRole } from '@/constants/permission';
import { DataSourceInfo } from '@/pages/user-setting/data-source/contant';
import { IDataSourceBase } from '@/pages/user-setting/data-source/interface';
import { zodResolver } from '@hookform/resolvers/zod';
import { useEffect, useState } from 'react';
import { useForm, useWatch } from 'react-hook-form';
@ -19,6 +21,9 @@ import {
} from '../dataset/generate-button/generate';
import { ChunkMethodForm } from './chunk-method-form';
import ChunkMethodLearnMore from './chunk-method-learn-more';
import LinkDataSource, {
IDataSourceNodeProps,
} from './components/link-data-source';
import { MainContainer } from './configuration-form-container';
import { ChunkMethodItem, ParseTypeItem } from './configuration/common-item';
import { formSchema } from './form-schema';
@ -78,10 +83,12 @@ export default function DatasetSettings() {
pipeline_id: '',
parseType: 1,
pagerank: 0,
connectors: [],
},
});
const knowledgeDetails = useFetchKnowledgeConfigurationOnMount(form);
// const [pipelineData, setPipelineData] = useState<IDataPipelineNodeProps>();
const [sourceData, setSourceData] = useState<IDataSourceNodeProps[]>();
const [graphRagGenerateData, setGraphRagGenerateData] =
useState<IGenerateLogButtonProps>();
const [raptorGenerateData, setRaptorGenerateData] =
@ -97,6 +104,19 @@ export default function DatasetSettings() {
// linked: true,
// };
// setPipelineData(data);
const source_data: IDataSourceNodeProps[] =
knowledgeDetails?.connectors?.map((connector) => {
return {
...connector,
icon:
DataSourceInfo[connector.source as keyof typeof DataSourceInfo]
?.icon || '',
};
});
setSourceData(source_data);
setGraphRagGenerateData({
finish_at: knowledgeDetails.graphrag_task_finish_at,
task_id: knowledgeDetails.graphrag_task_id,
@ -129,6 +149,23 @@ export default function DatasetSettings() {
// }
// };
const handleLinkOrEditSubmit = (data: IDataSourceBase[] | undefined) => {
if (data) {
const connectors = data.map((connector) => {
return {
...connector,
icon:
DataSourceInfo[connector.source as keyof typeof DataSourceInfo]
?.icon || '',
};
});
setSourceData(connectors as IDataSourceNodeProps[]);
form.setValue('connectors', connectors || []);
// form.setValue('pipeline_name', data.name || '');
// form.setValue('pipeline_avatar', data.avatar || '');
}
};
const handleDeletePipelineTask = (type: GenerateType) => {
if (type === GenerateType.KnowledgeGraph) {
setGraphRagGenerateData({
@ -158,6 +195,19 @@ export default function DatasetSettings() {
}
console.log('parseType', parseType);
}, [parseType, form]);
const unbindFunc = (data: IDataSourceBase) => {
if (data) {
const connectors = sourceData?.filter((connector) => {
return connector.id !== data.id;
});
console.log('🚀 ~ DatasetSettings ~ connectors:', connectors);
setSourceData(connectors as IDataSourceNodeProps[]);
form.setValue('connectors', connectors || []);
// form.setValue('pipeline_name', data.name || '');
// form.setValue('pipeline_avatar', data.avatar || '');
}
};
return (
<section className="p-5 h-full flex flex-col">
<TopTitle
@ -205,6 +255,13 @@ export default function DatasetSettings() {
data={pipelineData}
handleLinkOrEditSubmit={handleLinkOrEditSubmit}
/> */}
<Divider />
<LinkDataSource
data={sourceData}
handleLinkOrEditSubmit={handleLinkOrEditSubmit}
unbindFunc={unbindFunc}
/>
</MainContainer>
</div>
<div className="text-right items-center flex justify-end gap-3 w-[768px]">

View File

@ -0,0 +1,80 @@
import { Modal } from '@/components/ui/modal/modal';
import { IModalProps } from '@/interfaces/common';
import { useEffect, useState } from 'react';
import { FieldValues } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { DynamicForm, FormFieldConfig } from './component/dynamic-form';
import {
DataSourceFormBaseFields,
DataSourceFormDefaultValues,
DataSourceFormFields,
} from './contant';
import { IDataSorceInfo } from './interface';
const AddDataSourceModal = ({
visible,
hideModal,
loading,
sourceData,
onOk,
}: IModalProps<FieldValues> & { sourceData?: IDataSorceInfo }) => {
const { t } = useTranslation();
const [fields, setFields] = useState<FormFieldConfig[]>([]);
useEffect(() => {
if (sourceData) {
setFields([
...DataSourceFormBaseFields,
...DataSourceFormFields[
sourceData.id as keyof typeof DataSourceFormFields
],
] as FormFieldConfig[]);
}
}, [sourceData]);
const handleOk = async (values?: FieldValues) => {
await onOk?.(values);
hideModal?.();
};
return (
<Modal
title={t('setting.add')}
open={visible || false}
onOpenChange={(open) => !open && hideModal?.()}
// onOk={() => handleOk()}
okText={t('common.ok')}
cancelText={t('common.cancel')}
showfooter={false}
>
<DynamicForm.Root
fields={fields}
onSubmit={(data) => {
console.log(data);
}}
defaultValues={
DataSourceFormDefaultValues[
sourceData?.id as keyof typeof DataSourceFormDefaultValues
] as FieldValues
}
>
<div className="flex items-center justify-end w-full gap-2">
<DynamicForm.CancelButton
handleCancel={() => {
hideModal?.();
}}
/>
<DynamicForm.SavingButton
submitLoading={loading || false}
buttonText={t('common.ok')}
submitFunc={(values: FieldValues) => {
handleOk(values);
}}
/>
</div>
</DynamicForm.Root>
</Modal>
);
};
export default AddDataSourceModal;

View File

@ -0,0 +1,51 @@
import { ConfirmDeleteDialog } from '@/components/confirm-delete-dialog';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { Settings, Trash2 } from 'lucide-react';
import { useDeleteDataSource } from '../hooks';
import { IDataSorceInfo, IDataSourceBase } from '../interface';
export type IAddedSourceCardProps = IDataSorceInfo & {
list: IDataSourceBase[];
};
export const AddedSourceCard = (props: IAddedSourceCardProps) => {
const { list, name, icon } = props;
const { handleDelete } = useDeleteDataSource();
const { navigateToDataSourceDetail } = useNavigatePage();
const toDetail = (id: string) => {
navigateToDataSourceDetail(id);
};
return (
<Card className="bg-transparent border border-border-button px-5 pt-[10px] pb-5 rounded-md">
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-0 pb-3">
{/* <Users className="mr-2 h-5 w-5 text-[#1677ff]" /> */}
<CardTitle className="text-base flex gap-1 font-normal">
{icon}
{name}
</CardTitle>
</CardHeader>
<CardContent className="p-2 flex flex-col gap-2">
{list.map((item) => (
<div
key={item.id}
className="flex flex-row items-center justify-between rounded-md bg-bg-input px-[10px] py-4"
>
<div className="text-sm text-text-secondary ">{item.name}</div>
<div className="text-sm text-text-secondary flex gap-2">
<Settings
className="cursor-pointer"
size={14}
onClick={() => {
toDetail(item.id);
}}
/>
<ConfirmDeleteDialog onOk={() => handleDelete(item)}>
<Trash2 className="cursor-pointer" size={14} />
</ConfirmDeleteDialog>
</div>
</div>
))}
</CardContent>
</Card>
);
};

View File

@ -0,0 +1,725 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { forwardRef, useEffect, useImperativeHandle, useMemo } from 'react';
import {
DefaultValues,
FieldValues,
SubmitHandler,
useForm,
useFormContext,
} from 'react-hook-form';
import { ZodSchema, z } from 'zod';
import EditTag from '@/components/edit-tag';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { Checkbox } from '@/components/ui/checkbox';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import { t } from 'i18next';
import { Loader } from 'lucide-react';
// Field type enumeration
export enum FormFieldType {
Text = 'text',
Email = 'email',
Password = 'password',
Number = 'number',
Textarea = 'textarea',
Select = 'select',
Checkbox = 'checkbox',
Tag = 'tag',
}
// Field configuration interface
export interface FormFieldConfig {
name: string;
label: string;
type: FormFieldType;
hidden?: boolean;
required?: boolean;
placeholder?: string;
options?: { label: string; value: string }[];
defaultValue?: any;
validation?: {
pattern?: RegExp;
minLength?: number;
maxLength?: number;
min?: number;
max?: number;
message?: string;
};
render?: (fieldProps: any) => React.ReactNode;
horizontal?: boolean;
onChange?: (value: any) => void;
}
// Component props interface
interface DynamicFormProps<T extends FieldValues> {
fields: FormFieldConfig[];
onSubmit: SubmitHandler<T>;
className?: string;
children?: React.ReactNode;
defaultValues?: DefaultValues<T>;
}
// Form ref interface
export interface DynamicFormRef {
submit: () => void;
getValues: () => any;
reset: (values?: any) => void;
}
// Generate Zod validation schema based on field configurations
const generateSchema = (fields: FormFieldConfig[]): ZodSchema<any> => {
const schema: Record<string, ZodSchema> = {};
const nestedSchemas: Record<string, Record<string, ZodSchema>> = {};
fields.forEach((field) => {
let fieldSchema: ZodSchema;
// Create base validation schema based on field type
switch (field.type) {
case FormFieldType.Email:
fieldSchema = z.string().email('Please enter a valid email address');
break;
case FormFieldType.Number:
fieldSchema = z.coerce.number();
if (field.validation?.min !== undefined) {
fieldSchema = (fieldSchema as z.ZodNumber).min(
field.validation.min,
field.validation.message ||
`Value cannot be less than ${field.validation.min}`,
);
}
if (field.validation?.max !== undefined) {
fieldSchema = (fieldSchema as z.ZodNumber).max(
field.validation.max,
field.validation.message ||
`Value cannot be greater than ${field.validation.max}`,
);
}
break;
case FormFieldType.Checkbox:
fieldSchema = z.boolean();
break;
case FormFieldType.Tag:
fieldSchema = z.array(z.string());
break;
default:
fieldSchema = z.string();
break;
}
// Handle required fields
if (field.required) {
if (field.type === FormFieldType.Checkbox) {
fieldSchema = (fieldSchema as z.ZodBoolean).refine(
(val) => val === true,
{
message: `${field.label} is required`,
},
);
} else if (field.type === FormFieldType.Tag) {
fieldSchema = (fieldSchema as z.ZodArray<z.ZodString>).min(1, {
message: `${field.label} is required`,
});
} else {
fieldSchema = (fieldSchema as z.ZodString).min(1, {
message: `${field.label} is required`,
});
}
}
if (!field.required) {
fieldSchema = fieldSchema.optional();
}
// Handle other validation rules
if (
field.type !== FormFieldType.Number &&
field.type !== FormFieldType.Checkbox &&
field.type !== FormFieldType.Tag &&
field.required
) {
fieldSchema = fieldSchema as z.ZodString;
if (field.validation?.minLength !== undefined) {
fieldSchema = (fieldSchema as z.ZodString).min(
field.validation.minLength,
field.validation.message ||
`Enter at least ${field.validation.minLength} characters`,
);
}
if (field.validation?.maxLength !== undefined) {
fieldSchema = (fieldSchema as z.ZodString).max(
field.validation.maxLength,
field.validation.message ||
`Enter up to ${field.validation.maxLength} characters`,
);
}
if (field.validation?.pattern) {
fieldSchema = (fieldSchema as z.ZodString).regex(
field.validation.pattern,
field.validation.message || 'Invalid input format',
);
}
}
if (field.name.includes('.')) {
const keys = field.name.split('.');
const firstKey = keys[0];
if (!nestedSchemas[firstKey]) {
nestedSchemas[firstKey] = {};
}
let currentSchema = nestedSchemas[firstKey];
for (let i = 1; i < keys.length - 1; i++) {
const key = keys[i];
if (!currentSchema[key]) {
currentSchema[key] = {};
}
currentSchema = currentSchema[key];
}
const lastKey = keys[keys.length - 1];
currentSchema[lastKey] = fieldSchema;
} else {
schema[field.name] = fieldSchema;
}
});
Object.keys(nestedSchemas).forEach((key) => {
const buildNestedSchema = (obj: Record<string, any>): ZodSchema => {
const nestedSchema: Record<string, ZodSchema> = {};
Object.keys(obj).forEach((subKey) => {
if (
typeof obj[subKey] === 'object' &&
!(obj[subKey] instanceof z.ZodType)
) {
nestedSchema[subKey] = buildNestedSchema(obj[subKey]);
} else {
nestedSchema[subKey] = obj[subKey];
}
});
return z.object(nestedSchema);
};
schema[key] = buildNestedSchema(nestedSchemas[key]);
});
return z.object(schema);
};
// Generate default values based on field configurations
const generateDefaultValues = <T extends FieldValues>(
fields: FormFieldConfig[],
): DefaultValues<T> => {
const defaultValues: Record<string, any> = {};
fields.forEach((field) => {
if (field.name.includes('.')) {
const keys = field.name.split('.');
let current = defaultValues;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!current[key]) {
current[key] = {};
}
current = current[key];
}
const lastKey = keys[keys.length - 1];
if (field.defaultValue !== undefined) {
current[lastKey] = field.defaultValue;
} else if (field.type === FormFieldType.Checkbox) {
current[lastKey] = false;
} else if (field.type === FormFieldType.Tag) {
current[lastKey] = [];
} else {
current[lastKey] = '';
}
} else {
if (field.defaultValue !== undefined) {
defaultValues[field.name] = field.defaultValue;
} else if (field.type === FormFieldType.Checkbox) {
defaultValues[field.name] = false;
} else if (field.type === FormFieldType.Tag) {
defaultValues[field.name] = [];
} else {
defaultValues[field.name] = '';
}
}
});
return defaultValues as DefaultValues<T>;
};
// Dynamic form component
const DynamicForm = {
Root: forwardRef(
<T extends FieldValues>(
{
fields,
onSubmit,
className = '',
children,
defaultValues: formDefaultValues = {} as DefaultValues<T>,
}: DynamicFormProps<T>,
ref: React.Ref<any>,
) => {
// Generate validation schema and default values
const schema = useMemo(() => generateSchema(fields), [fields]);
const defaultValues = useMemo(() => {
const value = {
...generateDefaultValues(fields),
...formDefaultValues,
};
console.log('generateDefaultValues', fields, value);
return value;
}, [fields, formDefaultValues]);
// Initialize form
const form = useForm<T>({
resolver: zodResolver(schema),
defaultValues,
});
// Expose form methods via ref
useImperativeHandle(ref, () => ({
submit: () => form.handleSubmit(onSubmit)(),
getValues: () => form.getValues(),
reset: (values?: T) => {
if (values) {
form.reset(values);
} else {
form.reset();
}
},
setError: form.setError,
clearErrors: form.clearErrors,
trigger: form.trigger,
}));
useEffect(() => {
if (formDefaultValues && Object.keys(formDefaultValues).length > 0) {
form.reset({
...generateDefaultValues(fields),
...formDefaultValues,
});
}
}, [form, formDefaultValues, fields]);
// Submit handler
// const handleSubmit = form.handleSubmit(onSubmit);
// Render form fields
const renderField = (field: FormFieldConfig) => {
if (field.render) {
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (e: any) => {
fieldProps.onChange(e);
field.onChange?.(e.target?.value ?? e);
},
}
: fieldProps;
return field.render?.(finalFieldProps);
}}
</RAGFlowFormItem>
);
}
switch (field.type) {
case FormFieldType.Textarea:
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (e: any) => {
fieldProps.onChange(e);
field.onChange?.(e.target.value);
},
}
: fieldProps;
return (
<Textarea
{...finalFieldProps}
placeholder={field.placeholder}
className="resize-none"
/>
);
}}
</RAGFlowFormItem>
);
case FormFieldType.Select:
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (value: string) => {
console.log('select value', value);
if (fieldProps.onChange) {
fieldProps.onChange(value);
}
field.onChange?.(value);
},
}
: fieldProps;
return (
<SelectWithSearch
triggerClassName="!shrink"
{...finalFieldProps}
options={field.options}
/>
);
}}
</RAGFlowFormItem>
);
case FormFieldType.Checkbox:
return (
<FormField
control={form.control}
name={field.name as any}
render={({ field: formField }) => (
<FormItem
className={cn('flex items-center', {
'flex-row items-start space-x-3 space-y-0':
!field.horizontal,
})}
>
{field.label && !field.horizontal && (
<div className="space-y-1 leading-none">
<FormLabel className="font-normal">
{field.label}{' '}
{field.required && (
<span className="text-destructive">*</span>
)}
</FormLabel>
</div>
)}
<FormControl>
<Checkbox
checked={formField.value}
onCheckedChange={(checked) => {
formField.onChange(checked);
field.onChange?.(checked);
}}
/>
</FormControl>
{field.label && field.horizontal && (
<div className="space-y-1 leading-none">
<FormLabel className="font-normal">
{field.label}{' '}
{field.required && (
<span className="text-destructive">*</span>
)}
</FormLabel>
</div>
)}
<FormMessage />
</FormItem>
)}
/>
);
case FormFieldType.Tag:
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (value: string[]) => {
fieldProps.onChange(value);
field.onChange?.(value);
},
}
: fieldProps;
return (
// <TagInput {...fieldProps} placeholder={field.placeholder} />
<div className="w-full">
<EditTag {...finalFieldProps}></EditTag>
</div>
);
}}
</RAGFlowFormItem>
);
default:
return (
<RAGFlowFormItem
name={field.name}
label={field.label}
required={field.required}
horizontal={field.horizontal}
>
{(fieldProps) => {
const finalFieldProps = field.onChange
? {
...fieldProps,
onChange: (e: any) => {
fieldProps.onChange(e);
field.onChange?.(e.target.value);
},
}
: fieldProps;
return (
<Input
{...finalFieldProps}
type={field.type}
placeholder={field.placeholder}
/>
);
}}
</RAGFlowFormItem>
);
}
};
return (
<Form {...form}>
<form
className={`space-y-6 ${className}`}
onSubmit={(e) => {
e.preventDefault();
form.handleSubmit(onSubmit)(e);
}}
>
<>
{fields.map((field) => (
<div key={field.name} className={cn({ hidden: field.hidden })}>
{renderField(field)}
</div>
))}
{children}
</>
</form>
</Form>
);
},
) as <T extends FieldValues>(
props: DynamicFormProps<T> & { ref?: React.Ref<DynamicFormRef> },
) => React.ReactElement,
SavingButton: ({
submitLoading,
buttonText,
submitFunc,
}: {
submitLoading: boolean;
buttonText?: string;
submitFunc?: (values: FieldValues) => void;
}) => {
const form = useFormContext();
return (
<button
type="button"
disabled={submitLoading}
onClick={() => {
console.log('form submit');
(async () => {
console.log('form submit2');
try {
let beValid = await form.formControl.trigger();
console.log('form valid', beValid, form, form.formControl);
if (beValid) {
form.handleSubmit(async (values) => {
console.log('form values', values);
submitFunc?.(values);
})();
}
} catch (e) {
console.error(e);
} finally {
console.log('form submit3');
}
})();
}}
className={cn(
'px-2 py-1 bg-primary text-primary-foreground rounded-md hover:bg-primary/90',
)}
>
{submitLoading && (
<Loader className="inline-block mr-2 h-4 w-4 animate-spin" />
)}
{buttonText ?? t('modal.okText')}
</button>
);
},
CancelButton: ({
handleCancel,
cancelText,
}: {
handleCancel: () => void;
cancelText?: string;
}) => {
return (
<button
type="button"
onClick={() => handleCancel()}
className="px-2 py-1 border border-input rounded-md hover:bg-muted"
>
{cancelText ?? t('modal.cancelText')}
</button>
);
},
};
export { DynamicForm };
/**
* Usage Example 1: Basic Form
*
* <DynamicForm
* fields={[
* {
* name: "username",
* label: "Username",
* type: FormFieldType.Text,
* required: true,
* placeholder: "Please enter username"
* },
* {
* name: "email",
* label: "Email",
* type: FormFieldType.Email,
* required: true,
* placeholder: "Please enter email address"
* }
* ]}
* onSubmit={(data) => {
* console.log(data); // { username: "...", email: "..." }
* }}
* />
*
* Usage Example 2: Nested Object Form
*
* <DynamicForm
* fields={[
* {
* name: "user.name",
* label: "Name",
* type: FormFieldType.Text,
* required: true,
* placeholder: "Please enter name"
* },
* {
* name: "user.email",
* label: "Email",
* type: FormFieldType.Email,
* required: true,
* placeholder: "Please enter email address"
* },
* {
* name: "user.profile.age",
* label: "Age",
* type: FormFieldType.Number,
* required: true,
* validation: {
* min: 18,
* max: 100,
* message: "Age must be between 18 and 100"
* }
* },
* {
* name: "user.profile.bio",
* label: "Bio",
* type: FormFieldType.Textarea,
* placeholder: "Please briefly introduce yourself"
* },
* {
* name: "settings.notifications",
* label: "Enable Notifications",
* type: FormFieldType.Checkbox
* }
* ]}
* onSubmit={(data) => {
* console.log(data);
* // {
* // user: {
* // name: "...",
* // email: "...",
* // profile: {
* // age: ...,
* // bio: "..."
* // }
* // },
* // settings: {
* // notifications: true/false
* // }
* // }
* }}
* />
*
* Usage Example 3: Tag Type Form
*
* <DynamicForm
* fields={[
* {
* name: "skills",
* label: "Skill Tags",
* type: FormFieldType.Tag,
* required: true,
* placeholder: "Enter skill and press Enter to add tag"
* },
* {
* name: "user.hobbies",
* label: "Hobbies",
* type: FormFieldType.Tag,
* placeholder: "Enter hobby and press Enter to add tag"
* }
* ]}
* onSubmit={(data) => {
* console.log(data);
* // {
* // skills: ["JavaScript", "React", "TypeScript"],
* // user: {
* // hobbies: ["Reading", "Swimming", "Travel"]
* // }
* // }
* }}
* />
*/

View File

@ -0,0 +1,173 @@
import SvgIcon from '@/components/svg-icon';
import { t } from 'i18next';
import { FormFieldType } from './component/dynamic-form';
export enum DataSourceKey {
S3 = 's3',
NOTION = 'notion',
DISCORD = 'discord',
// CONFLUENNCE = 'confluence',
// GMAIL = 'gmail',
// GOOGLE_DRIVER = 'google_driver',
// JIRA = 'jira',
// SHAREPOINT = 'sharepoint',
// SLACK = 'slack',
// TEAMS = 'teams',
}
export const DataSourceInfo = {
[DataSourceKey.S3]: {
name: 'S3',
description: t(`setting.${DataSourceKey.S3}Description`),
icon: <SvgIcon name={'data-source/s3'} width={28} />,
},
[DataSourceKey.NOTION]: {
name: 'Notion',
description: t(`setting.${DataSourceKey.NOTION}Description`),
icon: <SvgIcon name={'data-source/notion'} width={28} />,
},
[DataSourceKey.DISCORD]: {
name: 'Discord',
description: t(`setting.${DataSourceKey.DISCORD}Description`),
icon: <SvgIcon name={'data-source/discord'} width={28} />,
},
};
export const DataSourceFormBaseFields = [
{
id: 'Id',
name: 'id',
type: FormFieldType.Text,
required: false,
hidden: true,
},
{
label: 'Name',
name: 'name',
type: FormFieldType.Text,
required: true,
},
{
label: 'Source',
name: 'source',
type: FormFieldType.Select,
required: true,
hidden: true,
options: Object.keys(DataSourceKey).map((item) => ({
label: item,
value: DataSourceKey[item as keyof typeof DataSourceKey],
})),
},
];
export const DataSourceFormFields = {
[DataSourceKey.S3]: [
{
label: 'AWS Access Key ID',
name: 'config.credentials.aws_access_key_id',
type: FormFieldType.Text,
required: true,
},
{
label: 'AWS Secret Access Key',
name: 'config.credentials.aws_secret_access_key',
type: FormFieldType.Text,
required: true,
},
{
label: 'Bucket Name',
name: 'config.bucket_name',
type: FormFieldType.Text,
required: true,
},
{
label: 'Bucket Type',
name: 'config.bucket_type',
type: FormFieldType.Select,
options: [
{ label: 'S3', value: 's3' },
{ label: 'R2', value: 'r2' },
{ label: 'Google Cloud Storage', value: 'google_cloud_storage' },
{ label: 'OCI Storage', value: 'oci_storage' },
],
required: true,
},
{
label: 'Prefix',
name: 'config.prefix',
type: FormFieldType.Text,
required: false,
},
],
[DataSourceKey.NOTION]: [
{
label: 'Notion Integration Token',
name: 'config.credentials.notion_integration_token',
type: FormFieldType.Text,
required: true,
},
{
label: 'Root Page Id',
name: 'config.root_page_id',
type: FormFieldType.Text,
required: false,
},
],
[DataSourceKey.DISCORD]: [
{
label: 'Discord Bot Token',
name: 'config.credentials.discord_bot_token',
type: FormFieldType.Text,
required: true,
},
{
label: 'Server IDs',
name: 'config.server_ids',
type: FormFieldType.Tag,
required: false,
},
{
label: 'Channels',
name: 'config.channels',
type: FormFieldType.Tag,
required: false,
},
],
};
export const DataSourceFormDefaultValues = {
[DataSourceKey.S3]: {
name: '',
source: DataSourceKey.S3,
config: {
bucket_name: '',
bucket_type: 's3',
prefix: '',
credentials: {
aws_access_key_id: '',
aws_secret_access_key: '',
},
},
},
[DataSourceKey.NOTION]: {
name: '',
source: DataSourceKey.NOTION,
config: {
root_page_id: '',
credentials: {
notion_integration_token: '',
},
},
},
[DataSourceKey.DISCORD]: {
name: '',
source: DataSourceKey.DISCORD,
config: {
server_ids: [],
channels: [],
credentials: {
discord_bot_token: '',
},
},
},
};

View File

@ -0,0 +1,193 @@
import BackButton from '@/components/back-button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import { RunningStatus } from '@/constants/knowledge';
import { t } from 'i18next';
import { debounce } from 'lodash';
import { CirclePause, Repeat } from 'lucide-react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FieldValues } from 'react-hook-form';
import {
DynamicForm,
DynamicFormRef,
FormFieldConfig,
FormFieldType,
} from '../component/dynamic-form';
import {
DataSourceFormBaseFields,
DataSourceFormDefaultValues,
DataSourceFormFields,
DataSourceInfo,
} from '../contant';
import {
useAddDataSource,
useDataSourceResume,
useFetchDataSourceDetail,
} from '../hooks';
import { DataSourceLogsTable } from './log-table';
const SourceDetailPage = () => {
const formRef = useRef<DynamicFormRef>(null);
const { data: detail } = useFetchDataSourceDetail();
const { handleResume } = useDataSourceResume();
const detailInfo = useMemo(() => {
if (detail) {
return DataSourceInfo[detail.source];
}
}, [detail]);
const [fields, setFields] = useState<FormFieldConfig[]>([]);
const [defaultValues, setDefaultValues] = useState<FieldValues>(
DataSourceFormDefaultValues[
detail?.source as keyof typeof DataSourceFormDefaultValues
] as FieldValues,
);
const runSchedule = useCallback(() => {
handleResume({
resume:
detail?.status === RunningStatus.RUNNING ||
detail?.status === RunningStatus.SCHEDULE
? false
: true,
});
}, [detail, handleResume]);
const customFields = useMemo(() => {
return [
{
label: 'Refresh Freq',
name: 'refresh_freq',
type: FormFieldType.Number,
required: false,
render: (fieldProps: FormFieldConfig) => (
<div className="flex items-center gap-1 w-full relative">
<Input {...fieldProps} type={FormFieldType.Number} />
<span className="absolute right-0 -translate-x-12 text-text-secondary italic ">
minutes
</span>
<button
type="button"
className="text-text-secondary bg-bg-input rounded-sm text-xs h-full p-2 border border-border-button "
onClick={() => {
runSchedule();
}}
>
{detail?.status === RunningStatus.RUNNING ||
detail?.status === RunningStatus.SCHEDULE ? (
<CirclePause size={12} />
) : (
<Repeat size={12} />
)}
</button>
</div>
),
},
{
label: 'Prune Freq',
name: 'prune_freq',
type: FormFieldType.Number,
required: false,
hidden: true,
render: (fieldProps: FormFieldConfig) => {
return (
<div className="flex items-center gap-1 w-full relative">
<Input {...fieldProps} type={FormFieldType.Number} />
<span className="absolute right-0 -translate-x-3 text-text-secondary italic ">
hours
</span>
</div>
);
},
},
{
label: 'Timeout Secs',
name: 'timeout_secs',
type: FormFieldType.Number,
required: false,
render: (fieldProps: FormFieldConfig) => (
<div className="flex items-center gap-1 w-full relative">
<Input {...fieldProps} type={FormFieldType.Number} />
<span className="absolute right-0 -translate-x-3 text-text-secondary italic ">
minutes
</span>
</div>
),
},
];
}, [detail, runSchedule]);
const { handleAddOk } = useAddDataSource();
const onSubmit = useCallback(() => {
formRef?.current?.submit();
}, [formRef]);
useEffect(() => {
if (detail) {
const fields = [
...DataSourceFormBaseFields,
...DataSourceFormFields[
detail.source as keyof typeof DataSourceFormFields
],
...customFields,
] as FormFieldConfig[];
const neweFields = fields.map((field) => {
return {
...field,
horizontal: true,
onChange: () => {
onSubmit();
},
};
});
setFields(neweFields);
const defultValueTemp = {
...(DataSourceFormDefaultValues[
detail?.source as keyof typeof DataSourceFormDefaultValues
] as FieldValues),
...detail,
};
console.log('defaultValue', defultValueTemp);
setDefaultValues(defultValueTemp);
}
}, [detail, customFields, onSubmit]);
return (
<div className="px-10 py-5">
<BackButton />
<Card className="bg-transparent border border-border-button px-5 pt-[10px] pb-5 rounded-md mt-5">
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-0 pb-3">
{/* <Users className="mr-2 h-5 w-5 text-[#1677ff]" /> */}
<CardTitle className="text-2xl text-text-primary flex gap-1 items-center font-normal pb-3">
{detailInfo?.icon}
{detail?.name}
</CardTitle>
</CardHeader>
<Separator className="border-border-button bg-border-button w-[calc(100%+2rem)] -translate-x-4 -translate-y-4" />
<CardContent className="p-2 flex flex-col gap-2 max-h-[calc(100vh-190px)] overflow-y-auto scrollbar-auto">
<div className="max-w-[1200px]">
<DynamicForm.Root
ref={formRef}
fields={fields}
onSubmit={debounce((data) => {
handleAddOk(data);
}, 500)}
defaultValues={defaultValues}
/>
</div>
<section className="flex flex-col gap-2 mt-6">
<div className="text-2xl text-text-primary">{t('setting.log')}</div>
<DataSourceLogsTable />
</section>
</CardContent>
</Card>
</div>
);
};
export default SourceDetailPage;

View File

@ -0,0 +1,240 @@
import FileStatusBadge from '@/components/file-status-badge';
import { RAGFlowAvatar } from '@/components/ragflow-avatar';
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 { RunningStatusMap } from '@/constants/knowledge';
import { RunningStatus } from '@/pages/dataset/dataset/constant';
import { Routes } from '@/routes';
import { formatDate } from '@/utils/date';
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from '@radix-ui/react-hover-card';
import {
ColumnDef,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
import { t } from 'i18next';
import { pick } from 'lodash';
import { Eye } from 'lucide-react';
import { useCallback, useMemo } from 'react';
import { useNavigate } from 'umi';
import { useLogListDataSource } from '../hooks';
const columns = ({
handleToDataSetDetail,
}: {
handleToDataSetDetail: (id: string) => void;
}) => {
return [
{
accessorKey: 'update_date',
header: t('setting.timeStarted'),
cell: ({ row }) => (
<div className="flex items-center gap-2 text-text-primary">
{row.original.update_date
? formatDate(row.original.update_date)
: '-'}
</div>
),
},
{
accessorKey: 'status',
header: t('knowledgeDetails.status'),
cell: ({ row }) => (
<FileStatusBadge
status={row.original.status as RunningStatus}
name={RunningStatusMap[row.original.status as RunningStatus]}
className="!w-20"
/>
),
},
{
accessorKey: 'kb_name',
header: t('knowledgeDetails.dataset'),
cell: ({ row }) => {
return (
<div
className="flex items-center gap-2 text-text-primary cursor-pointer"
onClick={() => {
console.log('handleToDataSetDetail', row.original.kb_id);
handleToDataSetDetail(row.original.kb_id);
}}
>
<RAGFlowAvatar
avatar={row.original.avatar}
name={row.original.kb_name}
className="size-4"
/>
{row.original.kb_name}
</div>
);
},
},
{
accessorKey: 'new_docs_indexed',
header: t('setting.newDocs'),
},
{
id: 'operations',
header: t('setting.errorMsg'),
cell: ({ row }) => (
<div className="flex gap-1 items-center">
{row.original.error_msg}
{row.original.error_msg && (
<div className="flex justify-start space-x-2 opacity-0 group-hover:opacity-100 transition-opacity">
<HoverCard>
<HoverCardTrigger>
<Button
variant="ghost"
size="sm"
className="p-1"
// onClick={() => {
// showLog(row, LogTabs.FILE_LOGS);
// }}
>
<Eye />
</Button>
</HoverCardTrigger>
<HoverCardContent className="w-[40vw] max-h-[40vh] overflow-auto bg-bg-base z-[999] px-3 py-2 rounded-md border border-border-default">
<div className="space-y-2">
{row.original.full_exception_trace}
</div>
</HoverCardContent>
</HoverCard>
</div>
)}
</div>
),
},
] as ColumnDef<any>[];
};
// const paginationInit = {
// current: 1,
// pageSize: 10,
// total: 0,
// };
export const DataSourceLogsTable = () => {
// const [pagination, setPagination] = useState(paginationInit);
const { data, pagination, setPagination } = useLogListDataSource();
const navigate = useNavigate();
const currentPagination = useMemo(
() => ({
pageIndex: (pagination.current || 1) - 1,
pageSize: pagination.pageSize || 10,
}),
[pagination],
);
const handleToDataSetDetail = useCallback(
(id: string) => {
console.log('handleToDataSetDetail', id);
navigate(`${Routes.DatasetBase}${Routes.DataSetSetting}/${id}`);
},
[navigate],
);
const table = useReactTable<any>({
data: data || [],
columns: columns({ handleToDataSetDetail }),
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,
rowCount: pagination.total ?? 0,
});
return (
// <div className="w-full h-[calc(100vh-360px)]">
// <Table rootClassName="max-h-[calc(100vh-380px)]">
<div className="w-full">
<Table>
<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 min-w-[1280px] overflow-auto">
{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 mt-4">
<div className="space-x-2">
{/* <RAGFlowPagination
{...{ current: pagination.current, pageSize: pagination.pageSize }}
total={pagination.total}
onChange={(page, pageSize) => setPagination({ page, pageSize })}
/> */}
<RAGFlowPagination
{...pick(pagination, 'current', 'pageSize')}
total={pagination.total}
onChange={(page, pageSize) => {
setPagination({ page, pageSize });
}}
></RAGFlowPagination>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,188 @@
import message from '@/components/ui/message';
import { useSetModalState } from '@/hooks/common-hooks';
import { useGetPaginationWithRouter } from '@/hooks/logic-hooks';
import dataSourceService, {
dataSourceResume,
deleteDataSource,
featchDataSourceDetail,
getDataSourceLogs,
} from '@/services/data-source-service';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { t } from 'i18next';
import { useCallback, useMemo, useState } from 'react';
import { useSearchParams } from 'umi';
import { DataSourceInfo, DataSourceKey } from './contant';
import { IDataSorceInfo, IDataSource, IDataSourceBase } from './interface';
export const useListDataSource = () => {
const { data: list, isFetching } = useQuery<IDataSource[]>({
queryKey: ['data-source'],
queryFn: async () => {
const { data } = await dataSourceService.dataSourceList();
return data.data;
},
});
const categorizeDataBySource = (data: IDataSourceBase[]) => {
const categorizedData: Record<DataSourceKey, any[]> = {} as Record<
DataSourceKey,
any[]
>;
data.forEach((item) => {
const source = item.source;
if (!categorizedData[source]) {
categorizedData[source] = [];
}
categorizedData[source].push({
...item,
});
});
return categorizedData;
};
const updatedDataSourceTemplates = useMemo(() => {
const categorizedData = categorizeDataBySource(list || []);
let sourcelist: Array<IDataSorceInfo & { list: Array<IDataSourceBase> }> =
[];
Object.keys(categorizedData).forEach((key: string) => {
const k = key as DataSourceKey;
sourcelist.push({
id: k,
name: DataSourceInfo[k].name,
description: DataSourceInfo[k].description,
icon: DataSourceInfo[k].icon,
list: categorizedData[k] || [],
});
});
console.log('🚀 ~ useListDataSource ~ sourcelist:', sourcelist);
return sourcelist;
}, [list]);
return { list, categorizedList: updatedDataSourceTemplates, isFetching };
};
export const useAddDataSource = () => {
const [addSource, setAddSource] = useState<IDataSorceInfo | undefined>(
undefined,
);
const [addLoading, setAddLoading] = useState<boolean>(false);
const {
visible: addingModalVisible,
hideModal: hideAddingModal,
showModal,
} = useSetModalState();
const showAddingModal = useCallback(
(data: IDataSorceInfo) => {
setAddSource(data);
showModal();
},
[showModal],
);
const queryClient = useQueryClient();
const handleAddOk = useCallback(
async (data: any) => {
setAddLoading(true);
const { data: res } = await dataSourceService.dataSourceSet(data);
console.log('🚀 ~ handleAddOk ~ code:', res.code);
if (res.code === 0) {
queryClient.invalidateQueries({ queryKey: ['data-source'] });
message.success(t(`message.operated`));
hideAddingModal();
}
setAddLoading(false);
},
[hideAddingModal, queryClient],
);
return {
addSource,
addLoading,
setAddSource,
addingModalVisible,
hideAddingModal,
showAddingModal,
handleAddOk,
};
};
export const useLogListDataSource = () => {
const { pagination, setPagination } = useGetPaginationWithRouter();
const [currentQueryParameters] = useSearchParams();
const id = currentQueryParameters.get('id');
const { data, isFetching } = useQuery<{ logs: IDataSource[]; total: number }>(
{
queryKey: ['data-source-logs', id, pagination],
queryFn: async () => {
const { data } = await getDataSourceLogs(id as string, {
page_size: pagination.pageSize,
page: pagination.current,
});
return data.data;
},
},
);
return {
data: data?.logs,
isFetching,
pagination: { ...pagination, total: data?.total },
setPagination,
};
};
export const useDeleteDataSource = () => {
const [deleteLoading, setDeleteLoading] = useState<boolean>(false);
const { hideModal, showModal } = useSetModalState();
const queryClient = useQueryClient();
const handleDelete = useCallback(
async ({ id }: { id: string }) => {
setDeleteLoading(true);
const { data } = await deleteDataSource(id);
if (data.code === 0) {
message.success(t(`message.deleted`));
queryClient.invalidateQueries({ queryKey: ['data-source'] });
}
setDeleteLoading(false);
},
[setDeleteLoading, queryClient],
);
return { deleteLoading, hideModal, showModal, handleDelete };
};
export const useFetchDataSourceDetail = () => {
const [currentQueryParameters] = useSearchParams();
const id = currentQueryParameters.get('id');
const { data } = useQuery<IDataSource>({
queryKey: ['data-source-detail', id],
enabled: !!id,
queryFn: async () => {
const { data } = await featchDataSourceDetail(id as string);
// if (data.code === 0) {
// }
return data.data;
},
});
return { data };
};
export const useDataSourceResume = () => {
const [currentQueryParameters] = useSearchParams();
const id = currentQueryParameters.get('id');
const queryClient = useQueryClient();
const handleResume = useCallback(
async (param: { resume: boolean }) => {
const { data } = await dataSourceResume(id as string, param);
if (data.code === 0) {
queryClient.invalidateQueries({ queryKey: ['data-source-detail', id] });
message.success(t(`message.operated`));
}
},
[id, queryClient],
);
return { handleResume };
};

View File

@ -0,0 +1,151 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useTranslation } from 'react-i18next';
import Spotlight from '@/components/spotlight';
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { Plus } from 'lucide-react';
import AddDataSourceModal from './add-datasource-modal';
import { AddedSourceCard } from './component/added-source-card';
import { DataSourceInfo, DataSourceKey } from './contant';
import { useAddDataSource, useListDataSource } from './hooks';
import { IDataSorceInfo } from './interface';
const dataSourceTemplates = [
{
id: DataSourceKey.S3,
name: DataSourceInfo[DataSourceKey.S3].name,
description: DataSourceInfo[DataSourceKey.S3].description,
icon: DataSourceInfo[DataSourceKey.S3].icon,
list: [
{
id: '1',
name: 'S3 Bucket 1',
},
{
id: '2',
name: 'S3 Bucket 1',
},
],
},
{
id: DataSourceKey.DISCORD,
name: DataSourceInfo[DataSourceKey.DISCORD].name,
description: DataSourceInfo[DataSourceKey.DISCORD].description,
icon: DataSourceInfo[DataSourceKey.DISCORD].icon,
},
{
id: DataSourceKey.NOTION,
name: DataSourceInfo[DataSourceKey.NOTION].name,
description: DataSourceInfo[DataSourceKey.NOTION].description,
icon: DataSourceInfo[DataSourceKey.NOTION].icon,
},
];
const DataSource = () => {
const { t } = useTranslation();
// useListTenantUser();
const { categorizedList } = useListDataSource();
const {
addSource,
addLoading,
addingModalVisible,
handleAddOk,
hideAddingModal,
showAddingModal,
} = useAddDataSource();
const AbailableSourceCard = ({
id,
name,
description,
icon,
}: IDataSorceInfo) => {
return (
<div className="p-[10px] border border-border-button rounded-lg relative group hover:bg-bg-card">
<div className="flex gap-2">
<div className="w-6 h-6">{icon}</div>
<div className="flex flex-1 flex-col items-start gap-2">
<div className="text-base text-text-primary">{name}</div>
<div className="text-xs text-text-secondary">{description}</div>
</div>
</div>
<div className=" absolute top-2 right-2">
<Button
onClick={() =>
showAddingModal({
id,
name,
description,
icon,
})
}
className=" rounded-md px-1 text-bg-base gap-1 bg-text-primary text-xs py-0 h-6 items-center hidden group-hover:flex"
>
<Plus size={12} />
{t('setting.add')}
</Button>
</div>
</div>
);
};
return (
<div className="w-full flex flex-col gap-4 relative ">
<Spotlight />
<Card className="bg-transparent border-none px-0">
<CardHeader className="flex flex-row items-center justify-between space-y-0 px-4 pt-4 pb-0">
<CardTitle className="text-2xl font-medium">
{t('setting.dataSources')}
<div className="text-sm text-text-secondary">
{t('setting.datasourceDescription')}
</div>
</CardTitle>
</CardHeader>
</Card>
<Separator className="border-border-button bg-border-button " />
<div className=" flex flex-col gap-4 p-4 max-h-[calc(100vh-120px)] overflow-y-auto overflow-x-hidden scrollbar-auto">
<div className="flex flex-col gap-3">
{categorizedList.map((item, index) => (
<AddedSourceCard key={index} {...item} />
))}
</div>
<Card className="bg-transparent border-none mt-8">
<CardHeader className="flex flex-row items-center justify-between space-y-0 p-0 pb-4">
{/* <Users className="mr-2 h-5 w-5 text-[#1677ff]" /> */}
<CardTitle className="text-2xl font-semibold">
{t('setting.availableSources')}
<div className="text-sm text-text-secondary font-normal">
{t('setting.availableSourcesDescription')}
</div>
</CardTitle>
</CardHeader>
<CardContent className="p-0">
{/* <TenantTable searchTerm={searchTerm}></TenantTable> */}
<div className="grid sm:grid-cols-1 lg:grid-cols-2 xl:grid-cols-2 2xl:grid-cols-4 3xl:grid-cols-4 gap-4">
{dataSourceTemplates.map((item, index) => (
<AbailableSourceCard {...item} key={index} />
))}
</div>
</CardContent>
</Card>
</div>
{addingModalVisible && (
<AddDataSourceModal
visible
loading={addLoading}
hideModal={hideAddingModal}
onOk={(data) => {
console.log(data);
handleAddOk(data);
}}
sourceData={addSource}
></AddDataSourceModal>
)}
</div>
);
};
export default DataSource;

View File

@ -0,0 +1,45 @@
import { RunningStatus } from '@/constants/knowledge';
import { DataSourceKey } from './contant';
export interface IDataSorceInfo {
id: DataSourceKey;
name: string;
description: string;
icon: React.ReactNode;
}
export type IDataSource = IDataSourceBase & {
config: any;
indexing_start: null | string;
input_type: string;
prune_freq: number;
refresh_freq: number;
status: string;
tenant_id: string;
update_date: string;
update_time: number;
};
export interface IDataSourceBase {
id: string;
name: string;
source: DataSourceKey;
}
export interface IDataSourceLog {
connector_id: string;
error_count: number;
error_msg: string;
id: string;
kb_id: string;
kb_name: string;
name: string;
new_docs_indexed: number;
poll_range_end: null | string;
poll_range_start: null | string;
reindex: string;
source: DataSourceKey;
status: RunningStatus;
tenant_id: string;
timeout_secs: number;
}

View File

@ -27,6 +27,7 @@ interface IProps {
const SystemSetting = ({ onOk, loading }: IProps) => {
const { systemSetting: initialValues, allOptions } =
useFetchSystemModelSettingOnMount();
const { t: tcommon } = useTranslate('common');
const { t } = useTranslate('setting');
const [formData, setFormData] = useState({
@ -159,7 +160,7 @@ const SystemSetting = ({ onOk, loading }: IProps) => {
value={value}
options={options}
onChange={(value) => handleFieldChange(id, value)}
placeholder={t('common:selectPlaceholder')}
placeholder={tcommon('selectPlaceholder')}
/>
</div>
);

View File

@ -1,4 +1,5 @@
import { LlmItem, useSelectLlmList } from '@/hooks/llm-hooks';
import { t } from 'i18next';
import { ModelProviderCard } from './modal-card';
export const UsedModel = ({
@ -11,7 +12,9 @@ export const UsedModel = ({
const { factoryList, myLlmList: llmList, loading } = useSelectLlmList();
return (
<div className="flex flex-col w-full gap-4 mb-4">
<div className="text-text-primary text-2xl mb-2 mt-4">Added models</div>
<div className="text-text-primary text-2xl font-semibold mb-2 mt-4">
{t('setting.addedModels')}
</div>
{llmList.map((llm) => {
return (
<ModelProviderCard

View File

@ -11,22 +11,24 @@ import {
} from '@/hooks/use-user-setting-request';
import { cn } from '@/lib/utils';
import { Routes } from '@/routes';
import { Banknote, Box, Cog, Unplug, User, Users } from 'lucide-react';
import { t } from 'i18next';
import { Banknote, Box, Cog, Server, Unplug, User, Users } from 'lucide-react';
import { useEffect } from 'react';
import { useHandleMenuClick } from './hooks';
const menuItems = [
{ icon: User, label: 'Profile', key: Routes.Profile },
{ icon: Users, label: 'Team', key: Routes.Team },
{ icon: Box, label: 'Model Providers', key: Routes.Model },
{ icon: Unplug, label: 'API', key: Routes.Api },
{ icon: User, label: t('setting.profile'), key: Routes.Profile },
{ icon: Users, label: t('setting.team'), key: Routes.Team },
{ icon: Box, label: t('setting.model'), key: Routes.Model },
{ icon: Unplug, label: t('setting.api'), key: Routes.Api },
// {
// icon: MessageSquareQuote,
// label: 'Prompt Templates',
// key: Routes.Profile,
// },
// { icon: TextSearch, label: 'Retrieval Templates', key: Routes.Profile },
{ icon: Cog, label: 'System', key: Routes.System },
{ icon: Server, label: t('setting.dataSources'), key: Routes.DataSource },
{ icon: Cog, label: t('setting.system'), key: Routes.System },
// { icon: Banknote, label: 'Plan', key: Routes.Plan },
{ icon: Banknote, label: 'MCP', key: Routes.Mcp },
];
@ -101,7 +103,7 @@ export function SideBar() {
logout();
}}
>
Log Out
{t('setting.logout')}
</Button>
</div>
</aside>

View File

@ -27,6 +27,8 @@ export enum Routes {
System = '/system',
Model = '/model',
Prompt = '/prompt',
DataSource = '/data-source',
DataSourceDetailPage = '/data-source-detail-page',
ProfileMcp = `${ProfileSetting}${Mcp}`,
ProfileTeam = `${ProfileSetting}${Team}`,
ProfilePlan = `${ProfileSetting}${Plan}`,
@ -400,9 +402,19 @@ const routes = [
path: `/user-setting${Routes.Mcp}`,
component: `@/pages${Routes.ProfileMcp}`,
},
{
path: `/user-setting${Routes.DataSource}`,
component: `@/pages/user-setting${Routes.DataSource}`,
},
],
},
{
path: `/user-setting${Routes.DataSource}${Routes.DataSourceDetailPage}`,
component: `@/pages/user-setting${Routes.DataSource}${Routes.DataSourceDetailPage}`,
layout: false,
},
// Admin routes
{
path: Routes.Admin,

View File

@ -0,0 +1,33 @@
import api from '@/utils/api';
import registerServer from '@/utils/register-server';
import request from '@/utils/request';
const { dataSourceSet, dataSourceList } = api;
const methods = {
dataSourceSet: {
url: dataSourceSet,
method: 'post',
},
dataSourceList: {
url: dataSourceList,
method: 'get',
},
} as const;
const dataSourceService = registerServer<keyof typeof methods>(
methods,
request,
);
export const deleteDataSource = (id: string) =>
request.post(api.dataSourceDel(id));
export const dataSourceResume = (id: string, data: { resume: boolean }) => {
console.log('api.dataSourceResume(id)', data);
return request.put(api.dataSourceResume(id), { data });
};
export const getDataSourceLogs = (id: string, params?: any) =>
request.get(api.dataSourceLogs(id), { params });
export const featchDataSourceDetail = (id: string) =>
request.get(api.dataSourceDetail(id));
export default dataSourceService;

View File

@ -34,6 +34,14 @@ export default {
enable_llm: `${api_host}/llm/enable_llm`,
deleteFactory: `${api_host}/llm/delete_factory`,
// data source
dataSourceSet: `${api_host}/connector/set`,
dataSourceList: `${api_host}/connector/list`,
dataSourceDel: (id: string) => `${api_host}/connector/${id}/rm`,
dataSourceResume: (id: string) => `${api_host}/connector/${id}/resume`,
dataSourceLogs: (id: string) => `${api_host}/connector/${id}/logs`,
dataSourceDetail: (id: string) => `${api_host}/connector/${id}`,
// plugin
llm_tools: `${api_host}/plugin/llm_tools`,