Merge branch 'develop' into docs/web_zy
This commit is contained in:
@@ -1,33 +1,39 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 13:59:41
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 13:59:41
|
||||
*/
|
||||
import { request } from '@/utils/request'
|
||||
import type { ApiKey } from '@/views/ApiKeyManagement/types'
|
||||
|
||||
// API Key列表
|
||||
// API Key list
|
||||
export const getApiKeyListUrl = '/apikeys'
|
||||
export const getApiKeyList = (data: Record<string, unknown>) => {
|
||||
return request.get(getApiKeyListUrl, data)
|
||||
}
|
||||
|
||||
// API Key详情
|
||||
// API Key details
|
||||
export const getApiKey = (id: string) => {
|
||||
return request.get(`/apikeys/${id}`)
|
||||
}
|
||||
|
||||
// 创建API Key
|
||||
// Create API Key
|
||||
export const createApiKey = (values: ApiKey) => {
|
||||
return request.post('/apikeys', values)
|
||||
}
|
||||
|
||||
// 更新API Key
|
||||
// Update API Key
|
||||
export const updateApiKey = (id: string, values: ApiKey) => {
|
||||
return request.put(`/apikeys/${id}`, values)
|
||||
}
|
||||
|
||||
// 删除 API Key
|
||||
// Delete API Key
|
||||
export const deleteApiKey = (id: string) => {
|
||||
return request.delete(`/apikeys/${id}`)
|
||||
}
|
||||
|
||||
// 使用统计
|
||||
// Usage statistics
|
||||
export const getApiKeyStats = (app_key_id: string) => {
|
||||
return request.get(`/apikeys/${app_key_id}/stats`)
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 13:59:45
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 13:59:45
|
||||
*/
|
||||
import { request } from '@/utils/request'
|
||||
import type { ApplicationModalData } from '@/views/ApplicationManagement/types'
|
||||
import type { Config } from '@/views/ApplicationConfig/types'
|
||||
@@ -5,71 +11,72 @@ import { handleSSE, type SSEMessage } from '@/utils/stream'
|
||||
import type { QueryParams } from '@/views/Conversation/types'
|
||||
import type { WorkflowConfig } from '@/views/Workflow/types'
|
||||
|
||||
// 应用列表
|
||||
// Application list
|
||||
export const getApplicationListUrl = '/apps'
|
||||
export const getApplicationList = (data: Record<string, unknown>) => {
|
||||
return request.get(getApplicationListUrl, data)
|
||||
}
|
||||
// 获取应用配置
|
||||
// Get application config
|
||||
export const getApplicationConfig = (id: string) => {
|
||||
return request.get(`/apps/${id}/config`)
|
||||
}
|
||||
// 获取集群应用配置
|
||||
// Get multi-agent config
|
||||
export const getMultiAgentConfig = (id: string) => {
|
||||
return request.get(`/apps/${id}/multi-agent`)
|
||||
}
|
||||
// 获取 workflow应用配置
|
||||
// Get workflow config
|
||||
export const getWorkflowConfig = (id: string) => {
|
||||
return request.get(`/apps/${id}/workflow`)
|
||||
}
|
||||
// 应用详情
|
||||
// Application details
|
||||
export const getApplication = (id: string) => {
|
||||
return request.get(`/apps/${id}`)
|
||||
}
|
||||
// 更新应用
|
||||
// Update application
|
||||
export const updateApplication = (id: string, values: ApplicationModalData) => {
|
||||
return request.put(`/apps/${id}`, values)
|
||||
}
|
||||
// 创建应用
|
||||
// Create application
|
||||
export const addApplication = (values: ApplicationModalData) => {
|
||||
return request.post('/apps', values)
|
||||
}
|
||||
// 保存Agent配置
|
||||
// Save agent config
|
||||
export const saveAgentConfig = (app_id: string, values: Config) => {
|
||||
return request.put(`/apps/${app_id}/config`, values)
|
||||
}
|
||||
// 保存集群配置
|
||||
// Save multi-agent config
|
||||
export const saveMultiAgentConfig = (app_id: string, values: Config) => {
|
||||
return request.put(`/apps/${app_id}/multi-agent`, values)
|
||||
}
|
||||
// 保存workflow配置
|
||||
// Save workflow config
|
||||
export const saveWorkflowConfig = (app_id: string, values: WorkflowConfig) => {
|
||||
return request.put(`/apps/${app_id}/workflow`, values)
|
||||
}
|
||||
// 模型比对试运行
|
||||
// Model comparison test run
|
||||
export const runCompare = (app_id: string, values: Record<string, unknown>, onMessage?: (data: SSEMessage[]) => void) => {
|
||||
return handleSSE(`/apps/${app_id}/draft/run/compare`, values, onMessage)
|
||||
}
|
||||
// Test run
|
||||
export const draftRun = (app_id: string, values: Record<string, unknown>, onMessage?: (data: SSEMessage[]) => void) => {
|
||||
return handleSSE(`/apps/${app_id}/draft/run`, values, onMessage)
|
||||
}
|
||||
// 删除应用
|
||||
// Delete application
|
||||
export const deleteApplication = (app_id: string) => {
|
||||
return request.delete(`/apps/${app_id}`)
|
||||
}
|
||||
// 发布版本列表
|
||||
// Release version list
|
||||
export const getReleaseList = (app_id: string) => {
|
||||
return request.get(`/apps/${app_id}/releases`)
|
||||
}
|
||||
// 发布版本
|
||||
// Publish release
|
||||
export const publishRelease = (app_id: string, values: Record<string, unknown>) => {
|
||||
return request.post(`/apps/${app_id}/publish`, values)
|
||||
}
|
||||
// 回滚版本
|
||||
// Rollback release
|
||||
export const rollbackRelease = (app_id: string, version: string) => {
|
||||
return request.post(`/apps/${app_id}/rollback/${version}`)
|
||||
}
|
||||
// 发布版本分享
|
||||
// Share release
|
||||
export const shareRelease = (app_id: string, release_id: string) => {
|
||||
return request.post(`/apps/${app_id}/releases/${release_id}/share`, {
|
||||
"is_enabled": true,
|
||||
@@ -77,7 +84,7 @@ export const shareRelease = (app_id: string, release_id: string) => {
|
||||
"allow_embed": true
|
||||
})
|
||||
}
|
||||
// 获取体验对话历史
|
||||
// Get conversation history
|
||||
export const getConversationHistory = (share_token: string, data: { page: number; pagesize: number }) => {
|
||||
return request.get(`/public/share/conversations`, data, {
|
||||
headers: {
|
||||
@@ -85,7 +92,7 @@ export const getConversationHistory = (share_token: string, data: { page: number
|
||||
}
|
||||
})
|
||||
}
|
||||
// 发送体验对话
|
||||
// Send conversation
|
||||
export const sendConversation = (values: QueryParams, onMessage: (data: SSEMessage[]) => void, shareToken: string) => {
|
||||
return handleSSE(`/public/share/chat`, values, onMessage, {
|
||||
headers: {
|
||||
@@ -93,7 +100,7 @@ export const sendConversation = (values: QueryParams, onMessage: (data: SSEMessa
|
||||
}
|
||||
})
|
||||
}
|
||||
// 获取体验会话详情
|
||||
// Get conversation details
|
||||
export const getConversationDetail = (share_token: string, conversation_id: string) => {
|
||||
return request.get(`/public/share/conversations/${conversation_id}`, {}, {
|
||||
headers: {
|
||||
@@ -101,15 +108,15 @@ export const getConversationDetail = (share_token: string, conversation_id: stri
|
||||
}
|
||||
})
|
||||
}
|
||||
// 获取体验对话token
|
||||
// Get share token
|
||||
export const getShareToken = (share_token: string, user_id: string) => {
|
||||
return request.post(`/public/share/${share_token}/token`, { user_id })
|
||||
}
|
||||
// 复制应用
|
||||
// Copy application
|
||||
export const copyApplication = (app_id: string, new_name: string) => {
|
||||
return request.post(`/apps/${app_id}/copy?new_name=${new_name}`)
|
||||
}
|
||||
// 数据统计
|
||||
// Data statistics
|
||||
export const getAppStatistics = (app_id: string, data: { start_date: number; end_date: number; }) => {
|
||||
return request.get(`/apps/${app_id}/statistics`, data)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { request } from "@/utils/request";
|
||||
// 列表查询参数
|
||||
// List query parameters
|
||||
export interface Query {
|
||||
page?: number;
|
||||
pagesize?: number;
|
||||
@@ -38,15 +38,15 @@ export interface versionResponse{
|
||||
codeName: string;
|
||||
};
|
||||
}
|
||||
// 首页数据统计
|
||||
// Dashboard data statistics
|
||||
export const getDashboardData = `/home-page/workspaces`
|
||||
|
||||
// 首页数据看板统计
|
||||
// Dashboard statistics
|
||||
export const getDashboardStatistics = async () => {
|
||||
const response = await request.get(`/home-page/statistics`);
|
||||
return response as DataResponse;
|
||||
};
|
||||
// 获取版本号
|
||||
// Get version
|
||||
export const getVersion = async () => {
|
||||
const response = await request.get(`/home-page/version`);
|
||||
return response as versionResponse;
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 13:59:56
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 13:59:56
|
||||
*/
|
||||
import { request, API_PREFIX } from '@/utils/request'
|
||||
|
||||
// Upload file,file storage has expiration period
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 14:00:01
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 14:00:01
|
||||
*/
|
||||
import { request } from '@/utils/request'
|
||||
|
||||
// 成员列表
|
||||
// Member list
|
||||
export const memberListUrl = '/workspaces/members'
|
||||
// 邀请成员
|
||||
// Invite member
|
||||
export const inviteMember = (values: { email: string }) => {
|
||||
return request.post(`/workspaces/invites`, values)
|
||||
}
|
||||
// 删除成员
|
||||
// Delete member
|
||||
export const deleteMember = (id: string) => {
|
||||
return request.delete(`/workspaces/members/${id}`)
|
||||
}
|
||||
// 更新成员
|
||||
// Update member
|
||||
export const updateMember = (values: { id: string, role: string }) => {
|
||||
return request.put(`/workspaces/members`, [values])
|
||||
}
|
||||
// 验证邀请token
|
||||
// Validate invite token
|
||||
export const validateInviteToken = (token: string) => {
|
||||
return request.get(`/workspaces/invites/validate/${token}`)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 14:00:06
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 14:00:06
|
||||
*/
|
||||
import { request } from '@/utils/request'
|
||||
import type {
|
||||
MemoryFormData,
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 14:00:09
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 14:00:09
|
||||
*/
|
||||
import { request } from '@/utils/request'
|
||||
import type { MultiKeyForm, Query, KeyConfigModalForm, CompositeModelForm, CustomModelForm } from '@/views/ModelManagement/types'
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 13:59:12
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 13:59:12
|
||||
*/
|
||||
import { request } from '@/utils/request'
|
||||
import type { Query, OntologyModalData, OntologyClassModalData, OntologyClassExtractModalData } from '@/views/Ontology/types'
|
||||
import type { Query, OntologyModalData, OntologyClassModalData, OntologyClassExtractModalData, OntologyExportModalData } from '@/views/Ontology/types'
|
||||
|
||||
// Scene list
|
||||
export const getOntologyScenesUrl = '/memory/ontology/scenes'
|
||||
@@ -37,3 +43,11 @@ export const createOntologyClass = (data: OntologyClassModalData) => {
|
||||
export const deleteOntologyClass = (class_id: string) => {
|
||||
return request.delete(`/memory/ontology/class/${class_id}`)
|
||||
}
|
||||
// Import scenario
|
||||
export const ontologyImport = (data: unknown) => {
|
||||
return request.uploadFile('/memory/ontology/import', data)
|
||||
}
|
||||
// Export scenario
|
||||
export const ontologyExport = (data: OntologyExportModalData, fileName: string, callback: () => void) => {
|
||||
return request.downloadFile('/memory/ontology/export', fileName, data, callback)
|
||||
}
|
||||
@@ -1,16 +1,23 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 14:00:14
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 14:00:14
|
||||
*/
|
||||
import { request } from '@/utils/request'
|
||||
import type { VoucherForm } from '@/views/OrderPayment/types'
|
||||
|
||||
export const getOrderListUrl = '/v1/orders/customer'
|
||||
|
||||
// 提交支付凭证API
|
||||
// Submit payment voucher API
|
||||
export const submitPaymentVoucherAPI = (voucherData: VoucherForm) => {
|
||||
return request.post('/v1/orders/', voucherData)
|
||||
}
|
||||
// 订单详情
|
||||
// Order details
|
||||
export const getOrderDetail = (order_no: string) => {
|
||||
return request.get(`/v1/orders/customer/${order_no}`)
|
||||
}
|
||||
// Order status enum
|
||||
export const orderStatusUrl = '/v1/order-status/'
|
||||
export const getOrderStatus = () => {
|
||||
return request.get(orderStatusUrl)
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 14:00:17
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 14:00:17
|
||||
*/
|
||||
import { request } from '@/utils/request'
|
||||
import type { AiPromptForm } from '@/views/ApplicationConfig/types'
|
||||
import type { PromptReleaseData } from '@/views/Prompt/types'
|
||||
|
||||
@@ -1,40 +1,46 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 14:00:23
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 14:00:23
|
||||
*/
|
||||
import { request } from '@/utils/request'
|
||||
import type { CreateModalData } from '@/views/UserManagement/types'
|
||||
import { cookieUtils } from '@/utils/request'
|
||||
|
||||
// 用户信息
|
||||
// User info
|
||||
export const getUsers = () => {
|
||||
return request.get('/users')
|
||||
}
|
||||
// 用户列表
|
||||
// User list
|
||||
export const getUserListUrl = '/users/superusers'
|
||||
// 登录
|
||||
// Login
|
||||
export const loginUrl = '/token'
|
||||
export const login = (data: { email: string; password: string; invite?: string; username?: string }) => {
|
||||
return request.post(loginUrl, data)
|
||||
}
|
||||
// 刷新token
|
||||
// Refresh token
|
||||
export const refreshTokenUrl = '/refresh'
|
||||
export const refreshToken = () => {
|
||||
return request.post(refreshTokenUrl, { refresh_token: cookieUtils.get('refreshToken') })
|
||||
}
|
||||
// 重置密码
|
||||
// Reset password
|
||||
export const changePassword = (data: { user_id: string; new_password: string }) => {
|
||||
return request.put('/users/admin/change-password', data)
|
||||
}
|
||||
// 禁用用户
|
||||
// Disable user
|
||||
export const deleteUser = (user_id: string) => {
|
||||
return request.delete(`/users/${user_id}`)
|
||||
}
|
||||
// 启用用户
|
||||
// Enable user
|
||||
export const enableUser = (user_id: string) => {
|
||||
return request.post(`/users/${user_id}/activate`)
|
||||
}
|
||||
// 创建用户
|
||||
// Create user
|
||||
export const addUser = (data: CreateModalData) => {
|
||||
return request.post('/users/superuser', data)
|
||||
}
|
||||
// 注销
|
||||
// Logout
|
||||
export const logoutUrl = '/logout'
|
||||
export const logout = () => {
|
||||
return request.post(logoutUrl)
|
||||
|
||||
@@ -1,28 +1,34 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 14:00:26
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 14:00:26
|
||||
*/
|
||||
import { request } from '@/utils/request'
|
||||
import type { SpaceModalData } from '@/views/SpaceManagement/types'
|
||||
import type { ConfigModalData } from '@/views/UserMemory/types'
|
||||
import type { SpaceConfigData } from '@/views/SpaceConfig/types'
|
||||
|
||||
// 空间列表
|
||||
// Workspace list
|
||||
export const getWorkspaces = () => {
|
||||
return request.get('/workspaces')
|
||||
}
|
||||
// 创建空间
|
||||
// Create workspace
|
||||
export const createWorkspace = (values: SpaceModalData) => {
|
||||
return request.post('/workspaces', values)
|
||||
}
|
||||
// 切换空间
|
||||
// Switch workspace
|
||||
export const switchWorkspace = (workspaceId: string) => {
|
||||
return request.put(`/workspaces/${workspaceId}/switch`)
|
||||
}
|
||||
// 获取空间存储类型
|
||||
// Get workspace storage type
|
||||
export const getWorkspaceStorageType = () => {
|
||||
return request.get(`/workspaces/storage`)
|
||||
}
|
||||
// 获取空间模型配置
|
||||
// Get workspace model config
|
||||
export const getWorkspaceModels = () => {
|
||||
return request.get(`/workspaces/workspace_models`)
|
||||
}
|
||||
// 更新空间模型配置
|
||||
export const updateWorkspaceModels = (data: ConfigModalData) => {
|
||||
// Update workspace model config
|
||||
export const updateWorkspaceModels = (data: SpaceConfigData) => {
|
||||
return request.put(`/workspaces/workspace_models`, data)
|
||||
}
|
||||
|
||||
@@ -57,6 +57,10 @@ const ALL_FILE_TYPE: {
|
||||
htm: 'text/html',
|
||||
html: 'text/html',
|
||||
json: 'application/json',
|
||||
owl: 'application/rdf+xml',
|
||||
ttl: 'text/turtle',
|
||||
rdf: 'application/rdf+xml',
|
||||
xml: 'application/rdf+xml',
|
||||
}
|
||||
export interface UploadFilesRef {
|
||||
fileList: UploadFile[];
|
||||
@@ -122,7 +126,7 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
|
||||
if (fileSize) {
|
||||
const isLtMaxSize = (file.size / 1024 / 1024) < fileSize;
|
||||
if (!isLtMaxSize) {
|
||||
message.error(`文件大小不能超过 ${fileSize}MB`);
|
||||
message.error(t('common.fileSizeTip', { size: fileSize }));
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
}
|
||||
@@ -139,7 +143,7 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
|
||||
const isValidMimeType = file.type && accept ? accept.includes(file.type) : true;
|
||||
|
||||
if (!isValidExtension && !isValidMimeType) {
|
||||
message.error(`不支持的文件类型: ${fileExtension || file.type}`);
|
||||
message.error(`${t('common.fileAcceptTip')}${fileExtension || file.type}`);
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
}
|
||||
@@ -236,12 +240,12 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
|
||||
fileList,
|
||||
beforeUpload,
|
||||
headers: {
|
||||
authorization: cookieUtils.get('authToken') || '',
|
||||
authorization: `Bearer ${cookieUtils.get('authToken')}`,
|
||||
},
|
||||
onRemove: handleRemove,
|
||||
onChange: handleChange,
|
||||
accept,
|
||||
disabled,
|
||||
disabled: disabled || fileList.length >= maxCount,
|
||||
showUploadList: {
|
||||
showPreviewIcon: false,
|
||||
showRemoveIcon: true,
|
||||
@@ -249,12 +253,12 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
|
||||
},
|
||||
itemRender: (_, file, __, actions) => {
|
||||
return (
|
||||
<div key={file.uid} className="rb:relative rb:w-full rb:pt-[8px] rb:pl-[10px] rb:pr-[10px] rb-pb-[10px] rb:border-1 rb:border-[#EBEBEB] rb:rounded rb:p-2 rb:mt-2 rb:bg-white">
|
||||
<div className="rb:text-[12px] rb:flex rb:items-center rb:justify-between rb:mb-[2px]">
|
||||
<div key={file.uid} className="rb:relative rb:w-full rb:pt-2 rb:pl-2.5 rb:pr-2.5 rb-pb-[10px] rb:border rb:border-[#EBEBEB] rb:rounded rb:p-2 rb:mt-2 rb:bg-white">
|
||||
<div className="rb:text-[12px] rb:flex rb:items-center rb:justify-between rb:mb-0.5">
|
||||
{file.name}
|
||||
<span className="rb:text-[#5B6167] rb:cursor-pointer" onClick={() => actions?.remove()}>Cancel</span>
|
||||
<span className="rb:text-[#5B6167] rb:cursor-pointer" onClick={() => actions?.remove()}>{t('common.cancel')}</span>
|
||||
</div>
|
||||
<Progress percent={file.percent || 0} strokeColor={file.status === 'error' ? '#FF5D34' : '#155EEF'} size="small" showInfo={false} />
|
||||
{isAutoUpload && <Progress percent={file.percent || 0} strokeColor={file.status === 'error' ? '#FF5D34' : '#155EEF'} size="small" showInfo={false} />}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -267,20 +271,20 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
|
||||
clearFiles
|
||||
}));
|
||||
|
||||
const hasProgress = fileList.some((item) => item.percent !== 100);
|
||||
const hasProgress = isAutoUpload && fileList.some((item) => item.percent !== 100);
|
||||
|
||||
if (isCanDrag) {
|
||||
return (
|
||||
<div className="rb:mb-[24px] rb:w-full">
|
||||
<div className="rb:mb-6 rb:w-full">
|
||||
<Dragger {...uploadProps} style={{ height: '270px' }}>
|
||||
<div className="rb:flex rb:justify-center rb:flex-col rb:items-center">
|
||||
<img className="rb:w-[48px] rb:h-[48px]" src={CloudUploadOutlined} />
|
||||
{!hasProgress && (!fileList || !fileList.length) &&
|
||||
<img className="rb:w-12 rb:h-12" src={CloudUploadOutlined} />
|
||||
{(!isAutoUpload || !hasProgress && (!fileList || !fileList.length)) &&
|
||||
<>
|
||||
<div className="rb:text-base rb:text-[14px] rb:font-medium rb:flex rb:items-center rb:mt-[8px] rb:leading-[20px]">
|
||||
{t('common.dragUploadTip')}<span className="rb:ml-[4px] rb:text-[#155EEF]">{t('common.uploadClickTip')}</span>
|
||||
<div className="rb:text-base rb:text-[14px] rb:font-medium rb:flex rb:items-center rb:mt-2 rb:leading-5">
|
||||
{t('common.dragUploadTip')}<span className="rb:ml-1 rb:text-[#155EEF]">{t('common.uploadClickTip')}</span>
|
||||
</div>
|
||||
{fileType && <div className="rb:text-[12px] rb:text-[#A8A9AA] rb:leading-[14px] rb:mt-[8px] rb:cursor-pointer">{t('common.supportedFileTypes', { types: fileType.join(',') })}</div>}
|
||||
{fileType && <div className="rb:text-[12px] rb:text-[#A8A9AA] rb:leading-3.5 rb:mt-2 rb:cursor-pointer">{t('common.supportedFileTypes', { types: fileType.join(',') })}</div>}
|
||||
{(fileSize || fileType || maxCount > 1) && (
|
||||
<div className='rb:text-xs rb:mt-2 rb:text-[#A8A9AA]'>
|
||||
{t('common.uploadFileTipMax', { max: fileSize, maxCount: maxCount })}
|
||||
@@ -288,7 +292,7 @@ const UploadFiles = forwardRef<UploadFilesRef, UploadFilesProps>(({
|
||||
)}
|
||||
</>
|
||||
}
|
||||
{hasProgress && <div className="rb:text-base rb:text-[14px] rb:font-medium rb:flex rb:items-center rb:mt-[8px] rb:mb-[24px] rb:leading-[20px]">{t('common.uploading')}</div>}
|
||||
{hasProgress && <div className="rb:text-base rb:text-[14px] rb:font-medium rb:flex rb:items-center rb:mt-2 rb:mb-6 rb:leading-5">{t('common.uploading')}</div>}
|
||||
</div>
|
||||
</Dragger>
|
||||
</div>
|
||||
|
||||
@@ -426,6 +426,7 @@ export const en = {
|
||||
fileAcceptTip: 'Unsupported file type:',
|
||||
nextStep: 'Next Step',
|
||||
prevStep: 'Previous Step',
|
||||
exportSuccess: 'Export successful',
|
||||
},
|
||||
model: {
|
||||
searchPlaceholder: 'search model…',
|
||||
@@ -2472,6 +2473,11 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
extract: 'Project Inference',
|
||||
source: 'Not Added',
|
||||
target: 'Added',
|
||||
import: 'Import Scenario',
|
||||
format: 'Export Format',
|
||||
export: 'Export Scenario',
|
||||
scene_id: 'Scenario',
|
||||
file: 'Import File',
|
||||
},
|
||||
prompt: {
|
||||
editor: 'Prompt Generator',
|
||||
|
||||
@@ -980,6 +980,7 @@ export const zh = {
|
||||
fileAcceptTip: '不支持的文件类型:',
|
||||
nextStep: '下一步',
|
||||
prevStep: '上一步',
|
||||
exportSuccess: '导出成功',
|
||||
},
|
||||
product: {
|
||||
applicationManagement: '应用管理',
|
||||
@@ -2561,6 +2562,11 @@ export const zh = {
|
||||
extract: '工程推理',
|
||||
source: '未添加项',
|
||||
target: '已添加项',
|
||||
import: '导入场景',
|
||||
format: '导出格式',
|
||||
export: '导出场景',
|
||||
scene_id: '场景',
|
||||
file: '导入文件',
|
||||
},
|
||||
prompt: {
|
||||
editor: '提示词生成器',
|
||||
|
||||
@@ -330,19 +330,20 @@ export const request = {
|
||||
...config
|
||||
});
|
||||
},
|
||||
downloadFile(url: string, fileName: string, data?: unknown) {
|
||||
downloadFile(url: string, fileName: string, data?: unknown, callback?: () => void) {
|
||||
service.post(url, data, {
|
||||
responseType: "blob",
|
||||
})
|
||||
.then(res =>{
|
||||
const link = document.createElement("a");
|
||||
const blob = new Blob([res.data], { type: "application/vnd.ms-excel" });
|
||||
const blob = new Blob([res as unknown as BlobPart]);
|
||||
link.style.display = "none";
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.setAttribute("download", decodeURI(res.headers['filename'] || fileName));
|
||||
link.setAttribute("download", decodeURI(fileName || fileName));
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
callback?.()
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@ const { TextArea } = Input;
|
||||
const radioWrapperBaseStyle: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
columnGap: 14, // 点与文字更宽的间距
|
||||
columnGap: 14, // Wider gap between dot and text
|
||||
width: '100%',
|
||||
border: '1px solid #E5E5E5',
|
||||
borderRadius: 8,
|
||||
@@ -97,7 +97,7 @@ const CreateDataset = () => {
|
||||
() => [
|
||||
{ title: t('knowledgeBase.selectFile') },
|
||||
{ title: t('knowledgeBase.parameterSettings') },
|
||||
// { title: t('knowledgeBase.dataPreview') }, // 暂时隐藏第三步
|
||||
// { title: t('knowledgeBase.dataPreview') }, // Temporarily hide step 3
|
||||
{ title: t('knowledgeBase.confirmUpload') },
|
||||
],
|
||||
[t],
|
||||
@@ -105,27 +105,27 @@ const CreateDataset = () => {
|
||||
// 存储每个文件的 AbortController,用于取消上传
|
||||
const abortControllersRef = useRef<Map<string, AbortController>>(new Map());
|
||||
const uploadRef = useRef<{ fileList: UploadFile[]; clearFiles: () => void }>(null);
|
||||
console.log('上传文件',uploadRef.current?.fileList.length)
|
||||
console.log('Upload files', uploadRef.current?.fileList.length)
|
||||
const handleNext = async () => {
|
||||
// 暂时隐藏第三步:调整步骤索引(0->1->2 对应 选择文件->参数设置->确认上传)
|
||||
// Temporarily hide step 3: adjust step index (0->1->2 corresponds to select file->parameter settings->confirm upload)
|
||||
let nextStep = current + 1;
|
||||
|
||||
if(nextStep === 1 && source === 'local') {
|
||||
// 检查是否有文件已上传
|
||||
// Check if files have been uploaded
|
||||
if (rechunkFileIds.length === 0) {
|
||||
// 如果没有文件,提示用户先上传文件
|
||||
// If no files, prompt user to upload first
|
||||
Modal.warning({
|
||||
title: t('common.warning') || '提示',
|
||||
content: t('knowledgeBase.pleaseUploadFileFirst') || '请先上传文件',
|
||||
title: t('common.warning') || 'Warning',
|
||||
content: t('knowledgeBase.pleaseUploadFileFirst') || 'Please upload files first',
|
||||
});
|
||||
return; // 不进入下一步
|
||||
return; // Don't proceed to next step
|
||||
}
|
||||
}else if(nextStep === 1 && source === 'text'){
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
// setLoading(true);
|
||||
|
||||
// TODO: 这里需要调用相应的API来保存内容
|
||||
// TODO: Need to call corresponding API to save content here
|
||||
const params = {
|
||||
// ...values,
|
||||
kb_id: knowledgeBaseId,
|
||||
@@ -162,41 +162,41 @@ const CreateDataset = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 立即执行一次,加载文档列表用于预览(不自动返回)
|
||||
// Execute once immediately to load document list for preview (don't auto-return)
|
||||
pollDocumentStatus(false);
|
||||
}
|
||||
|
||||
// 限制最大步骤为 2(确认上传)
|
||||
// Limit max step to 2 (confirm upload)
|
||||
setCurrent(Math.min(nextStep, 2));
|
||||
};
|
||||
const handlePrev = () => setCurrent((c) => Math.max(c - 1, 0));
|
||||
|
||||
// 开始上传:触发文档解析并启动轮询
|
||||
// Start upload: trigger document parsing and start polling
|
||||
const handleStartUpload = () => {
|
||||
if (rechunkFileIds.length === 0) {
|
||||
Modal.warning({
|
||||
title: t('common.warning') || '提示',
|
||||
content: t('knowledgeBase.pleaseUploadFileFirst') || '请先上传文件',
|
||||
title: t('common.warning') || 'Warning',
|
||||
content: t('knowledgeBase.pleaseUploadFileFirst') || 'Please upload files first',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示确认弹框
|
||||
confirm({
|
||||
title: t('knowledgeBase.startUploadConfirmTitle') || '开始处理文档',
|
||||
content: t('knowledgeBase.startUploadConfirmContent') || '文档处理将在后台进行,您可以选择立即返回列表页或停留在此页面查看处理进度。',
|
||||
okText: t('knowledgeBase.returnToList') || '返回列表页',
|
||||
cancelText: t('knowledgeBase.stayOnPage') || '停留在此页',
|
||||
title: t('knowledgeBase.startUploadConfirmTitle') || 'Start processing documents',
|
||||
content: t('knowledgeBase.startUploadConfirmContent') || 'Document processing will proceed in the background. You can choose to return to the list page immediately or stay on this page to view processing progress.',
|
||||
okText: t('knowledgeBase.returnToList') || 'Return to list',
|
||||
cancelText: t('knowledgeBase.stayOnPage') || 'Stay on this page',
|
||||
onOk: () => {
|
||||
// 用户选择返回列表页 - 不显示 loading,直接跳转
|
||||
// User chose to return to list - don't show loading, navigate directly
|
||||
startProcessing(true);
|
||||
},
|
||||
onCancel: () => {
|
||||
// 用户选择停留在当前页 - 显示 loading 并开始轮询
|
||||
console.log('用户选择停留,开始显示 loading');
|
||||
// User chose to stay on current page - show loading and start polling
|
||||
console.log('User chose to stay, starting to show loading');
|
||||
setPollingLoading(true);
|
||||
|
||||
// 延迟一点时间让用户看到 loading 效果,然后开始处理
|
||||
// Delay a bit to let user see loading effect, then start processing
|
||||
setTimeout(() => {
|
||||
startProcessing(false);
|
||||
}, 100);
|
||||
@@ -204,25 +204,25 @@ const CreateDataset = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 实际开始处理的函数
|
||||
// Function to actually start processing
|
||||
const startProcessing = (autoReturnToList: boolean) => {
|
||||
// 触发文档解析
|
||||
// Trigger document parsing
|
||||
rechunkFileIds.map((id) => {
|
||||
parseDocument(id, {});
|
||||
});
|
||||
|
||||
if (autoReturnToList) {
|
||||
// 用户选择立即返回,直接跳转(不显示 loading)
|
||||
console.log('用户选择立即返回列表页');
|
||||
// User chose to return immediately, navigate directly (no loading shown)
|
||||
console.log('User chose to return to list page immediately');
|
||||
handleBack();
|
||||
} else {
|
||||
// 用户选择停留,启动轮询查看进度(loading 已在 onCancel 中设置)
|
||||
console.log('用户选择停留查看进度');
|
||||
// User chose to stay, start polling to view progress (loading already set in onCancel)
|
||||
console.log('User chose to stay and view progress');
|
||||
|
||||
// 立即执行一次轮询(启用自动返回)
|
||||
// Execute polling once immediately (enable auto-return)
|
||||
pollDocumentStatus(true);
|
||||
|
||||
// 然后每3秒执行一次(启用自动返回)
|
||||
// Then execute every 3 seconds (enable auto-return)
|
||||
pollingTimerRef.current = setInterval(() => {
|
||||
pollDocumentStatus(true);
|
||||
}, 3000);
|
||||
@@ -244,11 +244,11 @@ const CreateDataset = () => {
|
||||
|
||||
},
|
||||
onCancel: () => {
|
||||
console.log('取消删除');
|
||||
console.log('Delete cancelled');
|
||||
},
|
||||
});
|
||||
}
|
||||
// 表格列配置
|
||||
// Table column configuration
|
||||
const columns: ColumnsType = [
|
||||
{
|
||||
title: t('knowledgeBase.name'),
|
||||
@@ -261,7 +261,7 @@ const CreateDataset = () => {
|
||||
dataIndex: 'progress',
|
||||
key: 'progress',
|
||||
render: (value: number, record: any) => {
|
||||
// value >= 1 时完成,0~1 时显示进度条
|
||||
// When value >= 1 it's complete, when 0~1 show progress bar
|
||||
if (value >= 1) {
|
||||
return (
|
||||
<span className="rb:text-xs rb:border rb:border-[#DFE4ED] rb:bg-[#FBFDFF] rb:rounded rb:items-center rb:text-[#212332] rb:py-1 rb:px-2">
|
||||
@@ -270,7 +270,7 @@ const CreateDataset = () => {
|
||||
</span>
|
||||
);
|
||||
} else if (value >= 0 && value < 1) {
|
||||
// 处理中,显示进度条
|
||||
// Processing, show progress bar
|
||||
return (
|
||||
<div className="rb:flex rb:items-center rb:gap-2">
|
||||
<Progress
|
||||
@@ -286,7 +286,7 @@ const CreateDataset = () => {
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// value = 0 或其他情况,显示待处理
|
||||
// value = 0 or other cases, show pending
|
||||
return (
|
||||
<span className="rb:text-xs rb:border rb:border-[#DFE4ED] rb:bg-[#FBFDFF] rb:rounded rb:items-center rb:text-[#212332] rb:py-1 rb:px-2">
|
||||
<span className="rb:inline-block rb:w-[5px] rb:h-[5px] rb:mr-2 rb:rounded-full" style={{ backgroundColor: '#FF8A4C' }}></span>
|
||||
@@ -304,7 +304,7 @@ const CreateDataset = () => {
|
||||
),
|
||||
},
|
||||
];
|
||||
// 检查媒体文件时长的辅助函数
|
||||
// Helper function to check media file duration
|
||||
const checkMediaDuration = (file: File): Promise<number> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = URL.createObjectURL(file);
|
||||
@@ -324,36 +324,36 @@ const CreateDataset = () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 上传文件
|
||||
// Upload file
|
||||
const handleUpload = async (options: UploadRequestOption) => {
|
||||
const { file, onSuccess, onError, onProgress, filename = 'file' } = options;
|
||||
|
||||
// 创建 AbortController 用于取消上传
|
||||
// Create AbortController for cancelling upload
|
||||
const abortController = new AbortController();
|
||||
const fileUid = (file as any).uid;
|
||||
abortControllersRef.current.set(fileUid, abortController);
|
||||
|
||||
// 获取文件扩展名
|
||||
// Get file extension
|
||||
const fileExtension = (file as File).name.split('.').pop()?.toLowerCase();
|
||||
const mediaExtensions = ['mp3', 'mp4', 'mov', 'wav'];
|
||||
|
||||
// 如果是媒体文件,进行大小和时长检查
|
||||
// If media file, check size and duration
|
||||
if (fileExtension && mediaExtensions.includes(fileExtension)) {
|
||||
const fileSizeInMB = (file as File).size / (1024 * 1024);
|
||||
|
||||
// 检查文件大小(50MB限制)
|
||||
if (fileSizeInMB > 100) {
|
||||
messageApi.error(`${t('knowledgeBase.sizeLimitError')}:${fileSizeInMB.toFixed(2)}MB`);
|
||||
messageApi.error(`${t('knowledgeBase.sizeLimitError')}: ${fileSizeInMB.toFixed(2)}MB`);
|
||||
onError?.(new Error(`${t('knowledgeBase.fileSizeExceeds')}`));
|
||||
abortControllersRef.current.delete(fileUid);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查媒体时长(150秒限制)
|
||||
// Check media duration (150 second limit)
|
||||
const duration = await checkMediaDuration(file as File);
|
||||
if (duration > 150) {
|
||||
messageApi.error(`${t('knowledgeBase.fileDurationLimitError')}:${Math.round(duration)}秒`);
|
||||
messageApi.error(`${t('knowledgeBase.fileDurationLimitError')}: ${Math.round(duration)}s`);
|
||||
onError?.(new Error(`${t('knowledgeBase.fileDurationExceeds')}`));
|
||||
abortControllersRef.current.delete(fileUid);
|
||||
return;
|
||||
@@ -386,7 +386,7 @@ const CreateDataset = () => {
|
||||
},
|
||||
})
|
||||
.then((res: UploadFileResponse) => {
|
||||
// 上传成功,移除 AbortController
|
||||
// Upload successful, remove AbortController
|
||||
abortControllersRef.current.delete(fileUid);
|
||||
|
||||
onSuccess?.(res, new XMLHttpRequest());
|
||||
@@ -399,12 +399,12 @@ const CreateDataset = () => {
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
// 移除 AbortController
|
||||
// Remove AbortController
|
||||
abortControllersRef.current.delete(fileUid);
|
||||
|
||||
// 如果是用户主动取消,不显示错误信息
|
||||
// If user actively cancelled, don't show error message
|
||||
if (error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
|
||||
console.log('上传已取消:', (file as File).name);
|
||||
console.log('Upload cancelled:', (file as File).name);
|
||||
return;
|
||||
}
|
||||
onError?.(error as Error);
|
||||
@@ -413,12 +413,12 @@ const CreateDataset = () => {
|
||||
|
||||
|
||||
// 轮询检查文档处理状态
|
||||
// autoReturn: 是否在所有文档完成时自动返回列表页
|
||||
// autoReturn: whether to automatically return to list page when all documents are completed
|
||||
const pollDocumentStatus = (autoReturn: boolean = false) => {
|
||||
console.log('开始轮询文档状态,当前 pollingLoading:', pollingLoading);
|
||||
console.log('Start polling document status, current pollingLoading:', pollingLoading);
|
||||
|
||||
if (!knowledgeBaseId || !parentId || rechunkFileIds.length === 0) {
|
||||
console.log('轮询条件不满足,退出');
|
||||
console.log('Polling conditions not met, exiting');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -436,10 +436,10 @@ const CreateDataset = () => {
|
||||
}
|
||||
|
||||
console.log('documents', documents);
|
||||
// 检查是否所有文档的 progress 都为 1
|
||||
// Check if all documents have progress of 1
|
||||
const allCompleted = documents.every((doc: KnowledgeBaseDocumentData) => doc.progress === 1);
|
||||
|
||||
console.log('轮询状态:', allCompleted);
|
||||
console.log('Polling status:', allCompleted);
|
||||
|
||||
// 检查是否所有文档都完成了
|
||||
// debugger
|
||||
@@ -455,23 +455,23 @@ const CreateDataset = () => {
|
||||
setPollingLoading(false);
|
||||
}, 1000);
|
||||
|
||||
// 只有在 autoReturn 为 true 时才自动返回
|
||||
// Only auto-return when autoReturn is true
|
||||
if (autoReturn) {
|
||||
// 延迟 2 秒后跳转,让用户看到完成状态
|
||||
console.log('所有文档处理完成,2秒后返回列表页');
|
||||
// Delay 2 seconds before navigating to let user see completion status
|
||||
console.log('All documents processed, returning to list page in 2 seconds');
|
||||
setTimeout(() => {
|
||||
handleBack();
|
||||
}, 2000);
|
||||
} else {
|
||||
console.log('所有文档处理完成,用户可手动操作');
|
||||
console.log('All documents processed, user can operate manually');
|
||||
}
|
||||
} else {
|
||||
// 如果还有文档在处理中,确保 loading 状态保持
|
||||
console.log('还有文档在处理中,保持 loading 状态');
|
||||
// If documents are still processing, keep loading state
|
||||
console.log('Documents still processing, maintaining loading state');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('轮询文档状态失败:', error);
|
||||
console.error('Failed to poll document status:', error);
|
||||
setPollingLoading(false);
|
||||
});
|
||||
};
|
||||
@@ -486,7 +486,7 @@ const CreateDataset = () => {
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.warn('缺少路由参数,无法返回');
|
||||
console.warn('Missing route parameters, unable to return');
|
||||
}
|
||||
};
|
||||
const handleChange = (value: number | null) =>{
|
||||
@@ -498,17 +498,17 @@ const CreateDataset = () => {
|
||||
const handleDeleteFile = async (fileId: string) => {
|
||||
try {
|
||||
await deleteDocument(fileId);
|
||||
// 删除成功,从 rechunkFileIds 中移除该 id
|
||||
// Delete successful, remove the id from rechunkFileIds
|
||||
setRechunkFileIds((prev) => prev.filter((id) => id !== fileId));
|
||||
console.log(`${t('common.deleteSuccess')}`);
|
||||
} catch (error) {
|
||||
messageApi.error(`${t('common.deleteFailed')}`);
|
||||
}
|
||||
};
|
||||
// 当从其他页面跳转过来且带有 fileIds 时,加载对应的文档数据
|
||||
// When navigating from other pages with fileIds, load corresponding document data
|
||||
// useEffect(() => {
|
||||
// if (initialFileIds.length > 0 && initialStepKey !== 'selectFile' && knowledgeBaseId && parentId) {
|
||||
// // 加载文档列表数据
|
||||
// // Load document list data
|
||||
// getDocumentList(knowledgeBaseId,{
|
||||
// document_ids: initialFileIds.join(','),
|
||||
// })
|
||||
@@ -517,12 +517,12 @@ const CreateDataset = () => {
|
||||
// setData(documents);
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// console.error('加载文档列表失败:', error);
|
||||
// console.error('Failed to load document list:', error);
|
||||
// });
|
||||
// }
|
||||
// }, []);
|
||||
|
||||
// 清理函数:组件卸载时清除定时器和 loading 状态
|
||||
// Cleanup function: clear timer and loading state when component unmounts
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (pollingTimerRef.current) {
|
||||
@@ -533,10 +533,10 @@ const CreateDataset = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 监听路由变化,确保在页面切换时清理状态
|
||||
// Watch for route changes, ensure state is cleaned up when page switches
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// 页面卸载时清理状态
|
||||
// Clean up state when page unmounts
|
||||
if (pollingTimerRef.current) {
|
||||
clearInterval(pollingTimerRef.current);
|
||||
pollingTimerRef.current = null;
|
||||
@@ -574,7 +574,7 @@ const CreateDataset = () => {
|
||||
fileType={fileType}
|
||||
customRequest={handleUpload}
|
||||
onChange={(fileList) => {
|
||||
console.log('文件列表变化:', fileList);
|
||||
console.log('File list changed:', fileList);
|
||||
}}
|
||||
onRemove={async (file) => {
|
||||
// 如果文件正在上传,取消上传
|
||||
@@ -583,26 +583,26 @@ const CreateDataset = () => {
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
abortControllersRef.current.delete(fileUid);
|
||||
console.log('已取消上传:', (file as any).name);
|
||||
console.log('Upload cancelled:', (file as any).name);
|
||||
// 取消上传后直接返回 true,允许移除文件
|
||||
return true;
|
||||
}
|
||||
|
||||
// 只有当文件已经上传成功(有response.id)时,才删除服务器上的文件
|
||||
// Only delete server file when file upload was successful (has response.id)
|
||||
if (file.response?.id) {
|
||||
try {
|
||||
await deleteDocument(file.response.id);
|
||||
setRechunkFileIds(prev => prev.filter(id => id !== file.response.id));
|
||||
console.log('已删除服务器文件:', file.response.id);
|
||||
console.log('Server file deleted:', file.response.id);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('删除文件失败:', error);
|
||||
messageApi.error(t('common.deleteFailed') || '删除文件失败');
|
||||
return false; // 删除失败时不移除文件
|
||||
console.error('Failed to delete file:', error);
|
||||
messageApi.error(t('common.deleteFailed') || 'Failed to delete file');
|
||||
return false; // Don't remove file when deletion fails
|
||||
}
|
||||
}
|
||||
|
||||
// 其他情况(如上传失败的文件)也允许移除
|
||||
// Also allow removal in other cases (such as failed uploads)
|
||||
return true;
|
||||
}} />
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* @Description: 文档详情
|
||||
/**
|
||||
* @Description: Document Details
|
||||
* @Version: 0.0.1
|
||||
* @Author: yujiangping
|
||||
* @Date: 2025-11-15 16:13:47
|
||||
@@ -57,28 +57,28 @@ const DocumentDetails: FC = () => {
|
||||
}
|
||||
}, [documentId]);
|
||||
|
||||
// 更新面包屑
|
||||
// Update breadcrumbs
|
||||
useEffect(() => {
|
||||
if (breadcrumbPath) {
|
||||
updateBreadcrumbs(breadcrumbPath);
|
||||
}
|
||||
}, [breadcrumbPath, updateBreadcrumbs]);
|
||||
|
||||
// 当文档加载完成且 progress === 1 时,加载分块列表
|
||||
// Load chunk list when document is loaded and progress === 1
|
||||
useEffect(() => {
|
||||
if (document && document.progress === 1 && !isManualRefreshRef.current) {
|
||||
ChunkList();
|
||||
}
|
||||
// 重置标志
|
||||
// Reset flag
|
||||
isManualRefreshRef.current = false;
|
||||
}, [document]);
|
||||
|
||||
// 监听 keywords 变化,重新搜索
|
||||
// Listen to keywords changes and re-search
|
||||
useEffect(() => {
|
||||
if (documentId && keywords && document?.progress === 1) {
|
||||
setPage(1); // 重置页码
|
||||
setChunkList([]); // 清空列表
|
||||
ChunkList(1, false); // 重新加载第一页
|
||||
setPage(1); // Reset page number
|
||||
setChunkList([]); // Clear list
|
||||
ChunkList(1, false); // Reload first page
|
||||
}
|
||||
}, [keywords]);
|
||||
|
||||
@@ -129,9 +129,9 @@ const DocumentDetails: FC = () => {
|
||||
const url = `${imagePath}/api/files/${response.file_id}`
|
||||
setFileUrl(url);
|
||||
setParserMode(response?.parser_config?.auto_questions || 0)
|
||||
// ChunkList 会在 useEffect 中根据 document.progress 自动调用
|
||||
// ChunkList will be called automatically in useEffect based on document.progress
|
||||
} catch (error) {
|
||||
console.error('获取文档详情失败:', error);
|
||||
console.error('Failed to fetch document details:', error);
|
||||
message.error(t('common.loadFailed') || '加载失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@@ -140,12 +140,12 @@ const DocumentDetails: FC = () => {
|
||||
const ChunkList = async (pageNum: number = 1, append: boolean = false, force: boolean = false) => {
|
||||
if (!documentId) return;
|
||||
|
||||
// 如果不是强制刷新,且正在加载中,则跳过
|
||||
// Skip if not force refresh and already loading
|
||||
if (!force && chunkLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 只有当文档处理完成时才获取分块列表
|
||||
// Only fetch chunk list when document processing is complete
|
||||
if (document && document.progress !== 1) {
|
||||
return;
|
||||
}
|
||||
@@ -157,10 +157,10 @@ const DocumentDetails: FC = () => {
|
||||
keywords: keywords || undefined,
|
||||
page: pageNum,
|
||||
pagesize: 20,
|
||||
_t: force ? Date.now() : undefined, // 强制刷新时添加时间戳破坏缓存
|
||||
_t: force ? Date.now() : undefined, // Add timestamp to break cache when force refresh
|
||||
});
|
||||
|
||||
// 转换数据格式以匹配 RecallTestData
|
||||
// Convert data format to match RecallTestData
|
||||
const formattedChunks: RecallTestData[] = response.items.map((item: any) => ({
|
||||
page_content: item.page_content || item.content || '',
|
||||
vector: null,
|
||||
@@ -172,7 +172,7 @@ const DocumentDetails: FC = () => {
|
||||
document_id: item.metadata.document_id || documentId || '',
|
||||
knowledge_id: item.metadata.knowledge_id || knowledgeBaseId || '',
|
||||
sort_id: item.metadata.sort_id || item.id || 0,
|
||||
score: item.metadata.score || null, // chunk 列表没有相似度分数
|
||||
score: item.metadata.score || null, // Chunk list has no similarity score
|
||||
status: item.metadata.status,
|
||||
},
|
||||
children: null,
|
||||
@@ -186,7 +186,7 @@ const DocumentDetails: FC = () => {
|
||||
|
||||
setHasMore(response.page?.has_next ?? false);
|
||||
} catch (error) {
|
||||
console.error('获取文档详情失败:', error);
|
||||
console.error('Failed to fetch document details:', error);
|
||||
message.error(t('common.loadFailed') || '加载失败');
|
||||
} finally {
|
||||
setChunkLoading(false);
|
||||
@@ -201,17 +201,17 @@ const DocumentDetails: FC = () => {
|
||||
|
||||
const handleBack = () => {
|
||||
if (knowledgeBaseId && breadcrumbPath) {
|
||||
// 返回到知识库详情页,并传递面包屑信息以恢复状态
|
||||
// Return to knowledge base detail page and pass breadcrumb info to restore state
|
||||
const navigationState = {
|
||||
fromKnowledgeBaseList: true,
|
||||
knowledgeBaseFolderPath: breadcrumbPath.knowledgeBaseFolderPath,
|
||||
navigateToDocumentFolder: locationParentId,
|
||||
documentFolderPath: breadcrumbPath.documentFolderPath,
|
||||
timestamp: Date.now(), // 添加时间戳确保状态变化
|
||||
timestamp: Date.now(), // Add timestamp to ensure state change
|
||||
};
|
||||
navigate(`/knowledge-base/${knowledgeBaseId}/private`, { state: navigationState });
|
||||
} else if (knowledgeBaseId) {
|
||||
// 降级处理:直接跳转到知识库详情页
|
||||
// Fallback: Navigate directly to knowledge base detail page
|
||||
navigate(`/knowledge-base/${knowledgeBaseId}/private`);
|
||||
}
|
||||
};
|
||||
@@ -226,61 +226,61 @@ const DocumentDetails: FC = () => {
|
||||
insertModalRef.current?.handleOpen(documentId);
|
||||
};
|
||||
|
||||
// 处理插入/编辑内容
|
||||
// Handle insert/edit content
|
||||
const handleInsertContent = async (_docId: string, content: string, chunkId?: string): Promise<boolean> => {
|
||||
try {
|
||||
if (chunkId) {
|
||||
// 编辑模式:更新现有块
|
||||
// Edit mode: Update existing chunk
|
||||
const response = await updateDocumentChunk(knowledgeBaseId || '', documentId, chunkId, { content });
|
||||
|
||||
// 直接更新前端列表,不等待后端缓存刷新
|
||||
// Update frontend list directly without waiting for backend cache refresh
|
||||
setChunkList(prev => prev.map(item =>
|
||||
item.metadata?.doc_id === chunkId
|
||||
? { ...item, page_content: response.page_content || content }
|
||||
: item
|
||||
));
|
||||
|
||||
// 编辑模式返回特殊标记,告诉 InsertModal 不要调用 onSuccess
|
||||
// Edit mode returns special flag to tell InsertModal not to call onSuccess
|
||||
return true;
|
||||
} else {
|
||||
// 插入模式:创建新块
|
||||
// Insert mode: Create new chunk
|
||||
await createDocumentChunk(knowledgeBaseId || '', documentId, { content });
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error);
|
||||
console.error('Operation failed:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 处理点击文本块
|
||||
// Handle click on text chunk
|
||||
const handleChunkClick = (item: RecallTestData, index: number) => {
|
||||
if (!documentId) return;
|
||||
const chunkId = String(item.metadata?.doc_id || index);
|
||||
insertModalRef.current?.handleOpen(documentId, item.page_content, chunkId);
|
||||
};
|
||||
|
||||
// 插入成功后的回调(仅用于插入新块,编辑操作已在 handleInsertContent 中同步更新)
|
||||
// Callback after successful insert (only for inserting new chunks, edit operations are already updated synchronously in handleInsertContent)
|
||||
const handleInsertSuccess = () => {
|
||||
// 设置手动刷新标志,防止 useEffect 重复调用
|
||||
// Set manual refresh flag to prevent useEffect from calling repeatedly
|
||||
isManualRefreshRef.current = true;
|
||||
|
||||
// 重置页码
|
||||
// Reset page number
|
||||
setPage(1);
|
||||
|
||||
// 等待后端处理完成,然后重新加载数据(仅用于插入新块的情况)
|
||||
// Wait for backend processing to complete, then reload data (only for inserting new chunks)
|
||||
setTimeout(() => {
|
||||
ChunkList(1, false, true).then(() => {
|
||||
return fetchDocumentDetail();
|
||||
}).catch(err => {
|
||||
console.error('刷新失败:', err);
|
||||
console.error('Refresh failed:', err);
|
||||
});
|
||||
}, 1000);
|
||||
};
|
||||
const handleAdjustmentParameter = () =>{
|
||||
if (!knowledgeBaseId || !document) return;
|
||||
const targetFileId = document.id;
|
||||
// 优先使用从 location 传递的 parentId,其次使用 document.parent_id,最后使用 knowledgeBaseId
|
||||
// Prioritize parentId from location, then document.parent_id, finally knowledgeBaseId
|
||||
const parentId = locationParentId ?? document.parent_id ?? document.kb_id ?? knowledgeBaseId;
|
||||
|
||||
navigate(`/knowledge-base/${knowledgeBaseId}/create-dataset`, {
|
||||
@@ -317,7 +317,7 @@ const DocumentDetails: FC = () => {
|
||||
<img src={exitIcon} alt='exit' className='rb:w-4 rb:h-4' />
|
||||
<span className='rb:text-gray-500 rb:text-sm'>{t('common.exit')}</span>
|
||||
</div>
|
||||
{/* 文档预览 */}
|
||||
{/* Document preview */}
|
||||
{fileUrl && (
|
||||
<div className='rb:flex-1 rb:border rb:border-[#DFE4ED] rb:bg-white rb:rounded-xl rb:p-4 rb:overflow-hidden'>
|
||||
<h3 className="rb:text-sm rb:font-medium rb:mb-3">
|
||||
@@ -339,7 +339,7 @@ const DocumentDetails: FC = () => {
|
||||
|
||||
return (<>
|
||||
<div className="rb:flex rb:flex-col rb:h-full rb:p-4">
|
||||
{/* 头部 */}
|
||||
{/* Header */}
|
||||
<div className="rb:flex rb:flex-col rb:text-left rb:mb-6">
|
||||
<div className='rb:flex rb:items-center rb:justify-between'>
|
||||
<div className='rb:flex rb:items-center rb:gap-2 rb:mb-4 rb:cursor-pointer' onClick={handleBack}>
|
||||
@@ -366,9 +366,9 @@ const DocumentDetails: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 内容区域 */}
|
||||
{/* Content area */}
|
||||
<div className="rb:flex rb:h-full rb:gap-4 rb:flex-1 rb:overflow-hidden">
|
||||
{/* 左侧:文档信息 */}
|
||||
{/* Left: Document info */}
|
||||
<div className='rb:w-80 rb:h-full rb:flex rb:flex-col rb:gap-4 rb:overflow-hidden'>
|
||||
<div className='rb:border rb:border-[#DFE4ED] rb:bg-white rb:rounded-xl rb:p-4'>
|
||||
<InfoPanel
|
||||
@@ -381,7 +381,7 @@ const DocumentDetails: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧:分块列表 */}
|
||||
{/* Right: Chunk list */}
|
||||
<div
|
||||
id="chunkScrollableDiv"
|
||||
className="rb:flex-1 rb:bg-white rb:rounded-lg rb:border rb:border-gray-200 rb:p-6 rb:overflow-y-auto"
|
||||
@@ -404,7 +404,7 @@ const DocumentDetails: FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 插入内容弹窗 */}
|
||||
{/* Insert content modal */}
|
||||
<InsertModal
|
||||
ref={insertModalRef}
|
||||
onInsert={handleInsertContent}
|
||||
|
||||
@@ -40,7 +40,7 @@ import KnowledgeGraphCard from '../components/KnowledgeGraphCard';
|
||||
import { useBreadcrumbManager, type BreadcrumbItem } from '@/hooks/useBreadcrumbManager';
|
||||
import './Private.css'
|
||||
const { confirm } = Modal
|
||||
// 树节点数据类型
|
||||
// Tree node data type
|
||||
|
||||
const Private: FC = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -73,9 +73,9 @@ const Private: FC = () => {
|
||||
const [isGraph, setIsGraph] = useState(false);
|
||||
const { updateBreadcrumbs } = useBreadcrumbManager({
|
||||
breadcrumbType: 'detail',
|
||||
// 不提供 onKnowledgeBaseMenuClick,让它使用默认的导航行为(返回列表页面)
|
||||
// Don't provide onKnowledgeBaseMenuClick, let it use default navigation behavior (return to list page)
|
||||
onKnowledgeBaseFolderClick: useCallback((folderId: string, folderPath: Array<{ id: string; name: string }>) => {
|
||||
// 点击文件夹面包屑时,导航到对应文件夹
|
||||
// Navigate to corresponding folder when clicking folder breadcrumb
|
||||
setParentId(folderId);
|
||||
setFolderPath(folderPath);
|
||||
setSelectedKeys([folderId]);
|
||||
@@ -84,7 +84,7 @@ const Private: FC = () => {
|
||||
parent_id: folderId
|
||||
});
|
||||
|
||||
// 确保query对象发生变化,触发表格刷新
|
||||
// Ensure query object changes to trigger table refresh
|
||||
setQuery({
|
||||
orderby: 'created_at',
|
||||
desc: true,
|
||||
@@ -92,10 +92,10 @@ const Private: FC = () => {
|
||||
_timestamp: Date.now()
|
||||
});
|
||||
|
||||
// 确保API URL正确设置
|
||||
// Ensure API URL is set correctly
|
||||
setTableApi(`/documents/${knowledgeBaseId}/documents`);
|
||||
|
||||
// 手动触发表格刷新,确保数据更新
|
||||
// Manually trigger table refresh to ensure data update
|
||||
setTimeout(() => {
|
||||
tableRef.current?.loadData();
|
||||
}, 100);
|
||||
@@ -108,7 +108,7 @@ const Private: FC = () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await getKnowledgeBaseDetail(id);
|
||||
// 将 KnowledgeBase 转换为 KnowledgeBaseListItem
|
||||
// Convert KnowledgeBase to KnowledgeBaseListItem
|
||||
const listItem = res as unknown as KnowledgeBaseListItem;
|
||||
setKnowledgeBase(listItem);
|
||||
} finally {
|
||||
@@ -122,7 +122,7 @@ const Private: FC = () => {
|
||||
setTableApi(url);
|
||||
fetchKnowledgeBaseDetail(knowledgeBaseId);
|
||||
|
||||
// 立即设置基础面包屑,确保不会显示其他页面的面包屑
|
||||
// Immediately set base breadcrumbs to ensure other page breadcrumbs are not displayed
|
||||
updateBreadcrumbs({
|
||||
knowledgeBaseFolderPath,
|
||||
knowledgeBase: {
|
||||
@@ -135,7 +135,7 @@ const Private: FC = () => {
|
||||
}
|
||||
}, [knowledgeBaseId]);
|
||||
|
||||
// 更新面包屑
|
||||
// Update breadcrumbs
|
||||
useEffect(() => {
|
||||
if (knowledgeBase) {
|
||||
updateBreadcrumbs({
|
||||
@@ -150,22 +150,22 @@ const Private: FC = () => {
|
||||
}
|
||||
}, [knowledgeBase, knowledgeBaseFolderPath, folderPath, updateBreadcrumbs]);
|
||||
|
||||
// 监听 tableApi 变化,自动刷新表格数据
|
||||
// Listen to tableApi changes and auto refresh table data
|
||||
useEffect(() => {
|
||||
if (tableApi) {
|
||||
tableRef.current?.loadData();
|
||||
}
|
||||
}, [tableApi]);
|
||||
|
||||
// 监听 query 变化,确保表格数据更新
|
||||
// Listen to query changes and ensure table data update
|
||||
useEffect(() => {
|
||||
if (tableApi && query._timestamp) {
|
||||
// 当 query 中有 _timestamp 时,说明是通过面包屑或其他方式触发的更新
|
||||
// When query has _timestamp, it means the update is triggered by breadcrumb or other means
|
||||
tableRef.current?.loadData();
|
||||
}
|
||||
}, [query._timestamp, tableApi]);
|
||||
|
||||
// 监听 location state 变化
|
||||
// Listen to location state changes
|
||||
useEffect(() => {
|
||||
const state = location.state as {
|
||||
refresh?: boolean;
|
||||
@@ -180,18 +180,18 @@ const Private: FC = () => {
|
||||
|
||||
if (state?.refresh) {
|
||||
tableRef.current?.loadData();
|
||||
// 清除 state,避免重复刷新
|
||||
// Clear state to avoid repeated refresh
|
||||
navigate(location.pathname, { replace: true, state: {} });
|
||||
}
|
||||
|
||||
// 如果是从知识库列表页跳转过来的,设置知识库文件夹路径
|
||||
// If navigated from knowledge base list page, set knowledge base folder path
|
||||
if (state?.fromKnowledgeBaseList && state?.knowledgeBaseFolderPath) {
|
||||
setKnowledgeBaseFolderPath(state.knowledgeBaseFolderPath);
|
||||
}
|
||||
|
||||
// 如果需要重置到根目录(回到初始状态)
|
||||
// If need to reset to root directory (return to initial state)
|
||||
if (state?.resetToRoot) {
|
||||
// 重置所有状态到初始状态,和页面初始化保持一致
|
||||
// Reset all states to initial state, consistent with page initialization
|
||||
setParentId(knowledgeBaseId);
|
||||
setFolderPath([]);
|
||||
setSelectedKeys([]);
|
||||
@@ -202,31 +202,31 @@ const Private: FC = () => {
|
||||
setQuery({
|
||||
orderby: 'created_at',
|
||||
desc: true,
|
||||
_timestamp: Date.now() // 添加时间戳确保query对象发生变化,触发API调用
|
||||
_timestamp: Date.now() // Add timestamp to ensure query object changes and trigger API call
|
||||
});
|
||||
|
||||
// 重新设置API URL
|
||||
// Reset API URL
|
||||
const rootUrl = `/documents/${knowledgeBaseId}/documents`;
|
||||
setTableApi(rootUrl);
|
||||
|
||||
// 清除自动展开路径
|
||||
// Clear auto expand path
|
||||
setAutoExpandPath([]);
|
||||
|
||||
// 刷新文件夹树 - 使用延迟确保状态重置完成后再刷新
|
||||
// Refresh folder tree - use delay to ensure state reset is complete before refresh
|
||||
setTimeout(() => {
|
||||
setFolderTreeRefreshKey((prev) => prev + 1);
|
||||
}, 100);
|
||||
|
||||
// 手动触发表格刷新,确保数据更新
|
||||
// Manually trigger table refresh to ensure data update
|
||||
setTimeout(() => {
|
||||
tableRef.current?.loadData();
|
||||
}, 200);
|
||||
|
||||
// 清除 state,避免重复处理
|
||||
// Clear state to avoid repeated processing
|
||||
navigate(location.pathname, { replace: true, state: {} });
|
||||
}
|
||||
|
||||
// 如果是从文档详情页返回,恢复文档文件夹路径
|
||||
// If returning from document details page, restore document folder path
|
||||
if (state?.navigateToDocumentFolder && state?.documentFolderPath) {
|
||||
setFolderPath(state.documentFolderPath);
|
||||
setParentId(state.navigateToDocumentFolder);
|
||||
@@ -242,25 +242,25 @@ const Private: FC = () => {
|
||||
setTableApi(`/documents/${knowledgeBaseId}/documents`);
|
||||
setSelectedKeys([state.navigateToDocumentFolder]);
|
||||
|
||||
// 设置自动展开路径,让FolderTree自动展开到对应位置
|
||||
// Set auto expand path to let FolderTree auto expand to corresponding position
|
||||
setAutoExpandPath(state.documentFolderPath);
|
||||
|
||||
// 手动触发表格刷新
|
||||
// Manually trigger table refresh
|
||||
setTimeout(() => {
|
||||
tableRef.current?.loadData();
|
||||
}, 100);
|
||||
|
||||
// 清除自动展开路径,避免重复触发(延迟清除,确保FolderTree处理完成)
|
||||
// Clear auto expand path to avoid repeated trigger (delayed clear to ensure FolderTree processing is complete)
|
||||
setTimeout(() => {
|
||||
setAutoExpandPath([]);
|
||||
}, 2000);
|
||||
}
|
||||
}, [location.state, knowledgeBaseId, navigate, location.pathname]);
|
||||
|
||||
// 处理树节点选择
|
||||
// Handle tree node selection
|
||||
const onSelect = (keys: React.Key[]) => {
|
||||
if (!keys.length) {
|
||||
// 如果没有选中任何节点,回到根目录(初始状态)
|
||||
// If no node is selected, return to root directory (initial state)
|
||||
setParentId(knowledgeBaseId);
|
||||
setFolder({
|
||||
kb_id: knowledgeBaseId ?? '',
|
||||
@@ -269,7 +269,7 @@ const Private: FC = () => {
|
||||
setQuery({
|
||||
orderby: 'created_at',
|
||||
desc: true,
|
||||
_timestamp: Date.now() // 添加时间戳确保query对象发生变化
|
||||
_timestamp: Date.now() // Add timestamp to ensure query object changes
|
||||
});
|
||||
setSelectedKeys([]);
|
||||
return;
|
||||
@@ -284,7 +284,7 @@ const Private: FC = () => {
|
||||
setQuery({
|
||||
...query,
|
||||
parent_id: String(keys[0]),
|
||||
_timestamp: Date.now() // 添加时间戳确保query对象发生变化
|
||||
_timestamp: Date.now() // Add timestamp to ensure query object changes
|
||||
})
|
||||
let url = `/documents/${knowledgeBaseId}/documents`;
|
||||
|
||||
@@ -294,14 +294,14 @@ const Private: FC = () => {
|
||||
setSelectedKeys(keys)
|
||||
};
|
||||
|
||||
// 处理文件夹路径变化
|
||||
// Handle folder path change
|
||||
const handleFolderPathChange = (path: Array<{ id: string; name: string }>) => {
|
||||
setFolderPath(path);
|
||||
};
|
||||
|
||||
// 处理树节点展开
|
||||
// Handle tree node expand
|
||||
const onExpand = (_expandedKeys: React.Key[], _info: any) => {
|
||||
// 展开节点时不需要特殊处理
|
||||
// No special handling needed when expanding nodes
|
||||
};
|
||||
// create / import list
|
||||
const createItems: MenuProps['items'] = [
|
||||
@@ -344,13 +344,13 @@ const Private: FC = () => {
|
||||
// createImageDataset?.current?.handleOpen(knowledgeBaseId || '', parentId || '')
|
||||
// },
|
||||
// },
|
||||
// 暂时未实现
|
||||
// Not implemented yet
|
||||
// {
|
||||
// key: '4',
|
||||
// icon: <img src={blankIcon} alt="blank" style={{ width: 16, height: 16 }} />,
|
||||
// label: t('knowledgeBase.blankDataset'),
|
||||
// onClick: () => {
|
||||
// handleCreate('folder'); // 传入 type: 'folder'
|
||||
// handleCreate('folder'); // Pass type: 'folder'
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
@@ -362,7 +362,7 @@ const Private: FC = () => {
|
||||
// icon: <img src={templateIcon} alt="import" style={{ width: 16, height: 16 }} />,
|
||||
// label: t('knowledgeBase.importTemplate'),
|
||||
// onClick: () => {
|
||||
// handleCreate('folder'); // 传入 type: 'folder'
|
||||
// handleCreate('folder'); // Pass type: 'folder'
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
@@ -370,17 +370,17 @@ const Private: FC = () => {
|
||||
// icon: <img src={backupIcon} alt="import" style={{ width: 16, height: 16 }} />,
|
||||
// label: t('knowledgeBase.importBackup'),
|
||||
// onClick: () => {
|
||||
// handleCreate('folder'); // 传入 type: 'folder'
|
||||
// handleCreate('folder'); // Pass type: 'folder'
|
||||
// },
|
||||
// },
|
||||
|
||||
];
|
||||
|
||||
// 处理开关
|
||||
// Handle switch
|
||||
const onChange = (checked: boolean) => {
|
||||
if (!knowledgeBase) return;
|
||||
|
||||
// 构造完整的更新数据,保留现有配置
|
||||
// Construct complete update data, keeping existing configuration
|
||||
const updateData: KnowledgeBaseFormData = {
|
||||
name: knowledgeBase.name,
|
||||
description: knowledgeBase.description,
|
||||
@@ -411,30 +411,30 @@ const Private: FC = () => {
|
||||
updateKnowledgeBase(knowledgeBaseId || '', updateData);
|
||||
console.log(`switch to ${checked}`);
|
||||
};
|
||||
// 处理搜索
|
||||
// Handle search
|
||||
const handleSearch = (value?: string) => {
|
||||
setQuery({ ...query, keywords: value })
|
||||
}
|
||||
|
||||
// 处理分享
|
||||
// Handle share
|
||||
const handleShare = () => {
|
||||
shareModalRef?.current?.handleOpen(knowledgeBaseId,knowledgeBase);
|
||||
}
|
||||
// 处理分享回调,接收选中的数据
|
||||
// Handle share callback, receive selected data
|
||||
const handleShareCallback = (selectedData: { checkedItems: any[], selectedItem: any | null }) => {
|
||||
console.log('选中的数据:', selectedData);
|
||||
// checkedItems: 所有 checked 为 true 的数据
|
||||
// selectedItem: 当前选中的项(curIndex 对应的数据)
|
||||
// 在这里处理分享逻辑
|
||||
console.log('Selected data:', selectedData);
|
||||
// checkedItems: All data with checked = true
|
||||
// selectedItem: Currently selected item (corresponding to curIndex)
|
||||
// Handle share logic here
|
||||
}
|
||||
const handleCreateDatasetCallback = (payload: { value: number; title: string; description: string }) => {
|
||||
console.log('创建数据集:', payload);
|
||||
console.log('Create dataset:', payload);
|
||||
}
|
||||
// 处理设置
|
||||
// Handle settings
|
||||
const handleSetting = () => {
|
||||
modalRef?.current?.handleOpen(knowledgeBase, '');
|
||||
}
|
||||
// 处理召回测试
|
||||
// Handle recall test
|
||||
const handleRecallTest = () => {
|
||||
recallTestDrawerRef?.current?.handleOpen(knowledgeBaseId);
|
||||
}
|
||||
@@ -443,7 +443,7 @@ const Private: FC = () => {
|
||||
const handelCreateOrImport = () => {
|
||||
|
||||
}
|
||||
// 生成下拉菜单项(根据当前 row)
|
||||
// Generate dropdown menu items (based on current row)
|
||||
const getOptMenuItems = (row: KnowledgeBaseListItem): MenuProps['items'] => [
|
||||
{
|
||||
key: '1',
|
||||
@@ -495,19 +495,19 @@ const Private: FC = () => {
|
||||
deleteDocument(item.id)
|
||||
.then(() => {
|
||||
messageApi.success(t('common.deleteSuccess'));
|
||||
// 刷新表格数据
|
||||
// Refresh table data
|
||||
tableRef.current?.loadData();
|
||||
})
|
||||
.catch((err: any) => {
|
||||
console.log('删除失败', err);
|
||||
console.log('Delete failed', err);
|
||||
});
|
||||
},
|
||||
onCancel: () => {
|
||||
console.log('取消删除');
|
||||
console.log('Cancel delete');
|
||||
},
|
||||
});
|
||||
}
|
||||
// 表格列配置
|
||||
// Table column configuration
|
||||
const columns: ColumnsType = [
|
||||
{
|
||||
title: t('knowledgeBase.name'),
|
||||
@@ -524,7 +524,7 @@ const Private: FC = () => {
|
||||
state: {
|
||||
documentId: document.id,
|
||||
parentId: parentId ?? knowledgeBaseId,
|
||||
// 传递面包屑信息
|
||||
// Pass breadcrumb information
|
||||
breadcrumbPath: {
|
||||
knowledgeBaseFolderPath,
|
||||
knowledgeBase: {
|
||||
@@ -572,7 +572,7 @@ const Private: FC = () => {
|
||||
render: (value: string) => {
|
||||
if (!value) return '-';
|
||||
|
||||
// 解析日志格式,将 \n 转换为换行
|
||||
// Parse log format, convert \n to newline
|
||||
const formattedText = value.replace(/\\n/g, '\n');
|
||||
|
||||
return (
|
||||
@@ -634,25 +634,25 @@ const Private: FC = () => {
|
||||
),
|
||||
},
|
||||
];
|
||||
// 刷新列表数据
|
||||
// Refresh list data
|
||||
if (loading) {
|
||||
return <div>加载中...</div>;
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (!knowledgeBase) {
|
||||
return <div>知识库不存在</div>;
|
||||
}
|
||||
const refreshDirectoryTree = async () => {
|
||||
// 先刷新知识库详情,确保数据是最新的
|
||||
// First refresh knowledge base details to ensure data is up-to-date
|
||||
if (knowledgeBase?.id) {
|
||||
await fetchKnowledgeBaseDetail(knowledgeBase.id);
|
||||
}
|
||||
// 添加短暂延迟,确保后端数据已经完全更新
|
||||
// Add short delay to ensure backend data is fully updated
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
// 然后刷新文件夹树
|
||||
// Then refresh folder tree
|
||||
setFolderTreeRefreshKey((prev) => prev + 1);
|
||||
|
||||
// 确保 folder 状态正确设置
|
||||
// Ensure folder state is set correctly
|
||||
if (!folder) {
|
||||
setFolder({
|
||||
kb_id: knowledgeBaseId ?? '',
|
||||
@@ -663,10 +663,10 @@ const Private: FC = () => {
|
||||
}
|
||||
const handleRootTreeLoad = (nodes: TreeNodeData[] | null) => {
|
||||
if (!nodes || nodes.length === 0) {
|
||||
// 如果没有节点,设置folder为null(这会隐藏FolderTree)
|
||||
// If no nodes, set folder to null (this will hide FolderTree)
|
||||
setFolder(null);
|
||||
} else {
|
||||
// 如果有节点且 folder 为 null,重新设置 folder
|
||||
// If there are nodes and folder is null, reset folder
|
||||
if (!folder) {
|
||||
setFolder({
|
||||
kb_id: knowledgeBaseId ?? '',
|
||||
@@ -687,7 +687,7 @@ const Private: FC = () => {
|
||||
}
|
||||
|
||||
const handleRefreshTable = () => {
|
||||
// 刷新表格数据
|
||||
// Refresh table data
|
||||
fetchKnowledgeBaseDetail(knowledgeBase.id)
|
||||
tableRef.current?.loadData();
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ const Share: FC = () => {
|
||||
|
||||
if (knowledgeBaseId) {
|
||||
fetchKnowledgeBaseDetail(knowledgeBaseId);
|
||||
// 打开召回测试组件
|
||||
// Open recall test component
|
||||
setTimeout(() => {
|
||||
console.log('Share.tsx - calling handleOpen with:', knowledgeBaseId);
|
||||
recallTestRef.current?.handleOpen(knowledgeBaseId);
|
||||
@@ -48,7 +48,7 @@ const Share: FC = () => {
|
||||
}
|
||||
}, [knowledgeBaseId]);
|
||||
|
||||
// 更新面包屑
|
||||
// Update breadcrumbs
|
||||
useEffect(() => {
|
||||
if (knowledgeBase) {
|
||||
updateBreadcrumbs({
|
||||
@@ -63,14 +63,14 @@ const Share: FC = () => {
|
||||
}
|
||||
}, [knowledgeBase, knowledgeBaseFolderPath, updateBreadcrumbs]);
|
||||
|
||||
// 监听 location state 变化
|
||||
// Listen to location state changes
|
||||
useEffect(() => {
|
||||
const state = location.state as {
|
||||
fromKnowledgeBaseList?: boolean;
|
||||
knowledgeBaseFolderPath?: BreadcrumbItem[];
|
||||
} | null;
|
||||
|
||||
// 如果是从知识库列表页跳转过来的,设置知识库文件夹路径
|
||||
// If navigated from knowledge base list page, set knowledge base folder path
|
||||
if (state?.fromKnowledgeBaseList && state?.knowledgeBaseFolderPath) {
|
||||
setKnowledgeBaseFolderPath(state.knowledgeBaseFolderPath);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ const CreateContentModal = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
|
||||
const values = await form.validateFields();
|
||||
setLoading(true);
|
||||
|
||||
// TODO: 这里需要调用相应的API来保存内容
|
||||
// TODO: Call appropriate API to save content
|
||||
const params = {
|
||||
// ...values,
|
||||
kb_id: kbId,
|
||||
@@ -55,7 +55,7 @@ const CreateContentModal = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
|
||||
}
|
||||
handleClose();
|
||||
} catch (err) {
|
||||
console.error('创建内容失败:', err);
|
||||
console.error('Failed to create content:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -3,18 +3,18 @@ import { Button } from 'antd';
|
||||
import CreateContentModal from './CreateContentModal';
|
||||
import type { CreateContentModalRef } from '../types';
|
||||
|
||||
// 使用示例组件
|
||||
// Example usage component
|
||||
const CreateContentModalExample = () => {
|
||||
const createContentModalRef = useRef<CreateContentModalRef>(null);
|
||||
|
||||
const handleOpenModal = () => {
|
||||
// 打开弹窗,传入知识库ID和父级ID
|
||||
// Open modal, pass knowledge base ID and parent ID
|
||||
createContentModalRef.current?.handleOpen('kb_123', 'parent_456');
|
||||
};
|
||||
|
||||
const handleRefreshTable = () => {
|
||||
console.log('刷新表格数据');
|
||||
// 这里可以添加刷新表格的逻辑
|
||||
console.log('Refresh table data');
|
||||
// Add table refresh logic here
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* @Description:
|
||||
/**
|
||||
* @Description: Create Dataset Modal
|
||||
* @Version: 0.0.1
|
||||
* @Author: yujiangping
|
||||
* @Date: 2025-11-10 18:52:55
|
||||
|
||||
@@ -13,7 +13,7 @@ const CreateFolderModal = forwardRef<CreateFolderModalRef,CreateFolderModalRefPr
|
||||
const [form] = Form.useForm<FolderFormData>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
// Close modal and reset state
|
||||
const handleClose = () => {
|
||||
setFolder({} as FolderFormData);
|
||||
form.resetFields();
|
||||
@@ -22,17 +22,16 @@ const CreateFolderModal = forwardRef<CreateFolderModalRef,CreateFolderModalRefPr
|
||||
};
|
||||
|
||||
const handleOpen = (folder?: FolderFormData | null) => {
|
||||
debugger
|
||||
if (folder) {
|
||||
setFolder(folder);
|
||||
// 设置表单值
|
||||
// Set form values
|
||||
form.setFieldsValue({
|
||||
folder_name: folder.folder_name,
|
||||
parent_id: folder.parent_id ?? '',
|
||||
kb_id: folder.kb_id ?? '',
|
||||
});
|
||||
} else {
|
||||
// 新建时,重置表单并设置默认值
|
||||
// Reset form and set default values for new folder
|
||||
form.resetFields();
|
||||
form.setFieldsValue({
|
||||
parent_id: '',
|
||||
@@ -41,7 +40,7 @@ const CreateFolderModal = forwardRef<CreateFolderModalRef,CreateFolderModalRefPr
|
||||
}
|
||||
setVisible(true);
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
// Save form data and submit
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields({ validateOnly: true })
|
||||
@@ -74,13 +73,13 @@ const CreateFolderModal = forwardRef<CreateFolderModalRef,CreateFolderModalRefPr
|
||||
});
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
// Expose methods to parent component
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
// 根据 type 获取标题
|
||||
// Get modal title based on folder state
|
||||
const getTitle = () => {
|
||||
if (folder.id) {
|
||||
return t('common.edit') + ' ' + (folder.folder_name || '');
|
||||
|
||||
@@ -28,12 +28,12 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
|
||||
const [parentId, setParentId] = useState<string>('');
|
||||
const [hasFiles, setHasFiles] = useState(false);
|
||||
const uploadRef = useRef<{ fileList: UploadFile[]; clearFiles: () => void }>(null);
|
||||
// 存储每个文件的 AbortController,用于取消上传
|
||||
// Store AbortController for each file to cancel upload
|
||||
const abortControllersRef = useRef<Map<string, AbortController>>(new Map());
|
||||
// const fileIds = [];
|
||||
|
||||
const handleClose = () => {
|
||||
// 取消所有正在进行的上传
|
||||
// Cancel all ongoing uploads
|
||||
abortControllersRef.current.forEach((controller) => {
|
||||
controller.abort();
|
||||
});
|
||||
@@ -69,7 +69,7 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
|
||||
}
|
||||
const ids = fileList.map((file) => file.response?.id);
|
||||
handleChunking(kbId, parentId, ids)
|
||||
// // 上传所有图片
|
||||
// // Upload all images
|
||||
// const uploadPromises = fileList.map(async (file) => {
|
||||
// if (file.originFileObj) {
|
||||
// const formData = new FormData();
|
||||
@@ -91,7 +91,7 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
|
||||
|
||||
handleClose();
|
||||
} catch (err) {
|
||||
console.error('创建图片数据集失败:', err);
|
||||
console.error('Failed to create image dataset:', err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -112,7 +112,7 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
// 检查媒体文件时长的辅助函数
|
||||
// Helper function to check media file duration
|
||||
const checkMediaDuration = (file: File): Promise<number> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = URL.createObjectURL(file);
|
||||
@@ -131,7 +131,7 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
|
||||
media.src = url;
|
||||
});
|
||||
};
|
||||
// 删除已上传的文件
|
||||
// Delete uploaded file
|
||||
const handleDeleteFile = async (fileId: string) => {
|
||||
try {
|
||||
await deleteDocument(fileId);
|
||||
@@ -141,24 +141,24 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
|
||||
}
|
||||
};
|
||||
|
||||
// 上传文件
|
||||
// Upload file
|
||||
const handleUpload = async (options: UploadRequestOption) => {
|
||||
const { file, onSuccess, onError, onProgress, filename = 'file' } = options;
|
||||
|
||||
// 创建 AbortController 用于取消上传
|
||||
// Create AbortController to cancel upload
|
||||
const abortController = new AbortController();
|
||||
const fileUid = (file as any).uid;
|
||||
abortControllersRef.current.set(fileUid, abortController);
|
||||
|
||||
// 获取文件扩展名
|
||||
// Get file extension
|
||||
const fileExtension = (file as File).name.split('.').pop()?.toLowerCase();
|
||||
const mediaExtensions = ['mp3', 'mp4', 'mov', 'wav'];
|
||||
|
||||
// 如果是媒体文件,进行大小和时长检查
|
||||
// If it's a media file, check size and duration
|
||||
if (fileExtension && mediaExtensions.includes(fileExtension)) {
|
||||
const fileSizeInMB = (file as File).size / (50 * 1024);
|
||||
|
||||
// 检查文件大小(50MB限制)
|
||||
// Check file size (50MB limit)
|
||||
if (fileSizeInMB > 50) {
|
||||
messageApi.error(`${t('knowledgeBase.sizeLimitError')}:${fileSizeInMB.toFixed(2)}MB`);
|
||||
onError?.(new Error(`${t('knowledgeBase.fileSizeExceeds')}`));
|
||||
@@ -167,7 +167,7 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查媒体时长(150秒限制)
|
||||
// Check media duration (150 seconds limit)
|
||||
const duration = await checkMediaDuration(file as File);
|
||||
if (duration > 150) {
|
||||
messageApi.error(`${t('knowledgeBase.fileDurationLimitError')}:${Math.round(duration)}秒`);
|
||||
@@ -204,21 +204,21 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
|
||||
},
|
||||
});
|
||||
|
||||
// 上传成功,移除 AbortController
|
||||
// Upload successful, remove AbortController
|
||||
abortControllersRef.current.delete(fileUid);
|
||||
onSuccess?.(res, new XMLHttpRequest());
|
||||
|
||||
if (res?.id) {
|
||||
// 上传成功
|
||||
// Upload successful
|
||||
// fileIds.push(res.id)
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 移除 AbortController
|
||||
// Remove AbortController
|
||||
abortControllersRef.current.delete(fileUid);
|
||||
|
||||
// 如果是用户主动取消,不显示错误信息
|
||||
// If user actively cancelled, don't show error message
|
||||
if (error.name === 'AbortError' || error.code === 'ERR_CANCELED') {
|
||||
console.log('上传已取消:', (file as File).name);
|
||||
console.log('Upload cancelled:', (file as File).name);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -259,11 +259,11 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
|
||||
fileType={['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'mp3', 'mp4', 'mov', 'wav']}
|
||||
customRequest={handleUpload}
|
||||
onChange={(fileList) => {
|
||||
// 实时更新文件状态
|
||||
// Update file status in real-time
|
||||
setHasFiles(fileList.length > 0);
|
||||
}}
|
||||
onRemove={async (file) => {
|
||||
// 如果文件正在上传,取消上传
|
||||
// If file is uploading, cancel upload
|
||||
const fileUid = file.uid;
|
||||
const abortController = abortControllersRef.current.get(fileUid);
|
||||
if (abortController) {
|
||||
@@ -271,12 +271,12 @@ const CreateImageDataset = forwardRef<CreateSetModalRef, CreateSetMoealRefProps>
|
||||
abortControllersRef.current.delete(fileUid);
|
||||
}
|
||||
|
||||
// 如果文件已经上传成功,删除服务器上的文件
|
||||
// If file is already uploaded successfully, delete file on server
|
||||
if (file.response?.id) {
|
||||
await handleDeleteFile(file.response.id);
|
||||
}
|
||||
|
||||
return true; // 允许移除文件
|
||||
return true; // Allow file removal
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
@@ -15,7 +15,7 @@ import RbModal from '@/components/RbModal'
|
||||
const { TextArea } = Input;
|
||||
const { confirm } = Modal
|
||||
|
||||
// 全局模型数据常量
|
||||
// Global model data constant
|
||||
let models: any = null;
|
||||
|
||||
const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
@@ -33,9 +33,9 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
const [activeTab, setActiveTab] = useState('basic');
|
||||
const [generatingEntityTypes, setGeneratingEntityTypes] = useState(false);
|
||||
const [isRebuildMode, setIsRebuildMode] = useState(false);
|
||||
const [originalType, setOriginalType] = useState<string>(''); // 保存原始的 type 参数
|
||||
const [originalType, setOriginalType] = useState<string>(''); // Save original type parameter
|
||||
|
||||
// 监听 parser_config.graphrag 相关字段的变化
|
||||
// Watch for changes to parser_config.graphrag related fields
|
||||
const parserConfig = Form.useWatch('parser_config', form);
|
||||
const graphragConfig = parserConfig?.graphrag;
|
||||
const enableKnowledgeGraph = graphragConfig?.use_graphrag || false;
|
||||
@@ -43,30 +43,30 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
const entityNormalization = graphragConfig?.resolution || false;
|
||||
const communityReportGeneration = graphragConfig?.community || false;
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
// Encapsulate cancel method, add close modal logic
|
||||
const handleClose = () => {
|
||||
setDatasets(null);
|
||||
form.resetFields();
|
||||
setLoading(false);
|
||||
setActiveTab('basic');
|
||||
setIsRebuildMode(false); // 重置重建模式标识
|
||||
setOriginalType(''); // 重置原始 type
|
||||
setIsRebuildMode(false); // Reset rebuild mode flag
|
||||
setOriginalType(''); // Reset original type
|
||||
setVisible(false);
|
||||
};
|
||||
|
||||
// 生成实体类型的函数
|
||||
// Generate entity types function
|
||||
const generateEntityTypes = async () => {
|
||||
const sceneName = form.getFieldValue(['parser_config', 'graphrag', 'scene_name']);
|
||||
if (!sceneName) {
|
||||
// 可以添加提示用户输入场景名称
|
||||
// Can add prompt for user to enter scenario name
|
||||
messageApi.error(t('knowledgeBase.enterScenarioName'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否选择了 LLM 模型
|
||||
// Check if LLM model is selected
|
||||
const llmId = form.getFieldValue('llm_id');
|
||||
if (!llmId) {
|
||||
// 跳转到基础配置页
|
||||
// Navigate to basic configuration page
|
||||
setActiveTab('basic');
|
||||
messageApi.error(t('knowledgeBase.pleaseSelectLLMModel'));
|
||||
return;
|
||||
@@ -74,7 +74,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
|
||||
setGeneratingEntityTypes(true);
|
||||
try {
|
||||
// 这里应该调用实际的API接口
|
||||
// Call the actual API interface here
|
||||
// const user = JSON.parse(localStorage.getItem('user') as any);
|
||||
//datasets?.id || datasets?.parent_id || user?.current_workspace_id,
|
||||
const params = {
|
||||
@@ -82,17 +82,17 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
llm_id: llmId
|
||||
};
|
||||
const response = await getKnowledgeGraphEntityTypes(params);
|
||||
// 模拟API调用
|
||||
// Simulate API call
|
||||
// await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// 处理API响应数据
|
||||
console.log('API Response:', response); // 调试日志
|
||||
// Process API response data
|
||||
console.log('API Response:', response); // Debug log
|
||||
|
||||
// 检查响应结构 - API直接返回字符串
|
||||
// Check response structure - API returns string directly
|
||||
if (response && typeof response === 'string' && response.trim()) {
|
||||
// 将逗号分隔的字符串转换为换行分隔的格式以便在TextArea中显示
|
||||
// Convert comma-separated string to newline-separated format for TextArea display
|
||||
const entityTypesString = response.replace(/,\s*/g, '\n');
|
||||
console.log('Converted entity types:', entityTypesString); // 调试日志
|
||||
console.log('Converted entity types:', entityTypesString); // Debug log
|
||||
|
||||
const currentGraphrag = form.getFieldValue(['parser_config', 'graphrag']) || {};
|
||||
const updatedGraphrag = {
|
||||
@@ -100,22 +100,22 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
entity_types: entityTypesString
|
||||
};
|
||||
|
||||
console.log('Updating form with:', updatedGraphrag); // 调试日志
|
||||
console.log('Updating form with:', updatedGraphrag); // Debug log
|
||||
|
||||
// 使用更直接的方式更新表单字段
|
||||
// Use more direct way to update form field
|
||||
form.setFieldValue(['parser_config', 'graphrag', 'entity_types'], entityTypesString);
|
||||
|
||||
// 强制触发表单重新渲染
|
||||
// Force trigger form re-render
|
||||
form.validateFields([['parser_config', 'graphrag', 'entity_types']]);
|
||||
|
||||
// 额外的强制更新机制
|
||||
// Additional forced update mechanism
|
||||
setTimeout(() => {
|
||||
form.setFieldValue(['parser_config', 'graphrag', 'entity_types'], entityTypesString);
|
||||
}, 100);
|
||||
|
||||
messageApi.success(t('knowledgeBase.generateEntityTypesSuccess'));
|
||||
} else {
|
||||
messageApi.error(t('knowledgeBase.generateEntityTypesFailed') + ':' + t('knowledgeBase.unknownError'));
|
||||
messageApi.error(t('knowledgeBase.generateEntityTypesFailed') + ': ' + t('knowledgeBase.unknownError'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(t('knowledgeBase.generateEntityTypesFailed') + ':', error);
|
||||
@@ -143,7 +143,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
};
|
||||
|
||||
const fetchModelLists = async (types: string[]) => {
|
||||
// 如果还没有获取过全部模型数据,则获取一次
|
||||
// If model data hasn't been fetched yet, fetch it once
|
||||
if (!models) {
|
||||
try {
|
||||
models = await getModelList({ page: 1, pagesize: 100 });
|
||||
@@ -153,7 +153,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
}
|
||||
}
|
||||
|
||||
// 从全部模型数据中过滤出需要的类型
|
||||
// Filter out the required types from all model data
|
||||
const typesToFetch = types.includes('llm') ? [...types, 'chat'] : types;
|
||||
const next: Record<string, { label: string; value: string }[]> = {};
|
||||
|
||||
@@ -165,7 +165,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
|
||||
setModelOptionsByType(next);
|
||||
|
||||
// 如果不是编辑模式,为每个类型的下拉框设置默认值为第一条数据
|
||||
// If not in edit mode, set default value to first item for each type dropdown
|
||||
if (!datasets?.id) {
|
||||
const defaultValues: Record<string, string> = {};
|
||||
types.forEach((tp) => {
|
||||
@@ -174,7 +174,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
? [...(next['llm'] || []), ...(next['chat'] || [])]
|
||||
: next[tp] || [];
|
||||
|
||||
// 如果有选项且当前字段没有值,设置第一个选项为默认值
|
||||
// If there are options and current field has no value, set first option as default
|
||||
if (options.length > 0 && !form.getFieldValue(fieldKey)) {
|
||||
defaultValues[fieldKey] = options[0].value;
|
||||
}
|
||||
@@ -204,7 +204,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
status: record.status,
|
||||
};
|
||||
|
||||
// 处理 parser_config 配置数据,如果没有则设置默认值
|
||||
// Process parser_config data, set default values if not present
|
||||
baseValues.parser_config = record.parser_config || {
|
||||
graphrag: {
|
||||
use_graphrag: false,
|
||||
@@ -216,13 +216,13 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
}
|
||||
};
|
||||
|
||||
// 如果存在 entity_types,转换为换行分隔格式用于 TextArea 显示
|
||||
// If entity_types exists, convert to newline-separated format for TextArea display
|
||||
if (baseValues.parser_config.graphrag.entity_types) {
|
||||
if (Array.isArray(baseValues.parser_config.graphrag.entity_types)) {
|
||||
// 如果是数组格式,转换为换行分隔字符串
|
||||
// If array format, convert to newline-separated string
|
||||
(baseValues.parser_config.graphrag as any).entity_types = baseValues.parser_config.graphrag.entity_types.join('\n');
|
||||
} else if (typeof baseValues.parser_config.graphrag.entity_types === 'string') {
|
||||
// 如果是逗号分隔字符串格式,转换为换行分隔字符串(兼容旧数据)
|
||||
// If comma-separated string format, convert to newline-separated string (compatible with old data)
|
||||
(baseValues.parser_config.graphrag as any).entity_types = (baseValues.parser_config.graphrag.entity_types as string).replace(/,\s*/g, '\n');
|
||||
}
|
||||
}
|
||||
@@ -249,13 +249,13 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
const handleOpen = (record?: KnowledgeBaseListItem | null, type?: string) => {
|
||||
setDatasets(record || null);
|
||||
|
||||
// 如果是重建模式,使用记录的实际类型,否则使用传入的类型
|
||||
// If rebuild mode, use record's actual type, otherwise use passed type
|
||||
const actualType = type === 'rebuild' ? (record?.type || 'General') : (type || currentType);
|
||||
setCurrentType(actualType as any);
|
||||
setIsRebuildMode(type === 'rebuild'); // 设置重建模式标识
|
||||
setOriginalType(type || ''); // 保存原始的 type 参数
|
||||
setIsRebuildMode(type === 'rebuild'); // Set rebuild mode flag
|
||||
setOriginalType(type || ''); // Save original type parameter
|
||||
|
||||
// 如果是重建模式,默认切换到知识图谱标签页
|
||||
// If rebuild mode, default to knowledge graph tab
|
||||
if (type === 'rebuild') {
|
||||
setActiveTab('knowledgeGraph');
|
||||
} else {
|
||||
@@ -285,13 +285,13 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
setDynamicModelFields(datasets, modelTypeList);
|
||||
}, [visible, datasets, currentType, modelTypeList]);
|
||||
|
||||
// 封装保存方法,添加提交逻辑
|
||||
// Encapsulate save method, add submit logic
|
||||
const handleSave = () => {
|
||||
// 获取当前表单中的知识图谱开启状态
|
||||
// Get current knowledge graph enabled status from form
|
||||
const currentFormValues = form.getFieldsValue();
|
||||
const isGraphragEnabled = currentFormValues?.parser_config?.graphrag?.use_graphrag || false;
|
||||
|
||||
// 如果原始 type 是 'rebuild' 并且知识图谱开启为true,显示确认弹框
|
||||
// If original type is 'rebuild' and knowledge graph is enabled, show confirmation dialog
|
||||
if (originalType === 'rebuild' && isGraphragEnabled) {
|
||||
confirm({
|
||||
title: t('knowledgeBase.rebuildConfirmTitle'),
|
||||
@@ -302,11 +302,11 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
await rebuildKnowledgeGraph(datasets?.id || '')
|
||||
},
|
||||
onCancel: () => {
|
||||
// 用户取消,不执行任何操作
|
||||
// User cancelled, no action taken
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// 非重建模式或知识图谱未开启,直接保存
|
||||
// Non-rebuild mode or knowledge graph not enabled, save directly
|
||||
performSave();
|
||||
}
|
||||
};
|
||||
@@ -318,7 +318,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
messageApi.error(t('knowledgeBase.deleteGraphFailed'))
|
||||
}
|
||||
};
|
||||
// 实际的保存逻辑
|
||||
// Actual save logic
|
||||
const performSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
@@ -326,7 +326,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
setLoading(true)
|
||||
const formValues = form.getFieldsValue();
|
||||
|
||||
// 处理 entity_types 格式转换:从换行分隔字符串转换为字符串数组
|
||||
// Process entity_types format conversion: from newline-separated string to string array
|
||||
if (formValues.parser_config && formValues.parser_config.graphrag && formValues.parser_config.graphrag.entity_types) {
|
||||
const entityTypesString = formValues.parser_config.graphrag.entity_types as any as string;
|
||||
const entityTypesArray = entityTypesString
|
||||
@@ -336,7 +336,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
formValues.parser_config.graphrag.entity_types = entityTypesArray;
|
||||
}
|
||||
|
||||
// 确保保存时使用正确的类型(不是 'rebuild')
|
||||
// Ensure correct type is used when saving (not 'rebuild')
|
||||
const saveType = originalType === 'rebuild' ? currentType : (formValues.type || currentType);
|
||||
|
||||
const payload: KnowledgeBaseFormData = {
|
||||
@@ -346,7 +346,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
parent_id: datasets?.parent_id || undefined,
|
||||
};
|
||||
|
||||
console.log('Saving payload:', payload); // 调试日志
|
||||
console.log('Saving payload:', payload); // Debug log
|
||||
|
||||
const submit = datasets?.id
|
||||
? updateKnowledgeBase(datasets.id, payload)
|
||||
@@ -367,32 +367,32 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
});
|
||||
}
|
||||
const handleChange = (_value: string, tp: string) => {
|
||||
// 只在编辑模式且类型为 embedding 时触发提示
|
||||
// Only trigger prompt in edit mode and when type is embedding
|
||||
if (datasets?.id && tp.toLowerCase() === 'embedding') {
|
||||
const fieldKey = typeToFieldKey(tp);
|
||||
// 从原始 datasets 对象中获取之前的值
|
||||
// Get previous value from original datasets object
|
||||
const previousValue = (datasets as any)[fieldKey];
|
||||
|
||||
confirm({
|
||||
title: t('common.updateWarning'),
|
||||
content: t('knowledgeBase.updateEmbeddingContent'),
|
||||
onOk: () => {
|
||||
// 确定时什么也不做,保持新值
|
||||
// Do nothing on confirm, keep new value
|
||||
},
|
||||
onCancel: () => {
|
||||
// 取消时恢复之前的值
|
||||
// Restore previous value on cancel
|
||||
form.setFieldsValue({ [fieldKey]: previousValue } as any);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
// 暴露给父组件的方法
|
||||
// Methods exposed to parent component
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
// 根据 type 获取标题
|
||||
// Get title based on type
|
||||
const getTitle = () => {
|
||||
if (isRebuildMode) {
|
||||
return t('knowledgeBase.rebuildGraph') + ' - ' + (datasets?.name || '');
|
||||
@@ -408,7 +408,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
|
||||
const dynamicTypeList = useMemo(() => modelTypeList.filter((tp) => (modelOptionsByType[tp] || []).length), [modelTypeList, modelOptionsByType]);
|
||||
|
||||
// 基础配置表单内容
|
||||
// Basic configuration form content
|
||||
const renderBasicConfig = () => (
|
||||
<>
|
||||
{!datasets?.id && (
|
||||
@@ -426,7 +426,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
|
||||
{currentType !== 'Folder' && dynamicTypeList.map((tp) => {
|
||||
const fieldKey = typeToFieldKey(tp);
|
||||
// 当 tp 为 'llm' 时,合并 llm 和 chat 的选项
|
||||
// When tp is 'llm', merge llm and chat options
|
||||
const options = tp.toLowerCase() === 'llm'
|
||||
? [...(modelOptionsByType['llm'] || []), ...(modelOptionsByType['chat'] || [])]
|
||||
: modelOptionsByType[tp] || [];
|
||||
@@ -451,7 +451,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
</>
|
||||
);
|
||||
|
||||
// 知识图谱配置表单内容
|
||||
// Knowledge graph configuration form content
|
||||
const renderKnowledgeGraphConfig = () => (
|
||||
<>
|
||||
<div className={`rb:flex rb:w-full rb:items-center rb:p-4 rb:border-1 rb:rounded-lg rb:mb-4 ${
|
||||
@@ -482,7 +482,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
<div className='rb:text-[#212332] rb:text-base rb:font-medium rb:mb-4'>
|
||||
{t('knowledgeBase.graphConfig')}
|
||||
</div>
|
||||
{/* 场景名称 */}
|
||||
{/* Scene name */}
|
||||
<div className='rb:flex rb:items-center rb:gap-2'>
|
||||
<Form.Item
|
||||
name={['parser_config', 'graphrag', 'scene_name']}
|
||||
@@ -506,7 +506,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
</div>
|
||||
|
||||
|
||||
{/* 实体类型 */}
|
||||
{/* Entity types */}
|
||||
<Form.Item
|
||||
name={['parser_config', 'graphrag', 'entity_types']}
|
||||
label={t('knowledgeBase.entityTypes')}
|
||||
@@ -517,7 +517,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 实体归一化 */}
|
||||
{/* Entity normalization */}
|
||||
<div className={`rb:flex rb:w-full rb:gap-2 rb:items-center rb:p-4 rb:border-1 rb:rounded-lg rb:mb-4 ${
|
||||
entityNormalization
|
||||
? 'rb:border-[#155EEF] rb:bg-[rgba(21,94,239,0.06)]'
|
||||
@@ -541,7 +541,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
</div>
|
||||
|
||||
|
||||
{/* 实体方法 */}
|
||||
{/* Entity method */}
|
||||
<Form.Item
|
||||
name={['parser_config', 'graphrag', 'method']}
|
||||
label={t('knowledgeBase.entityMethod')}
|
||||
@@ -553,7 +553,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
{/* 社区报告生成 */}
|
||||
{/* Community report generation */}
|
||||
<div className={`rb:flex rb:w-full rb:gap-2 rb:items-center rb:p-4 rb:border-1 rb:rounded-lg rb:mb-4 ${
|
||||
communityReportGeneration
|
||||
? 'rb:border-[#155EEF] rb:bg-[rgba(21,94,239,0.06)]'
|
||||
@@ -580,7 +580,7 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
</>
|
||||
);
|
||||
|
||||
// Tabs 配置
|
||||
// Tabs configuration
|
||||
const tabItems = [
|
||||
{
|
||||
key: 'basic',
|
||||
@@ -607,16 +607,16 @@ const CreateModal = forwardRef<CreateModalRef, CreateModalRefProps>(({
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
permission_id: 'Private', // 设置 permission_id 的默认值
|
||||
permission_id: 'Private', // Set default value for permission_id
|
||||
type: currentType,
|
||||
parser_config: {
|
||||
graphrag: {
|
||||
use_graphrag: false, // 默认不启用知识图谱
|
||||
scene_name: '', // 场景名称
|
||||
entity_types: '' as any, // 实体类型(界面上显示为字符串,保存时转为数组)
|
||||
method: 'general', // 默认使用通用方法
|
||||
resolution: false, // 默认不启用实体归一化
|
||||
community: false, // 默认不生成社区报告
|
||||
use_graphrag: false, // Default not to enable knowledge graph
|
||||
scene_name: '', // Scene name
|
||||
entity_types: '' as any, // Entity types (displayed as string in UI, converted to array when saving)
|
||||
method: 'general', // Default to use general method
|
||||
resolution: false, // Default not to enable entity normalization
|
||||
community: false, // Default not to generate community reports
|
||||
}
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -17,13 +17,13 @@ const DelimiterSelector: FC<DelimiterSelectorProps> = ({
|
||||
className = '',
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
// 默认值为空字符串(不设置)
|
||||
// Default value is empty string (not set)
|
||||
const [selectedValue, setSelectedValue] = useState<string>(value || '');
|
||||
const [customValue, setCustomValue] = useState<string>('');
|
||||
const [showCustomInput, setShowCustomInput] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// 检查当前值是否为自定义值
|
||||
// Check if current value is a custom delimiter
|
||||
if (value && isCustomDelimiter(value) && value !== 'custom') {
|
||||
setSelectedValue('custom');
|
||||
setCustomValue(value);
|
||||
@@ -39,15 +39,15 @@ const DelimiterSelector: FC<DelimiterSelectorProps> = ({
|
||||
|
||||
if (val === 'custom') {
|
||||
setShowCustomInput(true);
|
||||
// 如果已有自定义值,使用它;否则等待用户输入
|
||||
// If custom value exists, use it; otherwise wait for user input
|
||||
if (customValue) {
|
||||
onChange?.(customValue);
|
||||
} else {
|
||||
// 自定义但还没输入值,暂不触发 onChange
|
||||
// Custom selected but no value entered yet, don't trigger onChange
|
||||
onChange?.(undefined);
|
||||
}
|
||||
} else if (val === '') {
|
||||
// 选择"不设置"时,返回 undefined(不传递该参数)
|
||||
// When "Not set" is selected, return undefined (don't pass this parameter)
|
||||
setShowCustomInput(false);
|
||||
onChange?.(undefined);
|
||||
} else {
|
||||
@@ -59,7 +59,7 @@ const DelimiterSelector: FC<DelimiterSelectorProps> = ({
|
||||
const handleCustomInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const val = e.target.value;
|
||||
setCustomValue(val);
|
||||
// 只有当输入不为空时才触发 onChange
|
||||
// Only trigger onChange when input is not empty
|
||||
onChange?.(val || undefined);
|
||||
};
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ interface FolderTreeProps {
|
||||
onRootLoad?: (nodes: TreeNodeData[] | null) => void;
|
||||
onFolderPathChange?: (path: Array<{ id: string; name: string }>) => void;
|
||||
selectedKeys?: React.Key[];
|
||||
// 新增:自动展开到指定路径
|
||||
// New: Auto expand to specified path
|
||||
autoExpandPath?: Array<{ id: string; name: string }>;
|
||||
}
|
||||
|
||||
@@ -221,7 +221,7 @@ const extractItems = (resp: any): any[] => {
|
||||
return [];
|
||||
};
|
||||
|
||||
// 只加载当前层级的节点,不递归加载子节点
|
||||
// Only load nodes at current level, don't recursively load child nodes
|
||||
const buildTreeNodes = async (
|
||||
kbId: string,
|
||||
parentId: string,
|
||||
@@ -229,7 +229,7 @@ const buildTreeNodes = async (
|
||||
const currentParent = String(parentId ?? '');
|
||||
if (!currentParent) return [];
|
||||
|
||||
// 只请求一次当前层级的数据,不分页
|
||||
// Only request current level data once, no pagination
|
||||
const response = await getFolderList({
|
||||
kb_id: kbId,
|
||||
parent_id: currentParent,
|
||||
@@ -246,20 +246,20 @@ const buildTreeNodes = async (
|
||||
const nodeKey = String(keySource);
|
||||
const isFolder = isFolderLike(raw);
|
||||
|
||||
// 只显示文件夹
|
||||
// Only show folders
|
||||
if (!isFolder) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 文件夹节点初始不加载子节点,isLeaf设为false表示可能有子节点
|
||||
// Folder node initially doesn't load child nodes, isLeaf set to false indicates possible child nodes
|
||||
nodes.push({
|
||||
key: nodeKey,
|
||||
title: getNodeTitle(raw),
|
||||
icon: getNodeIcon(raw, isFolder),
|
||||
switcherIcon: isFolder ? switcherIcon : undefined,
|
||||
type: isFolder ? 'folder' : (typeof raw?.type === 'string' ? raw.type : normalizeExt(raw?.file_ext) || 'file'),
|
||||
isLeaf: false, // 文件夹节点初始设为false,表示可能有子节点,需要展开时加载
|
||||
children: undefined, // 初始不加载子节点
|
||||
isLeaf: false, // Folder node initially set to false, indicating possible child nodes, load when expanded
|
||||
children: undefined, // Initially don't load child nodes
|
||||
});
|
||||
}
|
||||
|
||||
@@ -283,7 +283,7 @@ const FolderTree: FC<FolderTreeProps> = ({
|
||||
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
|
||||
const [autoExpandInProgress, setAutoExpandInProgress] = useState(false);
|
||||
|
||||
// 更新树节点数据的辅助函数
|
||||
// Helper function to update tree node data
|
||||
const updateTreeData = (nodes: TreeNodeData[], key: Key, children: TreeNodeData[]): TreeNodeData[] => {
|
||||
return nodes.map((node) => {
|
||||
if (node.key === key) {
|
||||
@@ -303,17 +303,17 @@ const FolderTree: FC<FolderTreeProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
// 加载根节点
|
||||
// Load root nodes
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const load = async () => {
|
||||
if (!knowledgeBaseId) {
|
||||
setTreeData([]);
|
||||
setExpandedKeys([]); // 重置展开状态
|
||||
setExpandedKeys([]); // Reset expand state
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 重置展开状态,确保从根目录开始
|
||||
// Reset expand state, ensure starting from root directory
|
||||
setExpandedKeys([]);
|
||||
|
||||
const nodes = await buildTreeNodes(knowledgeBaseId, knowledgeBaseId);
|
||||
@@ -324,7 +324,7 @@ const FolderTree: FC<FolderTreeProps> = ({
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载文件夹树失败:', e);
|
||||
console.error('Failed to load folder tree:', e);
|
||||
if (!cancelled) {
|
||||
const fallback = buildMockTreeData();
|
||||
setTreeData(fallback);
|
||||
@@ -340,27 +340,27 @@ const FolderTree: FC<FolderTreeProps> = ({
|
||||
};
|
||||
}, [knowledgeBaseId, refreshKey]);
|
||||
|
||||
// 懒加载子节点 - 只在展开时加载
|
||||
// Lazy load child nodes - only load when expanded
|
||||
const onLoadData = async (node: any) => {
|
||||
const { key } = node;
|
||||
|
||||
// 如果已经加载过子节点,不再重复加载
|
||||
// If child nodes already loaded, don't reload
|
||||
if (node.children !== undefined) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用节点的 key 作为 parent_id 加载子文件夹
|
||||
// Use node's key as parent_id to load child folders
|
||||
const children = await buildTreeNodes(knowledgeBaseId, String(key));
|
||||
setTreeData((prevData) => updateTreeData(prevData, key, children));
|
||||
} catch (e) {
|
||||
console.error('加载子节点失败:', e);
|
||||
// 加载失败时,将该节点标记为叶子节点(没有子节点)
|
||||
console.error('Failed to load child nodes:', e);
|
||||
// On load failure, mark this node as leaf node (no child nodes)
|
||||
setTreeData((prevData) => updateTreeData(prevData, key, []));
|
||||
}
|
||||
};
|
||||
|
||||
// 查找节点路径的辅助函数
|
||||
// Helper function to find node path
|
||||
const findNodePath = (nodes: TreeNodeData[], targetKey: Key, currentPath: Array<{ id: string; name: string }> = []): Array<{ id: string; name: string }> | null => {
|
||||
for (const node of nodes) {
|
||||
const newPath = [...currentPath, { id: String(node.key), name: String(node.title) }];
|
||||
@@ -379,7 +379,7 @@ const FolderTree: FC<FolderTreeProps> = ({
|
||||
return null;
|
||||
};
|
||||
|
||||
// 查找节点的辅助函数
|
||||
// Helper function to find node
|
||||
const findNodeInTree = (nodes: TreeNodeData[], key: string): TreeNodeData | null => {
|
||||
for (const node of nodes) {
|
||||
if (String(node.key) === key) {
|
||||
@@ -393,7 +393,7 @@ const FolderTree: FC<FolderTreeProps> = ({
|
||||
return null;
|
||||
};
|
||||
|
||||
// 渐进式自动展开到指定路径
|
||||
// Progressive auto expand to specified path
|
||||
useEffect(() => {
|
||||
if (!autoExpandPath || autoExpandPath.length === 0 || autoExpandInProgress || treeData.length === 0) {
|
||||
return;
|
||||
@@ -406,46 +406,46 @@ const FolderTree: FC<FolderTreeProps> = ({
|
||||
const keysToExpand: React.Key[] = [];
|
||||
let currentTreeData = treeData;
|
||||
|
||||
// 逐级展开,从第一级开始(跳过根节点,因为根节点已经加载)
|
||||
// Expand level by level, starting from first level (skip root node as it's already loaded)
|
||||
for (let i = 0; i < autoExpandPath.length - 1; i++) {
|
||||
const nodeKey = autoExpandPath[i].id;
|
||||
keysToExpand.push(nodeKey);
|
||||
|
||||
// 查找当前节点
|
||||
// Find current node
|
||||
const targetNode = findNodeInTree(currentTreeData, nodeKey);
|
||||
|
||||
if (targetNode && targetNode.children === undefined) {
|
||||
// 如果子节点未加载,先加载
|
||||
// If child nodes not loaded, load first
|
||||
try {
|
||||
console.log(`自动展开:加载节点 ${nodeKey} 的子节点`);
|
||||
console.log(`Auto expand: Loading child nodes of ${nodeKey}`);
|
||||
const children = await buildTreeNodes(knowledgeBaseId, nodeKey);
|
||||
|
||||
// 更新树数据
|
||||
// Update tree data
|
||||
setTreeData((prevData) => {
|
||||
const newData = updateTreeData(prevData, nodeKey, children);
|
||||
currentTreeData = newData; // 更新当前引用
|
||||
currentTreeData = newData; // Update current reference
|
||||
return newData;
|
||||
});
|
||||
|
||||
// 等待状态更新完成
|
||||
// Wait for state update to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
|
||||
} catch (error) {
|
||||
console.error(`自动展开时加载节点 ${nodeKey} 失败:`, error);
|
||||
// 加载失败时停止展开
|
||||
console.error(`Failed to load node ${nodeKey} during auto expand:`, error);
|
||||
// Stop expanding on load failure
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置展开的节点
|
||||
// Set expanded nodes
|
||||
setExpandedKeys(keysToExpand);
|
||||
|
||||
// 选中最后一个节点(目标文件夹)
|
||||
// Select last node (target folder)
|
||||
const targetKey = autoExpandPath[autoExpandPath.length - 1]?.id;
|
||||
if (targetKey) {
|
||||
console.log(`自动展开:选中目标节点 ${targetKey}`);
|
||||
// 延迟选中,确保展开动画完成
|
||||
console.log(`Auto expand: Select target node ${targetKey}`);
|
||||
// Delay selection to ensure expand animation completes
|
||||
setTimeout(() => {
|
||||
if (onSelect) {
|
||||
onSelect([targetKey], {
|
||||
@@ -460,21 +460,21 @@ const FolderTree: FC<FolderTreeProps> = ({
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('自动展开路径失败:', error);
|
||||
console.error('Auto expand path failed:', error);
|
||||
} finally {
|
||||
// 延迟重置标志,确保展开过程完全完成
|
||||
// Delay reset flag to ensure expand process is fully complete
|
||||
setTimeout(() => {
|
||||
setAutoExpandInProgress(false);
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
// 延迟执行,确保树数据已经加载完成
|
||||
// Delay execution to ensure tree data is loaded
|
||||
const timer = setTimeout(expandToPath, 300);
|
||||
return () => clearTimeout(timer);
|
||||
}, [autoExpandPath, treeData.length, knowledgeBaseId, onSelect, autoExpandInProgress]);
|
||||
|
||||
// 处理展开事件
|
||||
// Handle expand event
|
||||
const handleExpand: TreeProps['onExpand'] = (expandedKeys, info) => {
|
||||
setExpandedKeys(expandedKeys);
|
||||
if (onExpand) {
|
||||
@@ -482,7 +482,7 @@ const FolderTree: FC<FolderTreeProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
// 处理选择事件,计算并传递路径
|
||||
// Handle select event, calculate and pass path
|
||||
const handleSelect: TreeProps['onSelect'] = (selectedKeys, info) => {
|
||||
if (selectedKeys.length > 0) {
|
||||
const path = findNodePath(treeData, selectedKeys[0]);
|
||||
@@ -493,7 +493,7 @@ const FolderTree: FC<FolderTreeProps> = ({
|
||||
onFolderPathChange([]);
|
||||
}
|
||||
|
||||
// 调用原始的 onSelect 回调
|
||||
// Call original onSelect callback
|
||||
if (onSelect) {
|
||||
onSelect(selectedKeys, info);
|
||||
}
|
||||
@@ -503,7 +503,7 @@ const FolderTree: FC<FolderTreeProps> = ({
|
||||
|
||||
return (
|
||||
<DirectoryTree
|
||||
key={refreshKey} // 添加key确保refreshKey变化时重新渲染整个组件
|
||||
key={refreshKey} // Add key to ensure component re-renders when refreshKey changes
|
||||
multiple={multiple}
|
||||
className={className}
|
||||
style={style}
|
||||
|
||||
@@ -130,7 +130,7 @@ const InsertModal = forwardRef<InsertModalRef, InsertModalProps>(({ onInsert, on
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('操作失败:', error);
|
||||
console.error('Operation failed:', error);
|
||||
const errorMsg = isEditMode
|
||||
? (t('knowledgeBase.updateFailed') || '更新失败')
|
||||
: (t('knowledgeBase.insertFailed') || '插入失败');
|
||||
|
||||
@@ -9,7 +9,7 @@ import pointer from '@/assets/images/userMemory/pointer.svg'
|
||||
import empty from '@/assets/images/userMemory/empty.svg'
|
||||
import Empty from '@/components/Empty'
|
||||
|
||||
// 知识图谱数据类型定义
|
||||
// Knowledge graph data type definitions
|
||||
export interface KnowledgeNode {
|
||||
id: string
|
||||
entity_name: string
|
||||
@@ -17,7 +17,7 @@ export interface KnowledgeNode {
|
||||
description: string
|
||||
pagerank: number
|
||||
source_id: string[]
|
||||
// ECharts 需要的属性
|
||||
// Properties required by ECharts
|
||||
name: string
|
||||
category: number
|
||||
symbolSize: number
|
||||
@@ -35,7 +35,7 @@ export interface KnowledgeEdge {
|
||||
source_id: string[]
|
||||
source: string
|
||||
target: string
|
||||
// ECharts 需要的属性
|
||||
// Properties required by ECharts
|
||||
value: number
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ const operations = [
|
||||
{ name: 'zoom', icon: zoom },
|
||||
]
|
||||
|
||||
// 预定义的颜色调色板
|
||||
// Predefined color palette
|
||||
const colorPalette = [
|
||||
'#155EEF', '#4DA8FF', '#9C6FFF', '#8BAEF7', '#369F21',
|
||||
'#FF5D34', '#FF8A4C', '#FFB048', '#E74C3C', '#9B59B6',
|
||||
@@ -73,7 +73,7 @@ const colorPalette = [
|
||||
'#8E44AD', '#2980B9', '#16A085', '#F1C40F', '#E67E22'
|
||||
]
|
||||
|
||||
// 动态生成实体类型颜色映射
|
||||
// Dynamically generate entity type color mapping
|
||||
const generateEntityTypeColors = (entityTypes: string[]): Record<string, string> => {
|
||||
const colorMap: Record<string, string> = {}
|
||||
entityTypes.forEach((type, index) => {
|
||||
@@ -93,12 +93,12 @@ const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
|
||||
const [selectedNode, setSelectedNode] = useState<KnowledgeNode | null>(null)
|
||||
const [entityTypeColors, setEntityTypeColors] = useState<Record<string, string>>({})
|
||||
|
||||
// 弹框拖动相关状态
|
||||
// Modal drag-related state
|
||||
const [modalPosition, setModalPosition] = useState({ x: 20, y: 20 })
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
|
||||
|
||||
// 拖动处理函数
|
||||
// Drag handling functions
|
||||
const handleMouseDown = useCallback((e: React.MouseEvent) => {
|
||||
setIsDragging(true)
|
||||
setDragStart({
|
||||
@@ -113,7 +113,7 @@ const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
|
||||
const newX = e.clientX - dragStart.x
|
||||
const newY = e.clientY - dragStart.y
|
||||
|
||||
// 限制拖动范围,确保弹框不会超出容器
|
||||
// Limit drag range to ensure modal doesn't exceed container bounds
|
||||
const container = chartRef.current?.getEchartsInstance().getDom().parentElement
|
||||
if (container && modalRef.current) {
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
@@ -133,7 +133,7 @@ const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
|
||||
setIsDragging(false)
|
||||
}, [])
|
||||
|
||||
// 添加全局鼠标事件监听
|
||||
// Add global mouse event listeners
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
document.addEventListener('mousemove', handleMouseMove)
|
||||
@@ -145,12 +145,12 @@ const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
|
||||
}
|
||||
}, [isDragging, handleMouseMove, handleMouseUp])
|
||||
|
||||
// 关闭弹框
|
||||
// Close modal
|
||||
const handleCloseModal = useCallback(() => {
|
||||
setSelectedNode(null)
|
||||
}, [])
|
||||
|
||||
// 处理知识图谱数据
|
||||
// Process knowledge graph data
|
||||
const processGraphData = useCallback(() => {
|
||||
if (!data?.graph) {
|
||||
setNodes([])
|
||||
@@ -164,31 +164,31 @@ const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
|
||||
const processedNodes: KnowledgeNode[] = []
|
||||
const processedEdges: KnowledgeEdge[] = []
|
||||
|
||||
// 获取所有实体类型
|
||||
// Get all entity types
|
||||
const entityTypes = [...new Set(rawNodes.map(node => node.entity_type))]
|
||||
const categoryMap = entityTypes.reduce((acc, type, index) => {
|
||||
acc[type] = index
|
||||
return acc
|
||||
}, {} as Record<string, number>)
|
||||
|
||||
// 动态生成实体类型颜色映射
|
||||
// Dynamically generate entity type color mapping
|
||||
const dynamicEntityTypeColors = generateEntityTypeColors(entityTypes)
|
||||
setEntityTypeColors(dynamicEntityTypeColors)
|
||||
|
||||
// 计算每个节点的连接数
|
||||
// Calculate connection count for each node
|
||||
const connectionCount: Record<string, number> = {}
|
||||
rawEdges.forEach(edge => {
|
||||
// 使用 src_id 和 tgt_id 计算连接数
|
||||
// Use src_id and tgt_id to calculate connection count
|
||||
connectionCount[edge.src_id] = (connectionCount[edge.src_id] || 0) + 1
|
||||
connectionCount[edge.tgt_id] = (connectionCount[edge.tgt_id] || 0) + 1
|
||||
})
|
||||
|
||||
// 处理节点数据
|
||||
// Process node data
|
||||
rawNodes.forEach(node => {
|
||||
const connections = connectionCount[node.id] || 0
|
||||
const categoryIndex = categoryMap[node.entity_type] || 0
|
||||
|
||||
// 根据 pagerank 和连接数计算节点大小
|
||||
// Calculate node size based on pagerank and connection count
|
||||
let symbolSize = Math.max(10, Math.min(50, node.pagerank * 200 + connections * 2))
|
||||
|
||||
processedNodes.push({
|
||||
@@ -202,19 +202,19 @@ const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
|
||||
})
|
||||
})
|
||||
|
||||
// 处理边数据
|
||||
// Process edge data
|
||||
rawEdges.forEach(edge => {
|
||||
// 注意:根据数据结构,source 和 target 字段可能与 src_id 和 tgt_id 相反
|
||||
// 我们使用 src_id 和 tgt_id 作为正确的连接关系
|
||||
// Note: Based on data structure, source and target fields may be opposite to src_id and tgt_id
|
||||
// We use src_id and tgt_id as the correct connection relationship
|
||||
processedEdges.push({
|
||||
...edge, // 保留所有原始字段
|
||||
source: edge.src_id, // 使用 src_id 作为源节点
|
||||
target: edge.tgt_id, // 使用 tgt_id 作为目标节点
|
||||
...edge, // Keep all original fields
|
||||
source: edge.src_id, // Use src_id as source node
|
||||
target: edge.tgt_id, // Use tgt_id as target node
|
||||
value: edge.weight || 1
|
||||
})
|
||||
})
|
||||
|
||||
// 验证节点ID和边的连接
|
||||
// Verify node IDs and edge connections
|
||||
const nodeIds = new Set(processedNodes.map(n => n.id))
|
||||
const validEdges = processedEdges.filter(edge => {
|
||||
const sourceExists = nodeIds.has(edge.source)
|
||||
@@ -225,18 +225,18 @@ const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
|
||||
return sourceExists && targetExists
|
||||
})
|
||||
|
||||
// 调试信息
|
||||
// Debug information
|
||||
console.log('Total nodes:', processedNodes.length)
|
||||
console.log('Total edges:', processedEdges.length)
|
||||
console.log('Valid edges:', validEdges.length)
|
||||
console.log('Node IDs:', Array.from(nodeIds).slice(0, 5))
|
||||
console.log('Edge sample:', validEdges.slice(0, 3))
|
||||
|
||||
// 设置分类
|
||||
// Set categories
|
||||
const processedCategories = entityTypes.map(type => ({ name: type }))
|
||||
|
||||
setNodes(processedNodes)
|
||||
setLinks(validEdges) // 只使用有效的边
|
||||
setLinks(validEdges) // Only use valid edges
|
||||
setCategories(processedCategories)
|
||||
}, [data])
|
||||
|
||||
@@ -334,7 +334,7 @@ const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
|
||||
lineStyle: {
|
||||
color: '#5B6167',
|
||||
curveness: 0.3,
|
||||
width: 2, // 固定线宽,避免函数问题
|
||||
width: 2, // Fixed line width to avoid function issues
|
||||
opacity: 0.8
|
||||
},
|
||||
force: {
|
||||
@@ -376,7 +376,7 @@ const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 实体详情弹框 */}
|
||||
{/* Entity details modal */}
|
||||
{selectedNode && (
|
||||
<div
|
||||
ref={modalRef}
|
||||
@@ -387,7 +387,7 @@ const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
|
||||
cursor: isDragging ? 'grabbing' : 'grab'
|
||||
}}
|
||||
>
|
||||
{/* 弹框头部 - 可拖动区域 */}
|
||||
{/* Modal header - draggable area */}
|
||||
<div
|
||||
className="rb:flex rb:items-center rb:justify-between rb:mb-3 rb:pb-2 rb:border-b rb:border-[#EBEBEB] rb:cursor-grab"
|
||||
onMouseDown={handleMouseDown}
|
||||
@@ -404,7 +404,7 @@ const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 弹框内容 */}
|
||||
{/* Modal content */}
|
||||
<div>
|
||||
<div className="rb:font-medium rb:mb-4">
|
||||
<div className="rb:text-[16px] rb:mb-2">{selectedNode.entity_name}</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ import { type KnowledgeBase } from '../types';
|
||||
import Empty from '@/components/Empty';
|
||||
interface KnowledgeGraphCardProps {
|
||||
knowledgeBase?: KnowledgeBase;
|
||||
onRebuildGraph?: () => void; // 添加重建图谱的回调函数
|
||||
onRebuildGraph?: () => void; // Callback function to rebuild graph
|
||||
}
|
||||
|
||||
const KnowledgeGraphCard: React.FC<KnowledgeGraphCardProps> = ({ knowledgeBase, onRebuildGraph }) => {
|
||||
@@ -23,7 +23,7 @@ const KnowledgeGraphCard: React.FC<KnowledgeGraphCardProps> = ({ knowledgeBase,
|
||||
const [data, setData] = useState<KnowledgeGraphResponse | undefined>()
|
||||
const [loading, setLoading] = useState(true)
|
||||
const handleRebuildGraph = () => {
|
||||
// 调用父组件传递的回调函数来打开CreateModal并传递重建标识
|
||||
// Call parent component's callback to open CreateModal with rebuild flag
|
||||
if (onRebuildGraph) {
|
||||
onRebuildGraph();
|
||||
}
|
||||
@@ -38,15 +38,15 @@ const KnowledgeGraphCard: React.FC<KnowledgeGraphCardProps> = ({ knowledgeBase,
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await getKnowledgeGraph(knowledgeBase?.id)
|
||||
// 判断 res.graph 是否为空对象或不存在
|
||||
// Check if res.graph is empty object or doesn't exist
|
||||
const graphResponse = res as KnowledgeGraphResponse;
|
||||
if (!graphResponse || !graphResponse.graph || Object.keys(graphResponse.graph).length === 0) {
|
||||
setData(undefined) // 设置为 undefined 以显示 empty 状态
|
||||
setData(undefined) // Set to undefined to show empty state
|
||||
} else {
|
||||
setData(graphResponse)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取知识图谱数据失败:', error)
|
||||
console.error('Failed to fetch knowledge graph data:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ const RecallTest = forwardRef<RecallTestDrawerRef>(({},ref) => {
|
||||
{ label: t('knowledgeBase.vector'), value: false },
|
||||
]);
|
||||
|
||||
// 获取检索模式选项
|
||||
// Get retrieval mode options
|
||||
useEffect(() => {
|
||||
fetchRetrievalModeOptions();
|
||||
}, []);
|
||||
@@ -36,9 +36,9 @@ const RecallTest = forwardRef<RecallTestDrawerRef>(({},ref) => {
|
||||
try {
|
||||
const response = await getRetrievalModeType();
|
||||
if (response && Array.isArray(response)) {
|
||||
// 将 API 返回的数据转换为选项格式
|
||||
// Convert API response to option format
|
||||
const options = response.map((item: any) => {
|
||||
// 支持多种数据格式
|
||||
// Support multiple data formats
|
||||
let label = t(`knowledgeBase.${item}`) + ' ' + t(`knowledgeBase.retrieve`);
|
||||
let value = item;
|
||||
|
||||
@@ -50,8 +50,8 @@ const RecallTest = forwardRef<RecallTestDrawerRef>(({},ref) => {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取检索模式选项失败:', error);
|
||||
// 保持默认选项
|
||||
console.error('Failed to fetch retrieval mode options:', error);
|
||||
// Keep default options
|
||||
}
|
||||
};
|
||||
|
||||
@@ -60,8 +60,8 @@ const RecallTest = forwardRef<RecallTestDrawerRef>(({},ref) => {
|
||||
setKnowledgeBaseId(kbId || '');
|
||||
form.resetFields();
|
||||
setData([]);
|
||||
setRetrieveType('hybrid'); // 重置为默认值
|
||||
// 确保表单字段也设置为默认值
|
||||
setRetrieveType('hybrid'); // Reset to default value
|
||||
// Ensure form field is also set to default value
|
||||
form.setFieldsValue({ retrieve_type: 'hybrid' });
|
||||
}
|
||||
const fetchData = (params: RecallTestParams) => {
|
||||
@@ -91,10 +91,10 @@ const RecallTest = forwardRef<RecallTestDrawerRef>(({},ref) => {
|
||||
console.log('RecallTest - params:', params);
|
||||
fetchData(params);
|
||||
}).catch((error) => {
|
||||
console.error('表单验证失败:', error);
|
||||
console.error('Form validation failed:', error);
|
||||
});
|
||||
}
|
||||
// 暴露给父组件的方法
|
||||
// Expose methods to parent component
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
@@ -134,7 +134,7 @@ const RecallTest = forwardRef<RecallTestDrawerRef>(({},ref) => {
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 当 retrieve_type = semantic 或 hybrid 时显示 */}
|
||||
{/* Show when retrieve_type = semantic or hybrid */}
|
||||
{(retrieveType === 'semantic' || retrieveType === 'hybrid') && (
|
||||
<Form.Item name="similarity_threshold" label={t('knowledgeBase.similarityThreshold')}>
|
||||
<Select
|
||||
@@ -155,7 +155,7 @@ const RecallTest = forwardRef<RecallTestDrawerRef>(({},ref) => {
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
{/* 当 retrieve_type = participle 或 hybrid 时显示 */}
|
||||
{/* Show when retrieve_type = participle or hybrid */}
|
||||
{(retrieveType === 'participle' || retrieveType === 'hybrid') && (
|
||||
<Form.Item name="vector_similarity_weight" label={t('knowledgeBase.semanticSimilarity')}>
|
||||
<Select
|
||||
|
||||
@@ -12,7 +12,7 @@ const RecallTestDrawer = forwardRef<RecallTestDrawerRef>(({},ref) => {
|
||||
const pendingKbIdRef = useRef<string | undefined>(undefined);
|
||||
const shouldCallHandleOpenRef = useRef(false);
|
||||
|
||||
// 调用 RecallTest 的 handleOpen 方法
|
||||
// Call RecallTest's handleOpen method
|
||||
const callRecallTestHandleOpen = useCallback(() => {
|
||||
if (recallTestRef.current && shouldCallHandleOpenRef.current) {
|
||||
recallTestRef.current.handleOpen(pendingKbIdRef.current);
|
||||
@@ -26,14 +26,14 @@ const RecallTestDrawer = forwardRef<RecallTestDrawerRef>(({},ref) => {
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
// 当 Drawer 打开时,尝试调用 handleOpen
|
||||
// When Drawer opens, try to call handleOpen
|
||||
useLayoutEffect(() => {
|
||||
if (open) {
|
||||
callRecallTestHandleOpen();
|
||||
}
|
||||
}, [open, callRecallTestHandleOpen]);
|
||||
|
||||
// 使用回调 ref 确保在组件挂载后立即调用
|
||||
// Use callback ref to ensure immediate call after component mount
|
||||
const setRecallTestRef = useCallback((node: any) => {
|
||||
recallTestRef.current = node;
|
||||
if (open && shouldCallHandleOpenRef.current) {
|
||||
@@ -41,7 +41,7 @@ const RecallTestDrawer = forwardRef<RecallTestDrawerRef>(({},ref) => {
|
||||
}
|
||||
}, [open, callRecallTestHandleOpen]);
|
||||
|
||||
// 暴露给父组件的方法
|
||||
// Expose methods to parent component
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* @Description: 滚动列表
|
||||
/**
|
||||
* @Description: Scroll List
|
||||
* @Version: 0.0.1
|
||||
* @Author: yujiangping
|
||||
* @Date: 2025-11-18 16:19:58
|
||||
@@ -22,9 +22,9 @@ interface RecallTestResultProps {
|
||||
loadMore?: () => void;
|
||||
loading?: boolean;
|
||||
scrollableTarget?: string;
|
||||
editable?: boolean; // 是否可编辑
|
||||
onItemClick?: (item: RecallTestData, index: number) => void; // 点击项的回调
|
||||
parserMode?: number; // 解析模式,1 表示 QA 格式
|
||||
editable?: boolean; // Whether editable
|
||||
onItemClick?: (item: RecallTestData, index: number) => void; // Click item callback
|
||||
parserMode?: number; // Parser mode, 1 means QA format
|
||||
}
|
||||
|
||||
const RecallTestResult = ({
|
||||
@@ -40,7 +40,7 @@ const RecallTestResult = ({
|
||||
}: RecallTestResultProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 解析 QA 格式内容
|
||||
// Parse QA format content
|
||||
const parseQAContent = (content: string) => {
|
||||
if (!content || parserMode !== 1) return null;
|
||||
|
||||
@@ -56,25 +56,25 @@ const RecallTestResult = ({
|
||||
return null;
|
||||
};
|
||||
|
||||
// 格式化 QA 内容为显示格式
|
||||
// Format QA content for display
|
||||
const formatQAContent = (question: string, answer: string) => {
|
||||
return `**${t('knowledgeBase.question')}:** ${question}\n**${t('knowledgeBase.answer')}:** ${answer}`;
|
||||
};
|
||||
|
||||
const handleItemClick = (e: React.MouseEvent, item: RecallTestData, index: number) => {
|
||||
// 检查点击的是否是图片或图片相关元素
|
||||
// Check if the click is on an image or image-related element
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
// 检查是否点击了图片本身、图片的容器、预览层、关闭按钮或 SVG 图标
|
||||
// Check if clicked on image itself, image container, preview layer, close button or SVG icon
|
||||
if (
|
||||
target.tagName === 'IMG' ||
|
||||
target.tagName === 'SVG' || // SVG 图标
|
||||
target.tagName === 'PATH' || // SVG 路径
|
||||
target.tagName === 'SVG' || // SVG icon
|
||||
target.tagName === 'PATH' || // SVG path
|
||||
target.closest('.ant-image') ||
|
||||
target.closest('.ant-image-preview') ||
|
||||
target.closest('.ant-image-preview-wrap') ||
|
||||
target.closest('.ant-image-preview-operations') ||
|
||||
target.closest('.anticon') || // Ant Design 图标
|
||||
target.closest('.anticon') || // Ant Design icon
|
||||
target.classList.contains('ant-image-img') ||
|
||||
target.classList.contains('ant-image-mask') ||
|
||||
target.classList.contains('ant-image-preview-close') ||
|
||||
@@ -88,7 +88,7 @@ const RecallTestResult = ({
|
||||
}
|
||||
};
|
||||
|
||||
// 根据分数获取颜色类名
|
||||
// Get color class based on score
|
||||
const getScoreColorClass = (score: number): string => {
|
||||
const percentage = score * 100;
|
||||
if (percentage >= 90) {
|
||||
@@ -177,7 +177,7 @@ const RecallTestResult = ({
|
||||
</div>
|
||||
);
|
||||
|
||||
// 如果提供了 loadMore 和 hasMore,使用 InfiniteScroll
|
||||
// If loadMore and hasMore are provided, use InfiniteScroll
|
||||
if (loadMore && hasMore !== undefined) {
|
||||
return (
|
||||
<div className='rb:flex rb:h-full rb:flex-col'>
|
||||
@@ -200,7 +200,7 @@ const RecallTestResult = ({
|
||||
);
|
||||
}
|
||||
|
||||
// 否则使用普通渲染
|
||||
// Otherwise use normal rendering
|
||||
return (
|
||||
<div className='rb:flex rb:flex-col'>
|
||||
<div className='rb:flex rb:items-center rb:justify-start rb:gap-2'>
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
* @Author: yujiangping
|
||||
* @Date: 2025-11-10 18:52:55
|
||||
* @LastEditors: yujiangping
|
||||
* @LastEditTime: 2025-11-29 12:29:31
|
||||
* @LastEditTime: 2026-02-03 17:08:00
|
||||
*/
|
||||
import { forwardRef, useImperativeHandle, useState, useRef } from 'react';
|
||||
import { Switch } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { message } from 'antd';
|
||||
import type { ShareModalRef, ShareModalRefProps, KnowledgeBase} from '@/views/KnowledgeBase/types';
|
||||
import type { ShareModalRef, ShareModalRefProps, KnowledgeBase, SpaceItem} from '@/views/KnowledgeBase/types';
|
||||
import RbModal from '@/components/RbModal'
|
||||
// import betchControlIcon from '@/assets/images/knowledgeBase/betch-control.png';
|
||||
import kbIcon from '@/assets/images/knowledgeBase/knowledge-management.png';
|
||||
@@ -33,7 +33,7 @@ const ShareModal = forwardRef<ShareModalRef,ShareModalRefProps>(({ handleShare:
|
||||
const [knowledgeBase, setKnowledgeBase] = useState<KnowledgeBase | null>(null);
|
||||
const [spaceList, setSpaceList] = useState<SpaceItem[]>([]);
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
// Close modal and reset state
|
||||
const handleClose = () => {
|
||||
setCurIndex(9999);
|
||||
setLoading(false)
|
||||
@@ -66,11 +66,11 @@ const ShareModal = forwardRef<ShareModalRef,ShareModalRefProps>(({ handleShare:
|
||||
console.log('Workspace IDs:', workspaceIds);
|
||||
shareSpaceModalRef?.current?.handleOpen(kbId,knowledgeBase,workspaceIds);
|
||||
|
||||
// 分享后关闭弹窗
|
||||
// Close modal after sharing
|
||||
handleClose();
|
||||
}
|
||||
const handleChange = (checked: boolean, item: any) => {
|
||||
// 打开/关闭分享出去的数据库
|
||||
// Toggle shared knowledge base status
|
||||
console.log('Switch changed:', checked, item);
|
||||
updateKnowledgeBase(item.target_kb?.id, {
|
||||
status: checked ? 1 : 2
|
||||
@@ -82,7 +82,7 @@ const ShareModal = forwardRef<ShareModalRef,ShareModalRefProps>(({ handleShare:
|
||||
})
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
// Expose methods to parent component
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose,
|
||||
|
||||
@@ -30,7 +30,7 @@ const ShareModal = forwardRef<ShareModalRef,ShareModalRefProps>(({ handleShare:
|
||||
const [knowledgeBase, setKnowledgeBase] = useState<KnowledgeBase | null>(null);
|
||||
const [spaceList, setSpaceList] = useState<SpaceItem[]>([]);
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
// Close modal and reset state
|
||||
const handleClose = () => {
|
||||
setCurIndex(-1);
|
||||
setLoading(false)
|
||||
@@ -51,10 +51,10 @@ const ShareModal = forwardRef<ShareModalRef,ShareModalRefProps>(({ handleShare:
|
||||
}
|
||||
const handleShare = async() => {
|
||||
|
||||
// 获取所有 checked 为 true 的数据
|
||||
// Get all data with checked = true
|
||||
const checkedItems = spaceList.filter(item => item.is_active);
|
||||
debugger
|
||||
// 获取当前选中的项(curIndex 对应的数据)
|
||||
// Get currently selected item (corresponding to curIndex)
|
||||
const selectedItem = curIndex !== -1 ? spaceList[curIndex] : null;
|
||||
if(!selectedItem){
|
||||
messageApi.error(t('knowledgeBase.selectSpace'));
|
||||
@@ -70,13 +70,13 @@ const ShareModal = forwardRef<ShareModalRef,ShareModalRefProps>(({ handleShare:
|
||||
}else{
|
||||
messageApi.error(t('knowledgeBase.shareFailed'));
|
||||
}
|
||||
// 调用父组件传递的回调函数,传递选中的数据
|
||||
// Call parent component's callback function with selected data
|
||||
onShare?.({
|
||||
checkedItems,
|
||||
selectedItem
|
||||
});
|
||||
|
||||
// 分享后关闭弹窗
|
||||
// Close modal after sharing
|
||||
handleClose();
|
||||
}
|
||||
const handleClick = (index: number, checked: boolean) => {
|
||||
@@ -84,7 +84,7 @@ const ShareModal = forwardRef<ShareModalRef,ShareModalRefProps>(({ handleShare:
|
||||
setCurIndex(index);
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
// Expose methods to parent component
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose,
|
||||
|
||||
@@ -36,11 +36,11 @@ const Datasets: FC = () => {
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div>加载中...</div>;
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (!knowledgeBase) {
|
||||
return <div>知识库不存在</div>;
|
||||
return <div>Knowledge base not found</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -50,7 +50,7 @@ const Datasets: FC = () => {
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={handleBack}
|
||||
>
|
||||
返回
|
||||
{t('common.back')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,7 @@ const Datasets: FC = () => {
|
||||
|
||||
<div className="rb:bg-white rb:p-4 rb:rounded">
|
||||
<h2 className="rb:text-lg rb:font-semibold rb:mb-4">{t('knowledgeBase.datasets')}</h2>
|
||||
{/* TODO: 添加数据集列表 */}
|
||||
{/* TODO: Add dataset list */}
|
||||
<div>{t('knowledgeBase.noDataSets')}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -388,7 +388,7 @@ const KnowledgeBaseManagement: FC = () => {
|
||||
});
|
||||
},
|
||||
onCancel: () => {
|
||||
console.log('取消删除');
|
||||
console.log('Cancel delete');
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -51,7 +51,6 @@ const ModelImplement: FC<ModelImplementProps> = ({ type, value, onChange }) => {
|
||||
const handleDelete = (vo: any) => {
|
||||
modal.confirm({
|
||||
title: t('common.confirmDeleteDesc', { name: [vo.model_name, vo.api_key].join(' / ') }),
|
||||
content: t('application.apiKeyDeleteContent'),
|
||||
okText: t('common.delete'),
|
||||
cancelText: t('common.cancel'),
|
||||
okType: 'danger',
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 14:10:42
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 14:10:42
|
||||
*/
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App, Transfer, type TransferProps, Flex } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -12,23 +18,37 @@ import Tag from '@/components/Tag';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
/**
|
||||
* Props for OntologyClassExtractModal component
|
||||
*/
|
||||
interface OntologyClassExtractModalProps {
|
||||
/** Callback function to refresh parent list after extraction */
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal component for extracting ontology classes using LLM
|
||||
* Two-step process: 1) Extract classes from scenario 2) Select and confirm classes to add
|
||||
*/
|
||||
const OntologyClassExtractModal = forwardRef<OntologyClassExtractModalRef, OntologyClassExtractModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<OntologyClassExtractModalData>();
|
||||
|
||||
// State
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [data, setData] = useState<OntologyClassData | null>(null)
|
||||
const [extractData, setExtractData] = useState<ExtractData | null>(null)
|
||||
const [targetKeys, setTargetKeys] = useState<TransferProps['targetKeys']>([]);
|
||||
const [selectedKeys, setSelectedKeys] = useState<TransferProps['selectedKeys']>([]);
|
||||
|
||||
/**
|
||||
* Close modal and reset all state
|
||||
*/
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
@@ -37,11 +57,19 @@ const OntologyClassExtractModal = forwardRef<OntologyClassExtractModalRef, Ontol
|
||||
setExtractData(null)
|
||||
};
|
||||
|
||||
/**
|
||||
* Open modal with scene data
|
||||
* @param vo - Ontology class data containing scene information
|
||||
*/
|
||||
const handleOpen = (vo: OntologyClassData) => {
|
||||
form.resetFields();
|
||||
setVisible(true);
|
||||
setData(vo)
|
||||
};
|
||||
|
||||
/**
|
||||
* Execute LLM extraction to get class suggestions
|
||||
*/
|
||||
const handleSave = () => {
|
||||
if (!data?.scene_id) return;
|
||||
form
|
||||
@@ -67,6 +95,10 @@ const OntologyClassExtractModal = forwardRef<OntologyClassExtractModalRef, Ontol
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm and create selected classes
|
||||
* First click runs extraction, second click creates classes
|
||||
*/
|
||||
const handleConfirm = () => {
|
||||
if (!extractData) {
|
||||
handleSave()
|
||||
@@ -90,11 +122,19 @@ const OntologyClassExtractModal = forwardRef<OntologyClassExtractModalRef, Ontol
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle transfer component target keys change
|
||||
* @param nextTargetKeys - New target keys after transfer
|
||||
*/
|
||||
const onChange: TransferProps['onChange'] = (nextTargetKeys) => {
|
||||
setTargetKeys(nextTargetKeys.filter(Boolean));
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle transfer component selection change
|
||||
* @param sourceSelectedKeys - Selected keys in source list
|
||||
* @param targetSelectedKeys - Selected keys in target list
|
||||
*/
|
||||
const onSelectChange: TransferProps['onSelectChange'] = (
|
||||
sourceSelectedKeys,
|
||||
targetSelectedKeys,
|
||||
@@ -102,6 +142,9 @@ const OntologyClassExtractModal = forwardRef<OntologyClassExtractModalRef, Ontol
|
||||
setSelectedKeys([...sourceSelectedKeys, ...targetSelectedKeys].filter(Boolean));
|
||||
};
|
||||
|
||||
/**
|
||||
* Expose methods to parent component via ref
|
||||
*/
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 14:10:39
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 14:10:39
|
||||
*/
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -8,31 +14,53 @@ import { createOntologyClass } from '@/api/ontology'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
/**
|
||||
* Props for OntologyClassModal component
|
||||
*/
|
||||
interface OntologyClassModalProps {
|
||||
/** Callback function to refresh parent list after save */
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal component for adding new ontology classes
|
||||
* Provides form interface for class name and description
|
||||
*/
|
||||
const OntologyClassModal = forwardRef<OntologyClassModalRef, OntologyClassModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<AddClassItem>();
|
||||
|
||||
// State
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [scene_id, setSceneId] = useState<string | null>(null)
|
||||
|
||||
/**
|
||||
* Close modal and reset form state
|
||||
*/
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
/**
|
||||
* Open modal for adding a new class
|
||||
* @param scene_id - Target scene identifier
|
||||
*/
|
||||
const handleOpen = (scene_id: string) => {
|
||||
form.resetFields();
|
||||
setVisible(true);
|
||||
setSceneId(scene_id)
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate and submit form data to create new class
|
||||
*/
|
||||
const handleSave = () => {
|
||||
if (!scene_id) return;
|
||||
form
|
||||
@@ -54,6 +82,9 @@ const OntologyClassModal = forwardRef<OntologyClassModalRef, OntologyClassModalP
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose methods to parent component via ref
|
||||
*/
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
144
web/src/views/Ontology/components/OntologyExportModal.tsx
Normal file
144
web/src/views/Ontology/components/OntologyExportModal.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 14:10:46
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 14:10:46
|
||||
*/
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, App, Select, type SelectProps } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { OntologyExportModalData, OntologyExportModalRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { ontologyExport, getOntologyScenesUrl } from '@/api/ontology'
|
||||
import CustomSelect from '@/components/CustomSelect';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
/**
|
||||
* Props for OntologyExportModal component
|
||||
*/
|
||||
interface OntologyExportModalProps {
|
||||
/** Callback function to refresh parent list after export */
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal component for exporting ontology scenes
|
||||
* Supports RDF/XML (.owl) and Turtle (.ttl) formats
|
||||
*/
|
||||
const OntologyExportModal = forwardRef<OntologyExportModalRef, OntologyExportModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [form] = Form.useForm<OntologyExportModalData>();
|
||||
|
||||
// State
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [fileName, setFileName] = useState('')
|
||||
|
||||
/**
|
||||
* Close modal and reset form state
|
||||
*/
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the export modal
|
||||
*/
|
||||
const handleOpen = () => {
|
||||
form.resetFields();
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle scene selection change to set export filename
|
||||
* @param _value - Selected scene ID
|
||||
* @param option - Selected option containing scene name
|
||||
*/
|
||||
const handleChange: SelectProps['onChange'] = (_value, option) => {
|
||||
const name = Array.isArray(option) ? option[0]?.children : option?.children;
|
||||
setFileName(String(name || ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and submit form data to export ontology
|
||||
* Downloads file with appropriate extension based on format
|
||||
*/
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
setLoading(true)
|
||||
ontologyExport(values, `${fileName}.${values.format === 'rdfxml' ?'owl' : 'ttl'}`, () => {
|
||||
message.success(t('common.exportSuccess'));
|
||||
handleClose();
|
||||
refresh();
|
||||
setLoading(false)
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose methods to parent component via ref
|
||||
*/
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('ontology.export')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.export')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{ format: 'rdfxml' }}
|
||||
>
|
||||
<FormItem
|
||||
name="scene_id"
|
||||
label={t('ontology.scene_id')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<CustomSelect
|
||||
url={getOntologyScenesUrl}
|
||||
params={{ page: 1, pagesize: 100 }}
|
||||
valueKey="scene_id"
|
||||
labelKey="scene_name"
|
||||
hasAll={false}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
name="format"
|
||||
label={t('ontology.format')}
|
||||
>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={[
|
||||
{ value: 'rdfxml', label: 'RDF/XML' },
|
||||
{ value: 'turtle', label: 'Turtle' },
|
||||
]}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default OntologyExportModal;
|
||||
139
web/src/views/Ontology/components/OntologyImportModal.tsx
Normal file
139
web/src/views/Ontology/components/OntologyImportModal.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 14:10:32
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 14:10:32
|
||||
*/
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { OntologyImportModalData, OntologyImportModalRef } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { ontologyImport } from '@/api/ontology'
|
||||
import UploadFiles from '@/components/Upload/UploadFiles';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
/**
|
||||
* Props for OntologyImportModal component
|
||||
*/
|
||||
interface OntologyImportModalProps {
|
||||
/** Callback function to refresh parent list after import */
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal component for importing ontology files
|
||||
* Supports OWL, TTL, RDF, XML file formats
|
||||
*/
|
||||
const OntologyImportModal = forwardRef<OntologyImportModalRef, OntologyImportModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [form] = Form.useForm<OntologyImportModalData>();
|
||||
|
||||
// State
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
/**
|
||||
* Close modal and reset form state
|
||||
*/
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
};
|
||||
|
||||
/**
|
||||
* Open the import modal
|
||||
*/
|
||||
const handleOpen = () => {
|
||||
form.resetFields();
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate and submit form data to import ontology file
|
||||
* Creates FormData with file and scene information
|
||||
*/
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
const { scene_name, scene_description, file } = values
|
||||
console.log('values', file);
|
||||
const formData = new FormData();
|
||||
formData.append('file', file[0]);
|
||||
formData.append('scene_name', scene_name);
|
||||
if (scene_description) {
|
||||
formData.append('scene_description', scene_description);
|
||||
}
|
||||
setLoading(true)
|
||||
ontologyImport(formData)
|
||||
.then(() => {
|
||||
message.success(t('common.saveSuccess'));
|
||||
handleClose();
|
||||
refresh();
|
||||
})
|
||||
.finally(() => setLoading(false))
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose methods to parent component via ref
|
||||
*/
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={t('ontology.import')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.create')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<FormItem
|
||||
name="scene_name"
|
||||
label={t('ontology.scene_name')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name="scene_description"
|
||||
label={t('ontology.scene_description')}
|
||||
>
|
||||
<Input.TextArea placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
name="file"
|
||||
label={t('ontology.file')}
|
||||
rules={[{ required: true, message: t('common.pleaseSelect') }]}
|
||||
>
|
||||
<UploadFiles
|
||||
isCanDrag={true}
|
||||
fileType={['owl', 'ttl', 'rdf', 'xml']}
|
||||
isAutoUpload={false}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default OntologyImportModal;
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 14:10:28
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 14:10:28
|
||||
*/
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, App } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -8,20 +14,34 @@ import { createOntologyScene, updateOntologyScene } from '@/api/ontology'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
/**
|
||||
* Props for OntologyModal component
|
||||
*/
|
||||
interface OntologyModalProps {
|
||||
/** Callback function to refresh parent list after save */
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal component for creating or editing ontology scenes
|
||||
* Provides form interface for scene name and description
|
||||
*/
|
||||
const OntologyModal = forwardRef<OntologyModalRef, OntologyModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [form] = Form.useForm<OntologyModalData>();
|
||||
|
||||
// State
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [editVo, setEditVo] = useState<OntologyItem | null>(null)
|
||||
const [form] = Form.useForm<OntologyModalData>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
/**
|
||||
* Close modal and reset form state
|
||||
*/
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
@@ -29,6 +49,10 @@ const OntologyModal = forwardRef<OntologyModalRef, OntologyModalProps>(({
|
||||
setEditVo(null)
|
||||
};
|
||||
|
||||
/**
|
||||
* Open modal for creating or editing
|
||||
* @param vo - Optional ontology item data for edit mode
|
||||
*/
|
||||
const handleOpen = (vo?: OntologyItem) => {
|
||||
if (vo) {
|
||||
setEditVo(vo);
|
||||
@@ -38,6 +62,11 @@ const OntologyModal = forwardRef<OntologyModalRef, OntologyModalProps>(({
|
||||
}
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate and submit form data
|
||||
* Creates new scene or updates existing one based on editVo
|
||||
*/
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
@@ -57,6 +86,9 @@ const OntologyModal = forwardRef<OntologyModalRef, OntologyModalProps>(({
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Expose methods to parent component via ref
|
||||
*/
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
}));
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 14:10:24
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 14:10:56
|
||||
*/
|
||||
import { type FC, type ReactNode } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Layout, Button } from 'antd';
|
||||
@@ -6,11 +12,23 @@ import logoutIcon from '@/assets/images/logout_hover.svg'
|
||||
|
||||
const { Header } = Layout;
|
||||
|
||||
/**
|
||||
* Props for PageHeader component
|
||||
*/
|
||||
interface ConfigHeaderProps {
|
||||
/** Page title/name */
|
||||
name?: string;
|
||||
/** Subtitle content displayed below the title */
|
||||
subTitle?: ReactNode | string;
|
||||
/** Extra content displayed on the right side */
|
||||
extra?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Page header component for ontology pages
|
||||
* Displays title, subtitle, back button and extra actions
|
||||
* @param props - Component props
|
||||
*/
|
||||
const PageHeader: FC<ConfigHeaderProps> = ({
|
||||
name,
|
||||
subTitle,
|
||||
@@ -19,6 +37,9 @@ const PageHeader: FC<ConfigHeaderProps> = ({
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
/**
|
||||
* Navigate back to previous page
|
||||
*/
|
||||
const goBack = () => {
|
||||
navigate(-1)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 14:10:15
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 14:10:15
|
||||
*/
|
||||
import { type FC, useState, useRef, type MouseEvent } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -5,29 +11,57 @@ import { Row, Col, Button, Flex, Divider, Space, App, Tooltip } from 'antd'
|
||||
|
||||
import SearchInput from '@/components/SearchInput';
|
||||
import OntologyModal from './components/OntologyModal'
|
||||
import type { OntologyModalRef, OntologyItem, Query } from './types'
|
||||
import type { OntologyModalRef, OntologyItem, Query, OntologyImportModalRef, OntologyExportModalRef } from './types'
|
||||
import RbCard from '@/components/RbCard/Card'
|
||||
import Tag from '@/components/Tag'
|
||||
import PageScrollList, { type PageScrollListRef } from '@/components/PageScrollList'
|
||||
import { getOntologyScenesUrl, deleteOntologyScene } from '@/api/ontology'
|
||||
import { formatDateTime } from '@/utils/format'
|
||||
import OntologyImportModal from './components/OntologyImportModal'
|
||||
import OntologyExportModal from './components/OntologyExportModal'
|
||||
|
||||
/**
|
||||
* Ontology management page component
|
||||
* Displays a list of ontology scenes with search, create, import, export functionality
|
||||
*/
|
||||
const Ontology: FC = () => {
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate()
|
||||
const { modal, message } = App.useApp();
|
||||
|
||||
// State
|
||||
const [query, setQuery] = useState<Query>({});
|
||||
|
||||
// Refs
|
||||
const scrollListRef = useRef<PageScrollListRef>(null)
|
||||
const entityModalRef = useRef<OntologyModalRef>(null)
|
||||
const ontologyImportModalRef = useRef<OntologyImportModalRef>(null)
|
||||
const ontologyExportModalRef = useRef<OntologyExportModalRef>(null)
|
||||
|
||||
/**
|
||||
* Open modal to create a new ontology scene
|
||||
*/
|
||||
const handleCreate = () => {
|
||||
entityModalRef.current?.handleOpen()
|
||||
}
|
||||
|
||||
/**
|
||||
* Open modal to edit an existing ontology scene
|
||||
* @param record - The ontology item to edit
|
||||
* @param e - Mouse event to prevent propagation
|
||||
*/
|
||||
const handleEdit = (record: OntologyItem, e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
entityModalRef.current?.handleOpen(record)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an ontology scene with confirmation
|
||||
* @param item - The ontology item to delete
|
||||
* @param e - Mouse event to prevent propagation
|
||||
*/
|
||||
const handleDelete = (item: OntologyItem, e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -45,9 +79,35 @@ const Ontology: FC = () => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to ontology detail page
|
||||
* @param record - The ontology item to view
|
||||
*/
|
||||
const handleJump = (record: OntologyItem) => {
|
||||
navigate(`/ontology/${record.scene_id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the ontology list
|
||||
*/
|
||||
const handleRefresh = () => {
|
||||
scrollListRef.current?.refresh()
|
||||
}
|
||||
|
||||
/**
|
||||
* Open export modal
|
||||
*/
|
||||
const handleExport = () => {
|
||||
ontologyExportModalRef.current?.handleOpen()
|
||||
}
|
||||
|
||||
/**
|
||||
* Open import modal
|
||||
*/
|
||||
const handleImport = () => {
|
||||
ontologyImportModalRef.current?.handleOpen()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -60,9 +120,17 @@ const Ontology: FC = () => {
|
||||
/>
|
||||
</Col>
|
||||
<Col span={16} className="rb:text-right">
|
||||
<Button type="primary" onClick={handleCreate}>
|
||||
+ {t('ontology.create')}
|
||||
</Button>
|
||||
<Space size={12}>
|
||||
<Button onClick={handleExport}>
|
||||
{t('ontology.export')}
|
||||
</Button>
|
||||
<Button onClick={handleImport}>
|
||||
{t('ontology.import')}
|
||||
</Button>
|
||||
<Button type="primary" onClick={handleCreate}>
|
||||
+ {t('ontology.create')}
|
||||
</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
@@ -124,7 +192,15 @@ const Ontology: FC = () => {
|
||||
|
||||
<OntologyModal
|
||||
ref={entityModalRef}
|
||||
refresh={() => scrollListRef.current?.refresh()}
|
||||
refresh={handleRefresh}
|
||||
/>
|
||||
<OntologyImportModal
|
||||
ref={ontologyImportModalRef}
|
||||
refresh={handleRefresh}
|
||||
/>
|
||||
<OntologyExportModal
|
||||
ref={ontologyExportModalRef}
|
||||
refresh={handleRefresh}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 14:10:20
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 14:10:20
|
||||
*/
|
||||
import { type FC, useEffect, useState, useRef } from 'react'
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -12,22 +18,35 @@ import SearchInput from '@/components/SearchInput';
|
||||
import OntologyClassExtractModal from '../components/OntologyClassExtractModal'
|
||||
import BodyWrapper from '@/components/Empty/BodyWrapper'
|
||||
|
||||
/**
|
||||
* Ontology detail page component
|
||||
* Displays and manages classes within a specific ontology scene
|
||||
*/
|
||||
const Detail: FC = () => {
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams()
|
||||
const { modal, message } = App.useApp()
|
||||
|
||||
// Refs
|
||||
const ontologyClassModalRef = useRef<OntologyClassModalRef>(null)
|
||||
const ontologyClassExtractModalRef = useRef<OntologyClassExtractModalRef>(null)
|
||||
|
||||
// State
|
||||
const [query, setQuery] = useState<{
|
||||
class_name?: string;
|
||||
}>({});
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [data, setData] = useState<OntologyClassData>({} as OntologyClassData)
|
||||
|
||||
// Fetch data when component mounts or dependencies change
|
||||
useEffect(() => {
|
||||
getData()
|
||||
}, [id, query])
|
||||
|
||||
/**
|
||||
* Fetch ontology class list data
|
||||
*/
|
||||
const getData = () => {
|
||||
if (!id) return;
|
||||
setLoading(true)
|
||||
@@ -42,6 +61,11 @@ const Detail: FC = () => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an ontology class with confirmation
|
||||
* @param item - The class item to delete
|
||||
*/
|
||||
const handleDelete = (item: OntologyClassItem) => {
|
||||
modal.confirm({
|
||||
title: t('common.confirmDeleteDesc', { name: item.class_name }),
|
||||
@@ -57,9 +81,17 @@ const Detail: FC = () => {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Open modal to add a new class
|
||||
*/
|
||||
const handleAdd = () => {
|
||||
ontologyClassModalRef.current?.handleOpen(data.scene_id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Open modal to extract classes using LLM
|
||||
*/
|
||||
const handleExtract = () => {
|
||||
ontologyClassExtractModalRef.current?.handleOpen(data)
|
||||
}
|
||||
|
||||
@@ -1,79 +1,214 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 14:10:10
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 14:10:10
|
||||
*/
|
||||
/**
|
||||
* Query parameters for ontology list pagination and filtering
|
||||
*/
|
||||
export interface Query {
|
||||
/** Number of items per page */
|
||||
pagesize?: number;
|
||||
/** Current page number */
|
||||
page?: number;
|
||||
/** Scene name for filtering */
|
||||
scene_name?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ontology scene item data structure
|
||||
*/
|
||||
export interface OntologyItem {
|
||||
/** Unique identifier for the scene */
|
||||
scene_id: string;
|
||||
/** Name of the ontology scene */
|
||||
scene_name: string;
|
||||
/** Description of the ontology scene */
|
||||
scene_description: string;
|
||||
/** Number of entity types in the scene */
|
||||
type_num: number;
|
||||
/** Array of entity type names */
|
||||
entity_type: string[];
|
||||
/** Associated workspace identifier */
|
||||
workspace_id: string;
|
||||
/** Creation timestamp */
|
||||
created_at: number;
|
||||
/** Last update timestamp */
|
||||
updated_at: number;
|
||||
/** Total count of classes in the scene */
|
||||
classes_count: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form data for creating/editing ontology scene
|
||||
*/
|
||||
export interface OntologyModalData {
|
||||
/** Scene name */
|
||||
scene_name: string;
|
||||
/** Scene description */
|
||||
scene_description: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ref methods exposed by OntologyModal component
|
||||
*/
|
||||
export interface OntologyModalRef {
|
||||
/**
|
||||
* Open the modal for creating or editing
|
||||
* @param data - Optional ontology item data for editing mode
|
||||
*/
|
||||
handleOpen: (data?: OntologyItem) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ontology class item data structure
|
||||
*/
|
||||
export interface OntologyClassItem {
|
||||
/** Unique identifier for the class */
|
||||
class_id: string;
|
||||
/** Name of the class */
|
||||
class_name: string;
|
||||
/** Description of the class */
|
||||
class_description: string;
|
||||
/** Associated scene identifier */
|
||||
scene_id: string;
|
||||
/** Creation timestamp */
|
||||
created_at: number;
|
||||
/** Last update timestamp */
|
||||
updated_at: number;
|
||||
}
|
||||
/**
|
||||
* Response data structure for ontology class list
|
||||
*/
|
||||
export interface OntologyClassData {
|
||||
/** Total number of classes */
|
||||
total: number;
|
||||
/** Scene identifier */
|
||||
scene_id: string;
|
||||
/** Scene name */
|
||||
scene_name: string;
|
||||
/** Scene description */
|
||||
scene_description: string;
|
||||
/** Array of class items */
|
||||
items: OntologyClassItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Data structure for adding a new class
|
||||
*/
|
||||
export interface AddClassItem {
|
||||
/** Name of the class to add */
|
||||
class_name: string;
|
||||
/** Description of the class to add */
|
||||
class_description: string;
|
||||
}
|
||||
/**
|
||||
* Form data for creating ontology classes
|
||||
*/
|
||||
export interface OntologyClassModalData {
|
||||
/** Target scene identifier */
|
||||
scene_id: string;
|
||||
/** Array of classes to create */
|
||||
classes: AddClassItem[]
|
||||
}
|
||||
/**
|
||||
* Ref methods exposed by OntologyClassModal component
|
||||
*/
|
||||
export interface OntologyClassModalRef {
|
||||
/**
|
||||
* Open the modal for adding classes
|
||||
* @param scene_id - Target scene identifier
|
||||
*/
|
||||
handleOpen: (scene_id: string) => void;
|
||||
}
|
||||
/**
|
||||
* Form data for extracting ontology classes using LLM
|
||||
*/
|
||||
export interface OntologyClassExtractModalData {
|
||||
/** LLM model identifier */
|
||||
llm_id: string;
|
||||
/** Target scene identifier */
|
||||
scene_id: string;
|
||||
/** Scenario description for extraction */
|
||||
scenario: string;
|
||||
domain: string; // scene_name
|
||||
/** Domain name (same as scene_name) */
|
||||
domain: string;
|
||||
}
|
||||
/**
|
||||
* Ref methods exposed by OntologyClassExtractModal component
|
||||
*/
|
||||
export interface OntologyClassExtractModalRef {
|
||||
/**
|
||||
* Open the modal for extracting classes
|
||||
* @param vo - Ontology class data containing scene information
|
||||
*/
|
||||
handleOpen: (vo: OntologyClassData) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracted class item from LLM
|
||||
*/
|
||||
export interface ExtractClassItem {
|
||||
/** Unique identifier for the extracted class */
|
||||
id: string;
|
||||
/** English name of the class */
|
||||
name: string;
|
||||
/** Chinese name of the class */
|
||||
name_chinese: string;
|
||||
/** Description of the class */
|
||||
description: string;
|
||||
/** Example instances of the class */
|
||||
examples: string[];
|
||||
/** Parent class name if exists */
|
||||
parent_class: string | null;
|
||||
/** Entity type classification */
|
||||
entity_type: string;
|
||||
/** Domain the class belongs to */
|
||||
domain: string;
|
||||
}
|
||||
/**
|
||||
* Response data structure for class extraction
|
||||
*/
|
||||
export interface ExtractData {
|
||||
/** Domain name */
|
||||
domain: string;
|
||||
/** Number of classes extracted */
|
||||
extracted_count: number;
|
||||
/** Array of extracted class items */
|
||||
classes: ExtractClassItem[]
|
||||
}
|
||||
/**
|
||||
* Ref methods exposed by OntologyImportModal component
|
||||
*/
|
||||
export interface OntologyImportModalRef {
|
||||
/** Open the import modal */
|
||||
handleOpen: () => void;
|
||||
}
|
||||
/**
|
||||
* Form data for importing ontology
|
||||
*/
|
||||
export interface OntologyImportModalData {
|
||||
/** Name for the imported scene */
|
||||
scene_name: string;
|
||||
/** Optional description for the imported scene */
|
||||
scene_description?: string;
|
||||
/** File to import (OWL, TTL, RDF, XML formats) */
|
||||
file: any;
|
||||
}
|
||||
/**
|
||||
* Ref methods exposed by OntologyExportModal component
|
||||
*/
|
||||
export interface OntologyExportModalRef {
|
||||
/** Open the export modal */
|
||||
handleOpen: () => void;
|
||||
}
|
||||
/**
|
||||
* Form data for exporting ontology
|
||||
*/
|
||||
export interface OntologyExportModalData {
|
||||
/** Scene identifier to export */
|
||||
scene_id: string;
|
||||
/** Export format: 'rdfxml' (.owl) or 'turtle' (.ttl) */
|
||||
format: 'rdfxml' | 'turtle';
|
||||
}
|
||||
@@ -38,7 +38,6 @@ const History: React.FC<{ query: HistoryQuery; edit: (item: HistoryItem) => void
|
||||
e?.stopPropagation();
|
||||
modal.confirm({
|
||||
title: t('common.confirmDeleteDesc', { name: item.title }),
|
||||
content: t('application.apiKeyDeleteContent'),
|
||||
okText: t('common.delete'),
|
||||
cancelText: t('common.cancel'),
|
||||
okType: 'danger',
|
||||
|
||||
@@ -1,30 +1,87 @@
|
||||
import { type FC } from 'react'
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 15:17:39
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 15:17:39
|
||||
*/
|
||||
import { useEffect, type FC } from 'react'
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Form, Input, Button, Row, Col } from 'antd'
|
||||
|
||||
import type { Suggestion } from '../../Editor/plugin/AutocompletePlugin'
|
||||
import VariableSelect from '../VariableSelect'
|
||||
|
||||
/**
|
||||
* Props for GroupVariableList component
|
||||
*/
|
||||
interface GroupVariableListProps {
|
||||
/** Current value - array of key-value pairs for grouped variables */
|
||||
value?: Array<{ key: string; value: string[]; }>;
|
||||
/** Form field name */
|
||||
name: string;
|
||||
/** Available variable options for selection */
|
||||
options: Suggestion[];
|
||||
/** Whether user can add custom groups */
|
||||
isCanAdd: boolean;
|
||||
/** Size of form controls */
|
||||
size: 'small' | 'middle'
|
||||
}
|
||||
|
||||
/**
|
||||
* GroupVariableList component
|
||||
* Manages grouped variable selection for var-aggregator node
|
||||
* Supports two modes:
|
||||
* 1. Simple mode (isCanAdd=false): Single variable list with type inference
|
||||
* 2. Advanced mode (isCanAdd=true): Multiple named groups with type inference per group
|
||||
* @param props - Component props
|
||||
*/
|
||||
const GroupVariableList: FC<GroupVariableListProps> = ({
|
||||
name,
|
||||
options = [],
|
||||
isCanAdd = false,
|
||||
size = "small"
|
||||
}) => {
|
||||
// Hooks
|
||||
const { t } = useTranslation();
|
||||
const form = Form.useFormInstance();
|
||||
|
||||
// Get current form value
|
||||
const value = form.getFieldValue(name) || [];
|
||||
|
||||
console.log('GroupVariableList', value)
|
||||
/**
|
||||
* Reset group_type when mode changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
form.setFieldValue('group_type', {})
|
||||
}, [isCanAdd])
|
||||
|
||||
/**
|
||||
* Auto-infer and set data types based on selected variables
|
||||
* In simple mode: Sets single output type
|
||||
* In advanced mode: Sets type for each group
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!isCanAdd && value[0]) {
|
||||
const firstVariable = options.find(opt => `{{${opt.value}}}` === value[0]);
|
||||
if (firstVariable) {
|
||||
form.setFieldValue(['group_type', 'output'], firstVariable.dataType);
|
||||
}
|
||||
} else if (isCanAdd) {
|
||||
value.forEach((item: any, index: number) => {
|
||||
if (item?.value?.[0]) {
|
||||
const firstVariable = options.find(opt => `{{${opt.value}}}` === item.value[0]);
|
||||
if (firstVariable) {
|
||||
form.setFieldValue(['group_type', index], firstVariable.dataType);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [isCanAdd, options, value, form])
|
||||
|
||||
/**
|
||||
* Simple mode rendering
|
||||
* Single variable list with automatic type filtering
|
||||
*/
|
||||
if (!isCanAdd) {
|
||||
// Filter options based on first variable's dataType if value exists
|
||||
let filteredOptions = options;
|
||||
@@ -53,77 +110,85 @@ const GroupVariableList: FC<GroupVariableListProps> = ({
|
||||
size={size}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name={['group_type', 'output']} hidden></Form.Item>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/**
|
||||
* Advanced mode rendering
|
||||
* Multiple named groups with individual variable lists
|
||||
*/
|
||||
return (
|
||||
<Form.List name={name}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => {
|
||||
return (
|
||||
<div key={key} className="rb:mb-4">
|
||||
<Row gutter={12} className="rb:mb-2!">
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={isCanAdd ? [name, 'key'] : undefined}
|
||||
rules={[
|
||||
{ pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/, message: t('workflow.config.var-aggregator.invalidVariableName') },
|
||||
]}
|
||||
noStyle
|
||||
>
|
||||
{isCanAdd ? <Input placeholder={t('common.pleaseEnter')} size={size} /> : t('workflow.config.var-aggregator.variable')}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<>
|
||||
<Form.List name={name}>
|
||||
{(fields, { add, remove }) => (
|
||||
<>
|
||||
{fields.map(({ key, name, ...restField }) => {
|
||||
return (
|
||||
<div key={key} className="rb:mb-4">
|
||||
<Row gutter={12} className="rb:mb-2!">
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={isCanAdd ? [name, 'key'] : undefined}
|
||||
rules={[
|
||||
{ pattern: /^[a-zA-Z_][a-zA-Z0-9_]*$/, message: t('workflow.config.var-aggregator.invalidVariableName') },
|
||||
]}
|
||||
noStyle
|
||||
>
|
||||
{isCanAdd ? <Input placeholder={t('common.pleaseEnter')} size={size} /> : t('workflow.config.var-aggregator.variable')}
|
||||
</Form.Item>
|
||||
</Col>
|
||||
|
||||
{isCanAdd && <Col span={12} className="rb:flex! rb:items-center rb:justify-end">
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(name)}
|
||||
></div>
|
||||
</Col>}
|
||||
</Row>
|
||||
{isCanAdd && <Col span={12} className="rb:flex! rb:items-center rb:justify-end">
|
||||
<div
|
||||
className="rb:ml-1 rb:size-4 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/workflow/deleteBg.svg')] rb:hover:bg-[url('@/assets/images/workflow/deleteBg_hover.svg')]"
|
||||
onClick={() => remove(name)}
|
||||
></div>
|
||||
</Col>}
|
||||
</Row>
|
||||
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'value']}
|
||||
noStyle
|
||||
>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={(() => {
|
||||
const currentGroupValue = value[name]?.value || [];
|
||||
if (currentGroupValue.length > 0) {
|
||||
const firstVariableValue = currentGroupValue[0];
|
||||
const firstVariable = options.find(opt => `{{${opt.value}}}` === firstVariableValue);
|
||||
if (firstVariable) {
|
||||
return options.filter(opt => opt.dataType === firstVariable.dataType);
|
||||
<Form.Item
|
||||
{...restField}
|
||||
name={[name, 'value']}
|
||||
noStyle
|
||||
>
|
||||
<VariableSelect
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={(() => {
|
||||
const currentGroupValue = value[name]?.value || [];
|
||||
if (currentGroupValue.length > 0) {
|
||||
const firstVariableValue = currentGroupValue[0];
|
||||
const firstVariable = options.find(opt => `{{${opt.value}}}` === firstVariableValue);
|
||||
if (firstVariable) {
|
||||
return options.filter(opt => opt.dataType === firstVariable.dataType);
|
||||
}
|
||||
}
|
||||
return options;
|
||||
})()
|
||||
}
|
||||
return options;
|
||||
})()
|
||||
}
|
||||
mode="multiple"
|
||||
size={size}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
mode="multiple"
|
||||
size={size}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
{isCanAdd && <Button
|
||||
type="dashed"
|
||||
block
|
||||
size="middle"
|
||||
className="rb:text-[12px]!"
|
||||
onClick={() => add({ key: `Group${fields.length + 1}` })}
|
||||
>
|
||||
+ {t('workflow.config.var-aggregator.addGroup')}
|
||||
</Button>}
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
{isCanAdd && <Button
|
||||
type="dashed"
|
||||
block
|
||||
size="middle"
|
||||
className="rb:text-[12px]!"
|
||||
onClick={() => add({ key: `Group${fields.length + 1}` })}
|
||||
>
|
||||
+ {t('workflow.config.var-aggregator.addGroup')}
|
||||
</Button>}
|
||||
</>
|
||||
)}
|
||||
</Form.List>
|
||||
<Form.Item name={['group_type']} hidden></Form.Item>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,36 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 15:40:13
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 15:40:13
|
||||
*/
|
||||
import { type FC } from 'react'
|
||||
import clsx from 'clsx';
|
||||
import { Select, type SelectProps } from 'antd'
|
||||
import type { Suggestion } from '../Editor/plugin/AutocompletePlugin'
|
||||
type LabelRender = SelectProps['labelRender'];
|
||||
|
||||
/**
|
||||
* Props for VariableSelect component
|
||||
*/
|
||||
interface VariableSelectProps extends SelectProps {
|
||||
/** Available variable options */
|
||||
options: Suggestion[];
|
||||
/** Current selected value */
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
/** Whether to show clear button */
|
||||
allowClear?: boolean;
|
||||
/** Filter out boolean type variables */
|
||||
filterBooleanType?: boolean;
|
||||
/** Size of the select component */
|
||||
size?: 'small' | 'middle' | 'large'
|
||||
}
|
||||
|
||||
/**
|
||||
* VariableSelect component
|
||||
* Custom select component for workflow variables with grouped options and custom rendering
|
||||
* @param props - Component props
|
||||
*/
|
||||
const VariableSelect: FC<VariableSelectProps> = ({
|
||||
placeholder,
|
||||
options,
|
||||
@@ -24,9 +42,19 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
...resetPorps
|
||||
}) => {
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
onChange?.(value);
|
||||
/**
|
||||
* Handle value change and pass selected option to parent
|
||||
* @param value - Selected value
|
||||
*/
|
||||
const handleChange: SelectProps['onChange'] = (value: string) => {
|
||||
const filterItem = options.find(option => `{{${option.value}}}` === value)
|
||||
onChange?.(value, filterItem);
|
||||
}
|
||||
/**
|
||||
* Custom label renderer for selected value
|
||||
* Displays node icon, name and variable label
|
||||
* @param props - Label render props
|
||||
*/
|
||||
const labelRender: LabelRender = (props) => {
|
||||
const { value } = props
|
||||
const filterOption = filteredOptions.find(vo => `{{${vo.value}}}` === value)
|
||||
@@ -57,10 +85,14 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
}
|
||||
return null
|
||||
}
|
||||
// Filter options based on boolean type if needed
|
||||
const filteredOptions = filterBooleanType
|
||||
? options.filter(option => option.dataType !== 'boolean')
|
||||
: options;
|
||||
|
||||
/**
|
||||
* Group suggestions by node ID
|
||||
*/
|
||||
const groupedSuggestions = filteredOptions.reduce((groups: Record<string, any[]>, suggestion) => {
|
||||
const { nodeData } = suggestion
|
||||
const nodeId = nodeData.id as string;
|
||||
@@ -71,6 +103,9 @@ const VariableSelect: FC<VariableSelectProps> = ({
|
||||
return groups;
|
||||
}, {});
|
||||
|
||||
/**
|
||||
* Format grouped options for Select component
|
||||
*/
|
||||
const groupedOptions = Object.entries(groupedSuggestions).map(([_nodeId, suggestions]) => ({
|
||||
label: suggestions[0].nodeData.name,
|
||||
options: suggestions.map(s => ({
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 15:39:59
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 15:39:59
|
||||
*/
|
||||
import { type FC, useEffect, useState, useMemo } from "react";
|
||||
import clsx from 'clsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@@ -31,17 +37,35 @@ import RbSlider from './RbSlider'
|
||||
import JinjaRender from './JinjaRender'
|
||||
import CodeExecution from './CodeExecution'
|
||||
|
||||
/**
|
||||
* Props for Properties component
|
||||
*/
|
||||
interface PropertiesProps {
|
||||
/** Currently selected node */
|
||||
selectedNode?: Node | null;
|
||||
/** Function to update selected node */
|
||||
setSelectedNode: (node: Node | null) => void;
|
||||
/** Reference to graph instance */
|
||||
graphRef: React.MutableRefObject<Graph | undefined>;
|
||||
/** Handler for blank canvas click */
|
||||
blankClick: () => void;
|
||||
/** Handler for delete event */
|
||||
deleteEvent: () => void;
|
||||
/** Handler for copy event */
|
||||
copyEvent: () => void;
|
||||
/** Handler for paste event */
|
||||
parseEvent: () => void;
|
||||
/** Workflow configuration */
|
||||
config?: any;
|
||||
/** Chat variables */
|
||||
chatVariables: ChatVariable[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Properties panel component
|
||||
* Displays and manages configuration for selected workflow node
|
||||
* @param props - Component props
|
||||
*/
|
||||
const Properties: FC<PropertiesProps> = ({
|
||||
selectedNode,
|
||||
graphRef,
|
||||
@@ -83,6 +107,10 @@ const Properties: FC<PropertiesProps> = ({
|
||||
}
|
||||
}, [selectedNode, form])
|
||||
|
||||
/**
|
||||
* Update node label in graph
|
||||
* @param newLabel - New label text
|
||||
*/
|
||||
const updateNodeLabel = (newLabel: string) => {
|
||||
if (selectedNode && form) {
|
||||
const nodeData = selectedNode.data as NodeProperties;
|
||||
@@ -107,8 +135,6 @@ const Properties: FC<PropertiesProps> = ({
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
|
||||
Object.keys(values).forEach(key => {
|
||||
if (selectedNode.data?.config?.[key]) {
|
||||
// Create a deep copy to avoid reference sharing between nodes
|
||||
@@ -131,7 +157,12 @@ const Properties: FC<PropertiesProps> = ({
|
||||
|
||||
|
||||
|
||||
// Filter out boolean type variables for loop and llm nodes
|
||||
/**
|
||||
* Get filtered variable list based on node type and config key
|
||||
* @param nodeType - Type of the node
|
||||
* @param key - Configuration key
|
||||
* @returns Filtered variable list
|
||||
*/
|
||||
const getFilteredVariableList = (nodeType?: string, key?: string) => {
|
||||
// Check if current node is a child of iteration node
|
||||
const parentIterationNode = selectedNode ? (() => {
|
||||
@@ -321,15 +352,33 @@ const Properties: FC<PropertiesProps> = ({
|
||||
|
||||
console.log('values', values)
|
||||
|
||||
/**
|
||||
* Get current node output variables
|
||||
*/
|
||||
const currentNodeVariables = useMemo(() => {
|
||||
if (!selectedNode) return []
|
||||
return getCurrentNodeVariables(selectedNode?.getData(), values)
|
||||
}, [selectedNode?.getData(), values])
|
||||
|
||||
const [outputCollapsed, setOutputCollapsed] = useState(true)
|
||||
/**
|
||||
* Toggle output section collapsed state
|
||||
*/
|
||||
const handleToggle = () => {
|
||||
setOutputCollapsed((prev: boolean) => !prev)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle variable list change and update output type for iteration nodes
|
||||
* @param _value - Selected value
|
||||
* @param option - Selected option
|
||||
* @param key - Configuration key
|
||||
*/
|
||||
const handleChangeVariableList = (_value: string, option: any, key: string) => {
|
||||
if (selectedNode?.data?.type === 'iteration' && key === 'output') {
|
||||
form.setFieldValue('output_type', option?.dataType)
|
||||
}
|
||||
}
|
||||
console.log('variableList', variableList, currentNodeVariables)
|
||||
|
||||
return (
|
||||
@@ -422,6 +471,9 @@ const Properties: FC<PropertiesProps> = ({
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
if (selectedNode?.data?.type === 'iteration' && key === 'output_type') {
|
||||
return (<Form.Item key={key} name={key} hidden />)
|
||||
}
|
||||
if (config.type === 'define') {
|
||||
return null
|
||||
}
|
||||
@@ -628,8 +680,8 @@ const Properties: FC<PropertiesProps> = ({
|
||||
);
|
||||
}
|
||||
return baseVariableList;
|
||||
})()
|
||||
}
|
||||
})()}
|
||||
onChange={(value, option) => handleChangeVariableList(value, option, key)}
|
||||
size="small"
|
||||
/>
|
||||
: config.type === 'switch'
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 15:06:18
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 15:25:25
|
||||
*/
|
||||
import LoopNode from './components/Nodes/LoopNode';
|
||||
import NormalNode from './components/Nodes/NormalNode';
|
||||
import ConditionNode from './components/Nodes/ConditionNode';
|
||||
@@ -9,33 +15,33 @@ import type { ReactShapeConfig } from '@antv/x6-react-shape';
|
||||
// Import workflow icons
|
||||
import startIcon from '@/assets/images/workflow/start.png';
|
||||
import endIcon from '@/assets/images/workflow/end.png';
|
||||
import answerIcon from '@/assets/images/workflow/answer.png';
|
||||
// import answerIcon from '@/assets/images/workflow/answer.png';
|
||||
import llmIcon from '@/assets/images/workflow/llm.png';
|
||||
import modelSelectionIcon from '@/assets/images/workflow/model_selection.png';
|
||||
import modelVotingIcon from '@/assets/images/workflow/model_voting.png';
|
||||
// import modelSelectionIcon from '@/assets/images/workflow/model_selection.png';
|
||||
// import modelVotingIcon from '@/assets/images/workflow/model_voting.png';
|
||||
import ragIcon from '@/assets/images/workflow/rag.png';
|
||||
import classificationIcon from '@/assets/images/workflow/classification.png';
|
||||
// import classificationIcon from '@/assets/images/workflow/classification.png';
|
||||
import parameterExtractionIcon from '@/assets/images/workflow/parameter_extraction.png';
|
||||
import taskPlanningIcon from '@/assets/images/workflow/task_planning.png';
|
||||
import reasoningControlIcon from '@/assets/images/workflow/reasoning_control.png';
|
||||
import selfReflectionIcon from '@/assets/images/workflow/self_reflection.png';
|
||||
import memoryEnhancementIcon from '@/assets/images/workflow/memory_enhancement.png';
|
||||
import agentSchedulingIcon from '@/assets/images/workflow/agent_scheduling.png';
|
||||
import agentCollaborationIcon from '@/assets/images/workflow/agent_collaboration.png';
|
||||
import agentArbitrationIcon from '@/assets/images/workflow/agent_arbitration.png';
|
||||
// import taskPlanningIcon from '@/assets/images/workflow/task_planning.png';
|
||||
// import reasoningControlIcon from '@/assets/images/workflow/reasoning_control.png';
|
||||
// import selfReflectionIcon from '@/assets/images/workflow/self_reflection.png';
|
||||
// import memoryEnhancementIcon from '@/assets/images/workflow/memory_enhancement.png';
|
||||
// import agentSchedulingIcon from '@/assets/images/workflow/agent_scheduling.png';
|
||||
// import agentCollaborationIcon from '@/assets/images/workflow/agent_collaboration.png';
|
||||
// import agentArbitrationIcon from '@/assets/images/workflow/agent_arbitration.png';
|
||||
import conditionIcon from '@/assets/images/workflow/condition.png';
|
||||
import iterationIcon from '@/assets/images/workflow/iteration.png';
|
||||
import loopIcon from '@/assets/images/workflow/loop.png';
|
||||
import parallelIcon from '@/assets/images/workflow/parallel.png';
|
||||
// import parallelIcon from '@/assets/images/workflow/parallel.png';
|
||||
import aggregatorIcon from '@/assets/images/workflow/aggregator.png';
|
||||
import httpRequestIcon from '@/assets/images/workflow/http_request.png';
|
||||
import toolsIcon from '@/assets/images/workflow/tools.png';
|
||||
import codeExecutionIcon from '@/assets/images/workflow/code_execution.png';
|
||||
import templateRenderingIcon from '@/assets/images/workflow/template_rendering.png';
|
||||
import sensitiveDetectionIcon from '@/assets/images/workflow/sensitive_detection.png';
|
||||
import outputAuditIcon from '@/assets/images/workflow/output_audit.png';
|
||||
import selfOptimizationIcon from '@/assets/images/workflow/self_optimization.png';
|
||||
import processEvolutionIcon from '@/assets/images/workflow/process_evolution.png';
|
||||
// import sensitiveDetectionIcon from '@/assets/images/workflow/sensitive_detection.png';
|
||||
// import outputAuditIcon from '@/assets/images/workflow/output_audit.png';
|
||||
// import selfOptimizationIcon from '@/assets/images/workflow/self_optimization.png';
|
||||
// import processEvolutionIcon from '@/assets/images/workflow/process_evolution.png';
|
||||
import questionClassifierIcon from '@/assets/images/workflow/question-classifier.png'
|
||||
import breakIcon from '@/assets/images/workflow/break.png'
|
||||
import assignerIcon from '@/assets/images/workflow/assigner.png'
|
||||
@@ -47,6 +53,10 @@ import { memoryConfigListUrl } from '@/api/memory'
|
||||
import { getModelListUrl } from '@/api/models'
|
||||
import type { NodeLibrary } from './types'
|
||||
|
||||
/**
|
||||
* Workflow node library configuration
|
||||
* Defines all available node types, their icons, and configuration schemas
|
||||
*/
|
||||
export const nodeLibrary: NodeLibrary[] = [
|
||||
{
|
||||
category: "coreNode",
|
||||
@@ -300,13 +310,16 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
dependsOn: 'parallel',
|
||||
dependsOnValue: true
|
||||
},
|
||||
flatten: { // 扁平化输出
|
||||
flatten: { // Flatten output
|
||||
type: 'switch',
|
||||
defaultValue: false
|
||||
},
|
||||
output: {
|
||||
type: 'variableList',
|
||||
filterChildNodes: true
|
||||
},
|
||||
output_type: {
|
||||
type: 'define',
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -345,6 +358,9 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
group_variables: {
|
||||
type: 'groupVariableList',
|
||||
defaultValue: [],
|
||||
},
|
||||
group_type: {
|
||||
type: 'define',
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -490,7 +506,10 @@ export const nodeLibrary: NodeLibrary[] = [
|
||||
// },
|
||||
];
|
||||
|
||||
// 节点注册库
|
||||
/**
|
||||
* Node registration library for X6 graph
|
||||
* Maps node shapes to their React components
|
||||
*/
|
||||
export const nodeRegisterLibrary: ReactShapeConfig[] = [
|
||||
{
|
||||
shape: 'loop-node',
|
||||
@@ -530,21 +549,39 @@ export const nodeRegisterLibrary: ReactShapeConfig[] = [
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Port configuration interface
|
||||
*/
|
||||
interface PortsConfig {
|
||||
/** Port group metadata */
|
||||
groups?: GroupMetadata;
|
||||
/** Port item metadata array */
|
||||
items?: PortMetadata[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Node configuration interface
|
||||
*/
|
||||
interface NodeConfig {
|
||||
/** Node width in pixels */
|
||||
width: number;
|
||||
/** Node height in pixels */
|
||||
height: number;
|
||||
/** Node shape type */
|
||||
shape: string;
|
||||
/** Port configuration */
|
||||
ports?: PortsConfig;
|
||||
}
|
||||
|
||||
/** Edge color for normal state */
|
||||
export const edge_color = '#155EEF';
|
||||
/** Edge color for selected state */
|
||||
export const edge_selected_color = '#4DA8FF'
|
||||
// 统一的端口 markup 配置
|
||||
|
||||
/**
|
||||
* Unified port markup configuration
|
||||
* Defines SVG elements for port rendering
|
||||
*/
|
||||
export const portMarkup = [
|
||||
{
|
||||
tagName: 'circle',
|
||||
@@ -556,7 +593,10 @@ export const portMarkup = [
|
||||
},
|
||||
];
|
||||
|
||||
// 统一的端口属性配置
|
||||
/**
|
||||
* Unified port attributes configuration
|
||||
* Defines visual styling for ports
|
||||
*/
|
||||
export const portAttrs = {
|
||||
body: {
|
||||
r: 6,
|
||||
@@ -576,20 +616,34 @@ export const portAttrs = {
|
||||
}
|
||||
}
|
||||
|
||||
// 统一的端口组配置
|
||||
/**
|
||||
* Unified port group configuration
|
||||
* Defines port positions and attributes for different sides
|
||||
*/
|
||||
const defaultPortGroups = {
|
||||
// top: { position: 'top', markup: portMarkup, attrs: portAttrs },
|
||||
right: { position: 'right', markup: portMarkup, attrs: portAttrs },
|
||||
// bottom: { position: 'bottom', markup: portMarkup, attrs: portAttrs },
|
||||
left: { position: 'left', markup: portMarkup, attrs: portAttrs },
|
||||
}
|
||||
/**
|
||||
* Default port items for standard nodes
|
||||
*/
|
||||
const defaultPortItems = [
|
||||
// { group: 'top' },
|
||||
{ group: 'right' },
|
||||
// { group: 'bottom' },
|
||||
{ group: 'left' }
|
||||
];
|
||||
/**
|
||||
* Port position arguments
|
||||
*/
|
||||
export const portArgs = { dy: 18 }
|
||||
|
||||
/**
|
||||
* Graph node library configuration
|
||||
* Maps node types to their visual and structural properties
|
||||
*/
|
||||
export const graphNodeLibrary: Record<string, NodeConfig> = {
|
||||
iteration: {
|
||||
width: 240,
|
||||
@@ -701,21 +755,33 @@ export const graphNodeLibrary: Record<string, NodeConfig> = {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Output variable configuration interface
|
||||
*/
|
||||
export interface OutputVariable {
|
||||
/** Default output variables */
|
||||
default?: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
}>;
|
||||
/** Dynamically defined variable keys */
|
||||
define?: string[];
|
||||
/** System-level output variables */
|
||||
sys?: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
}>;
|
||||
/** Error-related output variables */
|
||||
error?: Array<{
|
||||
name: string;
|
||||
type: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output variable definitions for each node type
|
||||
* Specifies what variables each node produces
|
||||
*/
|
||||
export const outputVariable: { [key: string]: OutputVariable } = {
|
||||
start: {
|
||||
sys: [
|
||||
@@ -806,6 +872,10 @@ export const outputVariable: { [key: string]: OutputVariable } = {
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Default edge attributes configuration
|
||||
* Defines visual styling for edges/connections
|
||||
*/
|
||||
export const edgeAttrs = {
|
||||
attrs: {
|
||||
line: {
|
||||
|
||||
@@ -1,48 +1,90 @@
|
||||
/*
|
||||
* @Author: ZhaoYing
|
||||
* @Date: 2026-02-03 15:17:48
|
||||
* @Last Modified by: ZhaoYing
|
||||
* @Last Modified time: 2026-02-03 15:17:48
|
||||
*/
|
||||
import { useRef, useEffect, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { App } from 'antd'
|
||||
import { Graph, Node, MiniMap, Snapline, Clipboard, Keyboard, type Edge } from '@antv/x6';
|
||||
import { register } from '@antv/x6-react-shape';
|
||||
import type { PortMetadata } from '@antv/x6/lib/model/port';
|
||||
|
||||
import { nodeRegisterLibrary, graphNodeLibrary, nodeLibrary, portMarkup, portAttrs, edgeAttrs, edge_color, edge_selected_color, portArgs } from '../constant';
|
||||
import type { WorkflowConfig, NodeProperties, ChatVariable } from '../types';
|
||||
import { getWorkflowConfig, saveWorkflowConfig } from '@/api/application'
|
||||
import type { PortMetadata } from '@antv/x6/lib/model/port';
|
||||
|
||||
/**
|
||||
* Props for useWorkflowGraph hook
|
||||
*/
|
||||
export interface UseWorkflowGraphProps {
|
||||
/** Reference to the main graph container element */
|
||||
containerRef: React.RefObject<HTMLDivElement>;
|
||||
/** Reference to the minimap container element */
|
||||
miniMapRef: React.RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return type for useWorkflowGraph hook
|
||||
*/
|
||||
export interface UseWorkflowGraphReturn {
|
||||
/** Current workflow configuration */
|
||||
config: WorkflowConfig | null;
|
||||
/** Function to update workflow configuration */
|
||||
setConfig: React.Dispatch<React.SetStateAction<WorkflowConfig | null>>;
|
||||
/** Reference to the X6 graph instance */
|
||||
graphRef: React.MutableRefObject<Graph | undefined>;
|
||||
/** Currently selected node */
|
||||
selectedNode: Node | null;
|
||||
/** Function to update selected node */
|
||||
setSelectedNode: React.Dispatch<React.SetStateAction<Node | null>>;
|
||||
/** Current zoom level of the graph */
|
||||
zoomLevel: number;
|
||||
/** Function to update zoom level */
|
||||
setZoomLevel: React.Dispatch<React.SetStateAction<number>>;
|
||||
/** Whether hand/pan mode is enabled */
|
||||
isHandMode: boolean;
|
||||
/** Function to toggle hand mode */
|
||||
setIsHandMode: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
/** Handler for dropping nodes onto canvas */
|
||||
onDrop: (event: React.DragEvent) => void;
|
||||
/** Handler for clicking blank canvas area */
|
||||
blankClick: () => void;
|
||||
/** Handler for delete keyboard event */
|
||||
deleteEvent: () => boolean | void;
|
||||
/** Handler for copy keyboard event */
|
||||
copyEvent: () => boolean | void;
|
||||
/** Handler for paste keyboard event */
|
||||
parseEvent: () => boolean | void;
|
||||
/** Function to save workflow configuration */
|
||||
handleSave: (flag?: boolean) => Promise<unknown>;
|
||||
/** Chat variables for workflow */
|
||||
chatVariables: ChatVariable[];
|
||||
/** Function to update chat variables */
|
||||
setChatVariables: React.Dispatch<React.SetStateAction<ChatVariable[]>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom hook for managing workflow graph
|
||||
* Handles graph initialization, node/edge operations, and workflow configuration
|
||||
* @param props - Hook props containing container references
|
||||
* @returns Object containing graph state and handlers
|
||||
*/
|
||||
export const useWorkflowGraph = ({
|
||||
containerRef,
|
||||
miniMapRef,
|
||||
}: UseWorkflowGraphProps): UseWorkflowGraphReturn => {
|
||||
// Hooks
|
||||
const { id } = useParams();
|
||||
const { message } = App.useApp();
|
||||
const { t } = useTranslation()
|
||||
|
||||
// Refs
|
||||
const graphRef = useRef<Graph>();
|
||||
|
||||
// State
|
||||
const [selectedNode, setSelectedNode] = useState<Node | null>(null);
|
||||
const [zoomLevel, setZoomLevel] = useState(1);
|
||||
const [isHandMode, setIsHandMode] = useState(true);
|
||||
@@ -52,6 +94,9 @@ export const useWorkflowGraph = ({
|
||||
useEffect(() => {
|
||||
getConfig()
|
||||
}, [id])
|
||||
/**
|
||||
* Fetch workflow configuration from API
|
||||
*/
|
||||
const getConfig = () => {
|
||||
if (!id) return
|
||||
getWorkflowConfig(id)
|
||||
@@ -73,6 +118,9 @@ export const useWorkflowGraph = ({
|
||||
initWorkflow()
|
||||
}, [config, graphRef.current])
|
||||
|
||||
/**
|
||||
* Initialize workflow graph with nodes and edges from configuration
|
||||
*/
|
||||
const initWorkflow = () => {
|
||||
if (!config || !graphRef.current) return
|
||||
const { nodes, edges } = config
|
||||
@@ -129,7 +177,7 @@ export const useWorkflowGraph = ({
|
||||
...position,
|
||||
}
|
||||
|
||||
// 如果是if-else节点,根据cases动态生成端口
|
||||
// Generate ports dynamically for if-else node based on cases
|
||||
if (type === 'if-else' && config.cases && Array.isArray(config.cases)) {
|
||||
const caseCount = config.cases.length;
|
||||
const totalPorts = caseCount + 1; // IF/ELIF + ELSE
|
||||
@@ -141,7 +189,7 @@ export const useWorkflowGraph = ({
|
||||
{ group: 'right', id: 'CASE1', args: portArgs, attrs: { text: { text: 'IF', fontSize: 12, fill: '#5B6167' }} }
|
||||
];
|
||||
|
||||
// 添加 ELIF 端口
|
||||
// Add ELIF ports
|
||||
for (let i = 1; i < caseCount; i++) {
|
||||
portItems.push({
|
||||
group: 'right',
|
||||
@@ -151,7 +199,7 @@ export const useWorkflowGraph = ({
|
||||
});
|
||||
}
|
||||
|
||||
// 添加 ELSE 端口
|
||||
// Add ELSE port
|
||||
portItems.push({
|
||||
group: 'right',
|
||||
id: `CASE${caseCount + 1}`,
|
||||
@@ -170,7 +218,7 @@ export const useWorkflowGraph = ({
|
||||
nodeConfig.height = newHeight;
|
||||
}
|
||||
|
||||
// 如果是question-classifier节点,根据categories动态生成端口
|
||||
// Generate ports dynamically for question-classifier node based on categories
|
||||
if (type === 'question-classifier' && config.categories && Array.isArray(config.categories)) {
|
||||
const categoryCount = config.categories.length;
|
||||
const baseHeight = 88;
|
||||
@@ -180,7 +228,7 @@ export const useWorkflowGraph = ({
|
||||
{ group: 'left' }
|
||||
];
|
||||
|
||||
// 添加分类端口
|
||||
// Add category ports
|
||||
config.categories.forEach((_category: any, index: number) => {
|
||||
portItems.push({
|
||||
group: 'right',
|
||||
@@ -201,7 +249,7 @@ export const useWorkflowGraph = ({
|
||||
nodeConfig.height = newHeight;
|
||||
}
|
||||
|
||||
// 如果是http-request节点,检查error_handle.method配置
|
||||
// Check error_handle.method config for http-request node
|
||||
if (type === 'http-request' && (config as any).error_handle?.method === 'branch') {
|
||||
nodeConfig.ports = {
|
||||
groups: {
|
||||
@@ -219,14 +267,14 @@ export const useWorkflowGraph = ({
|
||||
return nodeConfig
|
||||
})
|
||||
|
||||
// 分离父节点和子节点
|
||||
// Separate parent nodes and child nodes
|
||||
const parentNodes = nodeList.filter(node => !node.data.cycle)
|
||||
const childNodes = nodeList.filter(node => node.data.cycle)
|
||||
|
||||
// 先添加父节点
|
||||
// Add parent nodes first
|
||||
graphRef.current?.addNodes(parentNodes)
|
||||
|
||||
// 然后处理子节点,使用addChild添加到对应的父节点
|
||||
// Then process child nodes, use addChild to add to corresponding parent node
|
||||
childNodes.forEach(childNode => {
|
||||
const cycleId = childNode.data.cycle
|
||||
if (cycleId) {
|
||||
@@ -240,7 +288,7 @@ export const useWorkflowGraph = ({
|
||||
}
|
||||
})
|
||||
|
||||
// 调整父节点大小以适应子节点
|
||||
// Adjust parent node size to fit child nodes
|
||||
setTimeout(() => {
|
||||
const parentNodesWithChildren = parentNodes.filter(parentNode => {
|
||||
const parentId = parentNode.data.id
|
||||
@@ -274,7 +322,7 @@ export const useWorkflowGraph = ({
|
||||
}, 100)
|
||||
}
|
||||
if (edges.length) {
|
||||
// 去重处理:对于if-else和question-classifier节点,不同连接桩允许连接到相同节点
|
||||
// Deduplication: For if-else and question-classifier nodes, different ports can connect to same node
|
||||
const uniqueEdges = edges.filter((edge, index, arr) => {
|
||||
return arr.findIndex(e => {
|
||||
const sourceCell = graphRef.current?.getCellById(e.source);
|
||||
@@ -282,10 +330,10 @@ export const useWorkflowGraph = ({
|
||||
const isMultiPortNode = sourceType === 'question-classifier' || sourceType === 'if-else';
|
||||
|
||||
if (isMultiPortNode) {
|
||||
// 多端口节点需要同时比较source、target和label
|
||||
// Multi-port nodes need to compare source, target and label
|
||||
return e.source === edge.source && e.target === edge.target && e.label === edge.label;
|
||||
} else {
|
||||
// 其他节点只比较source和target
|
||||
// Other nodes only compare source and target
|
||||
return e.source === edge.source && e.target === edge.target;
|
||||
}
|
||||
}) === index;
|
||||
@@ -302,16 +350,16 @@ export const useWorkflowGraph = ({
|
||||
|
||||
let sourcePort = sourcePorts.find((port: any) => port.group === 'right')?.id || 'right';
|
||||
|
||||
// 如果是if-else节点且有label,根据label匹配对应的端口
|
||||
// If if-else node has label, match corresponding port by label
|
||||
if (sourceCell.getData()?.type === 'if-else' && label) {
|
||||
// 查找匹配的端口ID
|
||||
// Find matching port ID
|
||||
const matchingPort = sourcePorts.find((port: any) => port.id === label);
|
||||
if (matchingPort) {
|
||||
sourcePort = label;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是question-classifier节点且有label,根据label匹配对应的端口
|
||||
// If question-classifier node has label, match corresponding port by label
|
||||
if (sourceCell.getData()?.type === 'question-classifier' && label) {
|
||||
const matchingPort = sourcePorts.find((port: any) => port.id === label);
|
||||
if (matchingPort) {
|
||||
@@ -319,7 +367,7 @@ export const useWorkflowGraph = ({
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是http-request节点且有label,根据label匹配对应的端口
|
||||
// If http-request node has label, match corresponding port by label
|
||||
if (sourceCell.getData()?.type === 'http-request' && label) {
|
||||
const matchingPort = sourcePorts.find((port: any) => port.id === label);
|
||||
if (matchingPort) {
|
||||
@@ -348,7 +396,7 @@ export const useWorkflowGraph = ({
|
||||
graphRef.current.addEdges(edgeList.filter(vo => vo !== null))
|
||||
}
|
||||
|
||||
// 初始化完成后,将节点展示在可视区域内
|
||||
// Initialize after completion, display nodes in visible area
|
||||
if (nodes.length > 0 || edges.length > 0) {
|
||||
setTimeout(() => {
|
||||
if (graphRef.current) {
|
||||
@@ -357,7 +405,9 @@ export const useWorkflowGraph = ({
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
// 使用插件
|
||||
/**
|
||||
* Setup X6 graph plugins (MiniMap, Snapline, Clipboard, Keyboard)
|
||||
*/
|
||||
const setupPlugins = () => {
|
||||
if (!graphRef.current || !miniMapRef.current) return;
|
||||
// 添加小地图
|
||||
@@ -395,9 +445,12 @@ export const useWorkflowGraph = ({
|
||||
// ports[i].style.visibility = show ? 'visible' : 'hidden';
|
||||
// }
|
||||
// };
|
||||
// 节点选择事件
|
||||
/**
|
||||
* Handle node click event
|
||||
* @param node - Clicked node
|
||||
*/
|
||||
const nodeClick = ({ node }: { node: Node }) => {
|
||||
// 忽略 add-node 类型的节点点击
|
||||
// Ignore add-node type node clicks
|
||||
if (node.getData()?.type === 'add-node' || node.getData().type === 'break' || node.getData().type === 'cycle-start') {
|
||||
setSelectedNode(null)
|
||||
return;
|
||||
@@ -420,12 +473,17 @@ export const useWorkflowGraph = ({
|
||||
});
|
||||
setSelectedNode(node);
|
||||
};
|
||||
// 连线选择事件
|
||||
/**
|
||||
* Handle edge click event
|
||||
* @param edge - Clicked edge
|
||||
*/
|
||||
const edgeClick = ({ edge }: { edge: Edge }) => {
|
||||
edge.setAttrByPath('line/stroke', edge_selected_color);
|
||||
clearNodeSelect();
|
||||
};
|
||||
// 清空选中节点
|
||||
/**
|
||||
* Clear all selected nodes
|
||||
*/
|
||||
const clearNodeSelect = () => {
|
||||
const nodes = graphRef.current?.getNodes();
|
||||
|
||||
@@ -440,44 +498,54 @@ export const useWorkflowGraph = ({
|
||||
});
|
||||
setSelectedNode(null);
|
||||
};
|
||||
// 清空选中连线
|
||||
/**
|
||||
* Clear all selected edges
|
||||
*/
|
||||
const clearEdgeSelect = () => {
|
||||
graphRef.current?.getEdges().forEach(e => {
|
||||
e.setAttrByPath('line/stroke', edge_color);
|
||||
e.setAttrByPath('line/strokeWidth', 1);
|
||||
});
|
||||
};
|
||||
// 画布点击事件,取消选择
|
||||
/**
|
||||
* Handle blank canvas click - deselect all
|
||||
*/
|
||||
const blankClick = () => {
|
||||
clearNodeSelect();
|
||||
clearEdgeSelect();
|
||||
graphRef.current?.cleanSelection();
|
||||
};
|
||||
// 画布缩放事件
|
||||
/**
|
||||
* Handle canvas scale/zoom event
|
||||
* @param sx - Scale factor on x-axis
|
||||
*/
|
||||
const scaleEvent = ({ sx }: { sx: number }) => {
|
||||
setZoomLevel(sx);
|
||||
};
|
||||
// 节点移动事件
|
||||
/**
|
||||
* Handle node moved event - restrict child nodes within parent bounds
|
||||
* @param node - Moved node
|
||||
*/
|
||||
const nodeMoved = ({ node }: { node: Node }) => {
|
||||
const cycle = node.getData()?.cycle;
|
||||
if (cycle) {
|
||||
const parentNode = graphRef.current!.getNodes().find(n => n.id === cycle);
|
||||
if (parentNode?.getData()?.isGroup) {
|
||||
// 获取父节点和子节点的边界框
|
||||
// Get parent node and child node bounding boxes
|
||||
const parentBBox = parentNode.getBBox();
|
||||
const childBBox = node.getBBox();
|
||||
|
||||
// 计算父节点的内边距
|
||||
// Calculate parent node padding
|
||||
const padding = 24;
|
||||
const headerHeight = 50;
|
||||
|
||||
// 计算子节点允许的最小和最大位置
|
||||
// Calculate minimum and maximum positions allowed for child node
|
||||
const minX = parentBBox.x + padding;
|
||||
const minY = parentBBox.y + padding + headerHeight;
|
||||
const maxX = parentBBox.x + parentBBox.width - padding - childBBox.width;
|
||||
const maxY = parentBBox.y + parentBBox.height - padding - childBBox.height;
|
||||
|
||||
// 限制子节点在父节点内移动
|
||||
// Restrict child node movement within parent node
|
||||
let newX = childBBox.x;
|
||||
let newY = childBBox.y;
|
||||
|
||||
@@ -486,14 +554,17 @@ export const useWorkflowGraph = ({
|
||||
if (newX > maxX) newX = maxX;
|
||||
if (newY > maxY) newY = maxY;
|
||||
|
||||
// 如果子节点位置被限制,更新其位置
|
||||
// If child node position is restricted, update its position
|
||||
if (newX !== childBBox.x || newY !== childBBox.y) {
|
||||
node.setPosition(newX, newY);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// 复制快捷键事件
|
||||
/**
|
||||
* Handle copy keyboard shortcut (Ctrl+C / Cmd+C)
|
||||
* @returns false to prevent default behavior
|
||||
*/
|
||||
const copyEvent = () => {
|
||||
if (!graphRef.current) return false;
|
||||
const selectedNodes = graphRef.current.getNodes().filter(node => node.getData()?.isSelected);
|
||||
@@ -502,7 +573,10 @@ export const useWorkflowGraph = ({
|
||||
}
|
||||
return false;
|
||||
};
|
||||
// 粘贴快捷键事件
|
||||
/**
|
||||
* Handle paste keyboard shortcut (Ctrl+V / Cmd+V)
|
||||
* @returns false to prevent default behavior
|
||||
*/
|
||||
const parseEvent = () => {
|
||||
if (!graphRef.current?.isClipboardEmpty()) {
|
||||
graphRef.current?.paste({ offset: 32 });
|
||||
@@ -510,7 +584,11 @@ export const useWorkflowGraph = ({
|
||||
}
|
||||
return false;
|
||||
};
|
||||
// 删除选中的节点和连线事件
|
||||
/**
|
||||
* Handle delete keyboard shortcut
|
||||
* Removes selected nodes, edges, and handles parent-child relationships
|
||||
* @returns false to prevent default behavior
|
||||
*/
|
||||
const deleteEvent = () => {
|
||||
if (!graphRef.current) return;
|
||||
const nodes = graphRef.current?.getNodes();
|
||||
@@ -519,16 +597,16 @@ export const useWorkflowGraph = ({
|
||||
const nodesToDelete: Node[] = [];
|
||||
const parentNodesToUpdate: Node[] = [];
|
||||
|
||||
// 首先收集所有选中的节点,但排除默认子节点
|
||||
// First collect all selected nodes, but exclude default child nodes
|
||||
nodes?.forEach(node => {
|
||||
const data = node.getData();
|
||||
// 如果节点是默认子节点,不允许单独删除
|
||||
// If node is default child node, do not allow individual deletion
|
||||
if (data.isSelected && !data.isDefault) {
|
||||
nodesToDelete.push(node);
|
||||
}
|
||||
});
|
||||
|
||||
// 收集与选中节点相关的连线
|
||||
// Collect edges related to selected nodes
|
||||
edges?.forEach(edge => {
|
||||
const attrs = edge.getAttrs()
|
||||
if (attrs.line.stroke === edge_selected_color) {
|
||||
@@ -545,35 +623,35 @@ export const useWorkflowGraph = ({
|
||||
}
|
||||
})
|
||||
|
||||
// 对于每个选中的节点
|
||||
// For each selected node
|
||||
if (nodesToDelete.length > 0) {
|
||||
nodesToDelete.forEach(nodeToDelete => {
|
||||
// 检查是否为子节点
|
||||
// Check if it's a child node
|
||||
const nodeData = nodeToDelete.getData();
|
||||
if (nodeData.cycle) {
|
||||
// 找到对应的父节点
|
||||
// Find corresponding parent node
|
||||
const parentNode = nodes?.find(n => n.id === nodeData.cycle);
|
||||
if (parentNode) {
|
||||
// 使用removeChild方法删除子节点
|
||||
// Use removeChild method to delete child node
|
||||
parentNode.removeChild(nodeToDelete);
|
||||
parentNodesToUpdate.push(parentNode);
|
||||
}
|
||||
// 将子节点添加到删除列表
|
||||
// Add child node to deletion list
|
||||
cells.push(nodeToDelete);
|
||||
}
|
||||
// 检查是否为 LoopNode、IterationNode 或 SubGraphNode
|
||||
// Check if it's LoopNode, IterationNode or SubGraphNode
|
||||
else if (nodeToDelete.shape === 'loop-node' || nodeToDelete.shape === 'iteration-node' || nodeToDelete.shape === 'subgraph-node') {
|
||||
// 查找所有 cycle 为当前节点 id 的子节点
|
||||
// Find all child nodes with cycle equal to current node id
|
||||
nodes?.forEach(node => {
|
||||
const data = node.getData();
|
||||
if (data.cycle === nodeToDelete.id || data.cycle === nodeToDelete.getData()?.id) {
|
||||
cells.push(node);
|
||||
}
|
||||
});
|
||||
// 添加父节点到删除列表
|
||||
// Add parent node to deletion list
|
||||
cells.push(nodeToDelete);
|
||||
}
|
||||
// 普通节点
|
||||
// Normal node
|
||||
else {
|
||||
cells.push(nodeToDelete);
|
||||
}
|
||||
@@ -581,25 +659,29 @@ export const useWorkflowGraph = ({
|
||||
blankClick();
|
||||
}
|
||||
|
||||
// 删除所有收集的节点和连线
|
||||
// Delete all collected nodes and edges
|
||||
if (cells.length > 0) {
|
||||
graphRef.current?.removeCells(cells);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// 调整画布大小
|
||||
/**
|
||||
* Handle window resize event
|
||||
*/
|
||||
const handleResize = () => {
|
||||
if (containerRef.current && graphRef.current) {
|
||||
graphRef.current.resize(containerRef.current.offsetWidth, containerRef.current.offsetHeight);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化
|
||||
/**
|
||||
* Initialize X6 graph with configuration and event listeners
|
||||
*/
|
||||
const init = () => {
|
||||
if (!containerRef.current || !miniMapRef.current) return;
|
||||
|
||||
// 注册React形状
|
||||
// Register React shapes
|
||||
nodeRegisterLibrary.forEach((item) => {
|
||||
register(item);
|
||||
});
|
||||
@@ -616,8 +698,8 @@ export const useWorkflowGraph = ({
|
||||
type: 'dot',
|
||||
size: 10,
|
||||
args: {
|
||||
color: '#939AB1', // 网点颜色
|
||||
thickness: 1, // 网点大小
|
||||
color: '#939AB1', // Grid dot color
|
||||
thickness: 1, // Grid dot size
|
||||
}
|
||||
},
|
||||
panning: isHandMode,
|
||||
@@ -649,32 +731,32 @@ export const useWorkflowGraph = ({
|
||||
validateConnection({ sourceCell, targetCell, targetMagnet }) {
|
||||
if (!targetMagnet) return false;
|
||||
|
||||
// 节点不能与自己连线
|
||||
// Node cannot connect to itself
|
||||
if (sourceCell?.id === targetCell?.id) return false;
|
||||
|
||||
const sourceType = sourceCell?.getData()?.type;
|
||||
const targetType = targetCell?.getData()?.type;
|
||||
|
||||
// 开始节点不能作为连线的终点
|
||||
// Start node cannot be connection target
|
||||
if (targetType === 'start') return false;
|
||||
|
||||
// 结束节点不能作为连线的起点
|
||||
// End node cannot be connection source
|
||||
if (sourceType === 'end') return false;
|
||||
|
||||
// 获取源节点和目标节点的父节点ID
|
||||
// Get source node and target node parent IDs
|
||||
const sourceParentId = sourceCell?.getData()?.cycle;
|
||||
const targetParentId = targetCell?.getData()?.cycle;
|
||||
|
||||
// 验证父子节点关系:
|
||||
// 1. 如果两个节点都有父节点ID,必须相同才能连线
|
||||
// 2. 如果两个都没有父节点ID,可以正常连线
|
||||
// 3. 如果一个有父节点,一个没有,不能连线
|
||||
// Validate parent-child relationship:
|
||||
// 1. If both nodes have parent IDs, they must be same to connect
|
||||
// 2. If both have no parent ID, can connect normally
|
||||
// 3. If one has parent, one doesn't, cannot connect
|
||||
console.log('sourceParentId', sourceParentId, targetParentId)
|
||||
if (sourceParentId && targetParentId) {
|
||||
// 同一父节点下的子节点可以互相连线
|
||||
// Child nodes under same parent can connect to each other
|
||||
return sourceParentId === targetParentId;
|
||||
} else if (sourceParentId || targetParentId) {
|
||||
// 一个有父节点,一个没有,不能连线
|
||||
// One has parent, one doesn't, cannot connect
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -710,26 +792,26 @@ export const useWorkflowGraph = ({
|
||||
},
|
||||
},
|
||||
});
|
||||
// 使用插件
|
||||
// Use plugins
|
||||
setupPlugins();
|
||||
// 监听连线mouseleave事件
|
||||
// Listen to edge mouseleave event
|
||||
graphRef.current.on('edge:mouseleave', ({ edge }: { edge: Edge }) => {
|
||||
if (edge.getAttrByPath('line/stroke') !== edge_selected_color) {
|
||||
edge.setAttrByPath('line/stroke', edge_color);
|
||||
edge.setAttrByPath('line/strokeWidth', 1);
|
||||
}
|
||||
});
|
||||
// 监听节点选择事件
|
||||
// Listen to node selection event
|
||||
graphRef.current.on('node:click', nodeClick);
|
||||
// 监听连线选择事件
|
||||
// Listen to edge selection event
|
||||
graphRef.current.on('edge:click', edgeClick);
|
||||
// 监听连接桩点击事件
|
||||
// Listen to port click event
|
||||
graphRef.current.on('node:port:click', ({ e, node, port }: { e: MouseEvent, node: Node, port: string }) => {
|
||||
e.stopPropagation();
|
||||
const portElement = e.target as HTMLElement;
|
||||
const rect = portElement.getBoundingClientRect();
|
||||
|
||||
// 创建临时的popover触发元素
|
||||
// Create temporary popover trigger element
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.style.position = 'fixed';
|
||||
tempDiv.style.left = rect.left + 'px';
|
||||
@@ -739,23 +821,23 @@ export const useWorkflowGraph = ({
|
||||
tempDiv.style.zIndex = '9999';
|
||||
document.body.appendChild(tempDiv);
|
||||
|
||||
// 触发自定义事件来显示节点选择popover
|
||||
// Trigger custom event to show node selection popover
|
||||
const customEvent = new CustomEvent('port:click', {
|
||||
detail: { node, port, element: tempDiv, rect }
|
||||
});
|
||||
window.dispatchEvent(customEvent);
|
||||
});
|
||||
// 监听画布点击事件,取消选择
|
||||
// Listen to canvas click event, cancel selection
|
||||
graphRef.current.on('blank:click', blankClick);
|
||||
// 监听缩放事件
|
||||
// Listen to zoom event
|
||||
graphRef.current.on('scale', scaleEvent);
|
||||
// 监听节点移动事件
|
||||
// Listen to node move event
|
||||
graphRef.current.on('node:moved', nodeMoved);
|
||||
// 监听复制键盘事件
|
||||
// Listen to copy keyboard event
|
||||
graphRef.current.bindKey(['ctrl+c', 'cmd+c'], copyEvent);
|
||||
// 监听粘贴键盘事件
|
||||
// Listen to paste keyboard event
|
||||
graphRef.current.bindKey(['ctrl+v', 'cmd+v'], parseEvent);
|
||||
// 删除选中的节点和连线
|
||||
// Delete selected nodes and edges
|
||||
graphRef.current.bindKey(['ctrl+d', 'cmd+d', 'delete', 'backspace'], deleteEvent);
|
||||
|
||||
};
|
||||
@@ -771,6 +853,11 @@ export const useWorkflowGraph = ({
|
||||
};
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Handle node drop event from drag-and-drop
|
||||
* Creates new node at drop position
|
||||
* @param event - React drag event
|
||||
*/
|
||||
const onDrop = (event: React.DragEvent) => {
|
||||
if (!graphRef.current) return;
|
||||
event.preventDefault();
|
||||
@@ -780,13 +867,13 @@ export const useWorkflowGraph = ({
|
||||
|
||||
const point = graphRef.current.clientToLocal(event.clientX, event.clientY);
|
||||
|
||||
// 获取节点库中的原始配置,避免config数据串联
|
||||
// Get original config from node library to avoid config data chaining
|
||||
let nodeLibraryConfig = [...nodeLibrary]
|
||||
.flatMap(category => category.nodes)
|
||||
.find(n => n.type === dragData.type);
|
||||
nodeLibraryConfig = JSON.parse(JSON.stringify({ config: {}, ...nodeLibraryConfig })) as NodeProperties
|
||||
|
||||
// 创建干净的节点数据,只保留必要的字段
|
||||
// Create clean node data, only keep necessary fields
|
||||
const cleanNodeData = {
|
||||
id: `${dragData.type.replace(/-/g, '_')}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
name: t(`workflow.${dragData.type}`),
|
||||
@@ -802,7 +889,7 @@ export const useWorkflowGraph = ({
|
||||
data: { ...cleanNodeData, isGroup: true },
|
||||
});
|
||||
} else if (dragData.type === 'if-else') {
|
||||
// 创建条件节点
|
||||
// Create condition node
|
||||
graphRef.current.addNode({
|
||||
...graphNodeLibrary[dragData.type],
|
||||
x: point.x - 100,
|
||||
@@ -811,7 +898,7 @@ export const useWorkflowGraph = ({
|
||||
data: { ...cleanNodeData },
|
||||
});
|
||||
} else {
|
||||
// 普通节点创建,不支持拖拽到循环节点内
|
||||
// Normal node creation, does not support dragging into loop node
|
||||
graphRef.current.addNode({
|
||||
...(graphNodeLibrary[dragData.type] || graphNodeLibrary.default),
|
||||
x: point.x - 60,
|
||||
@@ -821,7 +908,12 @@ export const useWorkflowGraph = ({
|
||||
});
|
||||
}
|
||||
};
|
||||
// 保存workflow配置
|
||||
/**
|
||||
* Save workflow configuration to backend
|
||||
* Serializes graph state (nodes, edges, variables) and sends to API
|
||||
* @param flag - Whether to show success message (default: true)
|
||||
* @returns Promise that resolves when save is complete
|
||||
*/
|
||||
const handleSave = (flag = true) => {
|
||||
if (!graphRef.current || !config) return Promise.resolve()
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -869,6 +961,18 @@ export const useWorkflowGraph = ({
|
||||
})
|
||||
}
|
||||
itemConfig[key] = group_variables
|
||||
} else if (data.config[key] && 'defaultValue' in data.config[key] && key === 'group_type') {
|
||||
let group = data.config.group.defaultValue
|
||||
let group_type = group ? {} : data.config[key].defaultValue
|
||||
let group_variables = data.config.group_variables.defaultValue
|
||||
|
||||
if (group) {
|
||||
group_variables.forEach((item: any, index: number) => {
|
||||
group_type[item.key] = data.config[key].defaultValue[index] || data.config[key].defaultValue[item.key]
|
||||
})
|
||||
}
|
||||
|
||||
itemConfig[key] = group_type
|
||||
} else if (data.type === 'http-request' && (key === 'headers' || key === 'params') && data.config[key] && 'defaultValue' in data.config[key]) {
|
||||
const value = data.config[key].defaultValue
|
||||
itemConfig[key] = {}
|
||||
@@ -897,7 +1001,7 @@ export const useWorkflowGraph = ({
|
||||
id: data.id || node.id,
|
||||
type: data.type,
|
||||
name: data.name,
|
||||
cycle: data.cycle, // 保存cycle参数
|
||||
cycle: data.cycle, // Save cycle parameter
|
||||
position: {
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
@@ -910,13 +1014,13 @@ export const useWorkflowGraph = ({
|
||||
const targetCell = graphRef.current?.getCellById(edge.getTargetCellId());
|
||||
const sourcePortId = edge.getSourcePortId();
|
||||
|
||||
// 过滤无效连线:源节点或目标节点不存在,或者是add-node类型
|
||||
// Filter invalid edges: source or target node doesn't exist, or is add-node type
|
||||
if (!sourceCell?.getData()?.id || !targetCell?.getData()?.id ||
|
||||
sourceCell?.getData()?.type === 'add-node' || targetCell?.getData()?.type === 'add-node') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 如果是if-else节点的右侧端口连线,添加label
|
||||
// If if-else node right port connection, add label
|
||||
if (sourceCell?.getData()?.type === 'if-else' && sourcePortId?.startsWith('CASE')) {
|
||||
return {
|
||||
source: sourceCell.getData().id,
|
||||
@@ -925,7 +1029,7 @@ export const useWorkflowGraph = ({
|
||||
};
|
||||
}
|
||||
|
||||
// 如果是question-classifier节点的右侧端口连线,添加label
|
||||
// If question-classifier node right port connection, add label
|
||||
if (sourceCell?.getData()?.type === 'question-classifier' && sourcePortId?.startsWith('CASE')) {
|
||||
return {
|
||||
source: sourceCell.getData().id,
|
||||
@@ -934,7 +1038,7 @@ export const useWorkflowGraph = ({
|
||||
};
|
||||
}
|
||||
|
||||
// 如果是http-request节点的右侧端口连线,添加label
|
||||
// If http-request node right port connection, add label
|
||||
if (sourceCell?.getData()?.type === 'http-request') {
|
||||
if (sourcePortId === 'ERROR') {
|
||||
return {
|
||||
@@ -958,7 +1062,7 @@ export const useWorkflowGraph = ({
|
||||
})
|
||||
.filter(edge => edge !== null)
|
||||
.filter((edge, index, arr) => {
|
||||
// 去重:对于if-else和question-classifier节点,不同连接桩允许连接到相同节点
|
||||
// Deduplication: For if-else and question-classifier nodes, different ports can connect to same node
|
||||
return arr.findIndex(e => {
|
||||
if (!e || !edge) return false;
|
||||
const sourceCell = graphRef.current?.getCellById(e.source);
|
||||
@@ -966,10 +1070,10 @@ export const useWorkflowGraph = ({
|
||||
const isMultiPortNode = sourceType === 'question-classifier' || sourceType === 'if-else';
|
||||
|
||||
if (isMultiPortNode) {
|
||||
// 多端口节点需要同时比较source、target和label
|
||||
// Multi-port nodes need to compare source, target and label
|
||||
return e.source === edge.source && e.target === edge.target && e.label === edge.label;
|
||||
} else {
|
||||
// 其他节点只比较source和target
|
||||
// Other nodes only compare source and target
|
||||
return e.source === edge.source && e.target === edge.target;
|
||||
}
|
||||
}) === index;
|
||||
|
||||
Reference in New Issue
Block a user