import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'; import { Form, Input, Select, Modal, Tabs, Switch, Radio, Button, message } from 'antd'; import { useTranslation } from 'react-i18next'; import type { KnowledgeBaseListItem, KnowledgeBaseFormData, CreateModalRef, CreateModalRefProps } from '@/views/KnowledgeBase/types'; import { getModelTypeList, getModelList, createKnowledgeBase, updateKnowledgeBase, getKnowledgeGraphEntityTypes, deleteKnowledgeGraph, rebuildKnowledgeGraph, checkFeishuSync, checkYuqueSync } from '@/api/knowledgeBase' import RbModal from '@/components/RbModal' import SliderInput from '@/components/SliderInput' const { TextArea } = Input; const { confirm } = Modal // Global model data constant let models: any = null; const CreateModal = forwardRef(({ refreshTable }, ref) => { const { t } = useTranslation(); const [messageApi, contextHolder] = message.useMessage(); const [visible, setVisible] = useState(false); const [modelTypeList, setModelTypeList] = useState([]); const [modelOptionsByType, setModelOptionsByType] = useState>({}); const [datasets, setDatasets] = useState(null); const [currentType, setCurrentType] = useState<'General' | 'Web' | 'Third-party' | 'Folder'>('General'); const [thirdPartyPlatform, setThirdPartyPlatform] = useState<'yuque' | 'feishu'>('yuque'); const [form] = Form.useForm(); const [loading, setLoading] = useState(false); const [activeTab, setActiveTab] = useState('basic'); const [generatingEntityTypes, setGeneratingEntityTypes] = useState(false); const [isRebuildMode, setIsRebuildMode] = useState(false); const [originalType, setOriginalType] = useState(''); // Save original type parameter // 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; const entityTypes = graphragConfig?.entity_types || ''; const entityNormalization = graphragConfig?.resolution || false; const communityReportGeneration = graphragConfig?.community || false; // Watch for changes to _third_party_platform field directly const formThirdPartyPlatform = Form.useWatch(['parser_config', '_third_party_platform'], form); // Sync form value to state when form value changes useEffect(() => { if (formThirdPartyPlatform && (formThirdPartyPlatform === 'yuque' || formThirdPartyPlatform === 'feishu')) { setThirdPartyPlatform(formThirdPartyPlatform); } }, [formThirdPartyPlatform]); // Encapsulate cancel method, add close modal logic const handleClose = () => { setDatasets(null); form.resetFields(); setLoading(false); setActiveTab('basic'); setIsRebuildMode(false); // Reset rebuild mode flag setOriginalType(''); // Reset original type setThirdPartyPlatform('yuque'); // Reset third party platform 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; } // 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; } setGeneratingEntityTypes(true); try { // 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 = { scenario: sceneName, llm_id: llmId }; const response = await getKnowledgeGraphEntityTypes(params); // Simulate API call // await new Promise(resolve => setTimeout(resolve, 1000)); // Process API response data console.log('API Response:', response); // Debug log // Check response structure - API returns string directly if (response && typeof response === 'string' && response.trim()) { // Convert comma-separated string to newline-separated format for TextArea display const entityTypesString = response.replace(/,\s*/g, '\n'); console.log('Converted entity types:', entityTypesString); // Debug log const currentGraphrag = form.getFieldValue(['parser_config', 'graphrag']) || {}; const updatedGraphrag = { ...currentGraphrag, entity_types: entityTypesString }; 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')); } } catch (error) { console.error(t('knowledgeBase.generateEntityTypesFailed') + ':', error); } finally { setGeneratingEntityTypes(false); } }; const typeToFieldKey = (type: string): string => { switch ((type || '').toLowerCase()) { case 'embedding': return 'embedding_id'; case 'llm': return 'llm_id'; case 'image2text': return 'image2text_id'; case 'rerank': case 'reranker': return 'reranker_id'; case 'chat': return 'chat_id'; default: return `${type.toLowerCase()}_id`; } }; 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 }); } catch (error) { console.error('Failed to fetch models:', error); models = { items: [] }; } } // Filter out the required types from all model data const typesToFetch = types.includes('llm') ? [...types, 'chat'] : types; const next: Record = {}; typesToFetch.forEach((tp) => { const targetType = tp === 'image2text' ? 'chat' : tp; const filteredModels = (models?.items || []).filter((m: any) => m.type === targetType); next[tp] = filteredModels.map((m: any) => ({ label: m.name, value: m.id })); }); setModelOptionsByType(next); // If not in edit mode, set default value to first item for each type dropdown if (!datasets?.id) { const defaultValues: Record = {}; types.forEach((tp) => { const fieldKey = typeToFieldKey(tp); const options = tp.toLowerCase() === 'llm' ? [...(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 as any)) { defaultValues[fieldKey] = options[0].value; } }); if (Object.keys(defaultValues).length > 0) { form.setFieldsValue(defaultValues as any); } } }; const setBaseFields = (record: KnowledgeBaseListItem | null, type?: string) => { if (!record) { form.resetFields(); const defaults: Partial = { permission_id: 'Private', type: type || currentType, }; form.setFieldsValue(defaults); // Reset third party platform to default when creating new setThirdPartyPlatform('yuque'); return; } const baseValues: Partial = { name: record.name, description: record.description, permission_id: record.permission_id || 'Private', type: type || record.type || currentType, status: record.status, }; // Process parser_config data, set default values if not present baseValues.parser_config = { ...record.parser_config, graphrag: { use_graphrag: false, scene_name: '', entity_types: [] as any, method: 'general', resolution: false, community: false, ...(record.parser_config?.graphrag || {}) } }; // 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'); } } // Set form values first form.setFieldsValue(baseValues); // Then sync third party platform state from form value // This ensures the state matches what's actually in the form const platform = baseValues.parser_config?._third_party_platform; if (platform === 'yuque' || platform === 'feishu') { setThirdPartyPlatform(platform); } else { // Reset to default if no platform specified setThirdPartyPlatform('yuque'); } }; const setDynamicModelFields = (record: KnowledgeBaseListItem | null, types: string[]) => { if (!record || !types.length) return; const dynamicValues: Record = {}; const source = record as unknown as Record; types.forEach((tp) => { const fieldKey = typeToFieldKey(tp); const fieldValue = source[fieldKey]; if (typeof fieldValue === 'string') { dynamicValues[fieldKey] = fieldValue; } }); if (Object.keys(dynamicValues).length) { form.setFieldsValue(dynamicValues as Partial); } }; const handleOpen = (record?: KnowledgeBaseListItem | null, type?: string) => { setDatasets(record || null); // If rebuild mode, use record's actual type, otherwise use passed type // If editing (record exists but no type passed), use record's type const actualType = type === 'rebuild' ? (record?.type || 'General') : (type || record?.type || currentType); setCurrentType(actualType as any); setIsRebuildMode(type === 'rebuild'); // Set rebuild mode flag setOriginalType(type || ''); // Save original type parameter // Note: third party platform state will be set in setBaseFields function // No need to set it here separately to avoid inconsistency // If rebuild mode, default to knowledge graph tab if (type === 'rebuild') { setActiveTab('knowledgeGraph'); } else { setActiveTab('basic'); } setBaseFields(record || null, actualType); getTypeList(record || null); setVisible(true); }; const getTypeList = async (record: KnowledgeBaseListItem | null) => { const response = await getModelTypeList(); const types = Array.isArray(response) ? [...response.filter(type => type !== 'chat'),'image2text'] : []; setModelTypeList(types); if (types.length) { await fetchModelLists(types); setDynamicModelFields(record, types); } else { setModelOptionsByType({}); } }; useEffect(() => { if (!visible) return; // Only set fields when modal becomes visible, not on every state change // setBaseFields is already called in handleOpen // This useEffect is mainly for syncing dynamic model fields if (datasets && modelTypeList.length > 0) { setDynamicModelFields(datasets, modelTypeList); } }, [visible, 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; // If original type is 'rebuild' and knowledge graph is enabled, show confirmation dialog if (originalType === 'rebuild' && isGraphragEnabled) { confirm({ title: t('knowledgeBase.rebuildConfirmTitle'), content: t('knowledgeBase.rebuildConfirmContent'), onOk: async() => { handleDeleteGraph() performSave(); await rebuildKnowledgeGraph(datasets?.id || '') }, onCancel: () => { // User cancelled, no action taken }, }); } else { // Non-rebuild mode or knowledge graph not enabled, save directly performSave(); } }; const handleDeleteGraph = () => { try{ deleteKnowledgeGraph(datasets?.id || '') console.log(t('knowledgeBase.deleteGraphSuccess')) }catch(e){ messageApi.error(t('knowledgeBase.deleteGraphFailed')) } }; // Actual save logic const performSave = async () => { try { await form.validateFields(); setLoading(true); const formValues = form.getFieldsValue(); // Check Third-party authentication before saving if (formValues.type === 'Third-party' || currentType === 'Third-party') { const platform = formValues.parser_config?._third_party_platform || thirdPartyPlatform; try { if (platform === 'yuque') { // Validate Yuque credentials const yuqueParams = { yuque_user_id: formValues.parser_config?.yuque_user_id, yuque_token: formValues.parser_config?.yuque_token }; if (!yuqueParams.yuque_user_id || !yuqueParams.yuque_token) { messageApi.error(t('knowledgeBase.yuqueAuthRequired')); setLoading(false); return; } await checkYuqueSync(yuqueParams); messageApi.success(t('knowledgeBase.yuqueAuthSuccess')); } else if (platform === 'feishu') { // Validate Feishu credentials const feishuParams = { feishu_app_id: formValues.parser_config?.feishu_app_id, feishu_app_secret: formValues.parser_config?.feishu_app_secret, feishu_folder_token: formValues.parser_config?.feishu_folder_token }; if (!feishuParams.feishu_app_id || !feishuParams.feishu_app_secret || !feishuParams.feishu_folder_token) { messageApi.error(t('knowledgeBase.feishuAuthRequired')); setLoading(false); return; } await checkFeishuSync(feishuParams); messageApi.success(t('knowledgeBase.feishuAuthSuccess')); } } catch (error) { console.error('Authentication failed:', error); messageApi.error(t('knowledgeBase.authFailed')); setLoading(false); return; } } // 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 .split('\n') .map((item: string) => item.trim()) .filter((item: string) => item.length > 0); formValues.parser_config.graphrag.entity_types = entityTypesArray; } // Ensure correct type is used when saving (not 'rebuild') const saveType = originalType === 'rebuild' ? currentType : (formValues.type || currentType); const payload: KnowledgeBaseFormData = { ...formValues, type: saveType, permission_id: formValues.permission_id || 'Private', parent_id: datasets?.parent_id || undefined, }; console.log('Saving payload:', payload); // Debug log const submit = datasets?.id ? updateKnowledgeBase(datasets.id, payload) : createKnowledgeBase(payload); await submit; if (refreshTable) { refreshTable(); } handleClose(); } catch (err) { console.log('Validation or save failed:', err); setLoading(false); } } const handleChange = (_value: string, tp: string) => { // Only trigger prompt in edit mode and when type is embedding if (datasets?.id && tp.toLowerCase() === 'embedding') { const fieldKey = typeToFieldKey(tp); // 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 })); // Get title based on type const getTitle = () => { if (isRebuildMode) { return t('knowledgeBase.rebuildGraph') + ' - ' + (datasets?.name || ''); } if (datasets?.id) { return t('knowledgeBase.edit') + ' ' + datasets.name; } if (currentType === 'Folder') { return t('knowledgeBase.createA') + ' ' + t('knowledgeBase.folder'); } return t('knowledgeBase.createA') + ' ' + t('knowledgeBase.knowledgeBase'); }; const dynamicTypeList = useMemo(() => modelTypeList.filter((tp) => (modelOptionsByType[tp] || []).length), [modelTypeList, modelOptionsByType]); // Basic configuration form content const renderBasicConfig = () => ( <> {!datasets?.id && ( )}