mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 12:32:30 +08:00
### 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:
14
web/src/assets/svg/data-source/discord.svg
Normal file
14
web/src/assets/svg/data-source/discord.svg
Normal 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="矩形" 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 |
19
web/src/assets/svg/data-source/notion.svg
Normal file
19
web/src/assets/svg/data-source/notion.svg
Normal 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="矩形" 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="矩形_2" width="24" height="24" fill="white"/>
|
||||
</mask>
|
||||
<g mask="url(#mask0_826_561463)">
|
||||
</g>
|
||||
<g id="Log in out – 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 |
29
web/src/assets/svg/data-source/s3.svg
Normal file
29
web/src/assets/svg/data-source/s3.svg
Normal 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="矩形" 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 |
41
web/src/components/back-button/index.tsx
Normal file
41
web/src/components/back-button/index.tsx
Normal 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;
|
||||
@ -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 || ''}
|
||||
|
||||
@ -39,7 +39,7 @@ export function RAGFlowFormItem({
|
||||
<FormItem
|
||||
className={cn(
|
||||
{
|
||||
'flex items-center': horizontal,
|
||||
'flex items-center w-full': horizontal,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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. ',
|
||||
|
||||
@ -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: '正在从数据源下载文件。',
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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) => {
|
||||
|
||||
@ -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]">
|
||||
|
||||
@ -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;
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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"]
|
||||
* // }
|
||||
* // }
|
||||
* }}
|
||||
* />
|
||||
*/
|
||||
173
web/src/pages/user-setting/data-source/contant.tsx
Normal file
173
web/src/pages/user-setting/data-source/contant.tsx
Normal 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: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -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;
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
188
web/src/pages/user-setting/data-source/hooks.ts
Normal file
188
web/src/pages/user-setting/data-source/hooks.ts
Normal 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 };
|
||||
};
|
||||
151
web/src/pages/user-setting/data-source/index.tsx
Normal file
151
web/src/pages/user-setting/data-source/index.tsx
Normal 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;
|
||||
45
web/src/pages/user-setting/data-source/interface.ts
Normal file
45
web/src/pages/user-setting/data-source/interface.ts
Normal 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;
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
33
web/src/services/data-source-service.ts
Normal file
33
web/src/services/data-source-service.ts
Normal 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;
|
||||
@ -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`,
|
||||
|
||||
|
||||
Reference in New Issue
Block a user