feat(knowledge): refactor knowledge creation form and improve form handling

This commit is contained in:
2025-10-16 18:52:20 +08:00
parent f4e2f4f10c
commit 50628816e3
5 changed files with 312 additions and 150 deletions

4
.env
View File

@@ -1,5 +1,5 @@
# VITE_API_BASE_URL = http://150.158.121.95 VITE_API_BASE_URL = http://150.158.121.95
VITE_API_BASE_URL = http://154.9.253.114:9380 # VITE_API_BASE_URL = http://154.9.253.114:9380
VITE_RSA_PUBLIC_KEY="-----BEGIN PUBLIC KEY----- VITE_RSA_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArq9XTUSeYr2+N1h3Afl/z8Dse/2yD0ZGrKwx+EEEcdsBLca9Ynmx3nIB5obmLlSfmskLpBo0UACBmB5rEjBp2Q2f3AG3Hjd4B+gNCG6BDaawuDlgANIhGnaTLrIqWrrcm4EMzJOnAOI1fgzJRsOOUEfaS318Eq9OVO3apEyCCt0lOQK6PuksduOjVxtltDav+guVAA068NrPYmRNabVKRNLJpL8w4D44sfth5RvZ3q9t+6RTArpEtc5sh5ChzvqPOzKGMXW83C95TxmXqpbK6olN4RevSfVjEAgCydH6HN6OhtOQEcnrU97r9H0iZOWwbw3pVrZiUkuRD1R56Wzs2wIDAQAB MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArq9XTUSeYr2+N1h3Afl/z8Dse/2yD0ZGrKwx+EEEcdsBLca9Ynmx3nIB5obmLlSfmskLpBo0UACBmB5rEjBp2Q2f3AG3Hjd4B+gNCG6BDaawuDlgANIhGnaTLrIqWrrcm4EMzJOnAOI1fgzJRsOOUEfaS318Eq9OVO3apEyCCt0lOQK6PuksduOjVxtltDav+guVAA068NrPYmRNabVKRNLJpL8w4D44sfth5RvZ3q9t+6RTArpEtc5sh5ChzvqPOzKGMXW83C95TxmXqpbK6olN4RevSfVjEAgCydH6HN6OhtOQEcnrU97r9H0iZOWwbw3pVrZiUkuRD1R56Wzs2wIDAQAB

View File

@@ -272,7 +272,9 @@ export const useKnowledgeOperations = () => {
* 更新知识库基础信息 * 更新知识库基础信息
* 包括名称、描述、语言等基本信息 * 包括名称、描述、语言等基本信息
*/ */
const updateKnowledgeBasicInfo = useCallback(async (data: IKnowledge) => { const updateKnowledgeBasicInfo = useCallback(async (data: {
kb_id: string;
} & Partial<IKnowledge>) => {
try { try {
setLoading(true); setLoading(true);
setError(null); setError(null);
@@ -300,10 +302,7 @@ export const useKnowledgeOperations = () => {
*/ */
const updateKnowledgeModelConfig = useCallback(async (data: { const updateKnowledgeModelConfig = useCallback(async (data: {
kb_id: string; kb_id: string;
embd_id?: string; } & Partial<IKnowledge>) => {
parser_config?: Partial<IParserConfig>;
parser_id?: string;
}) => {
try { try {
setLoading(true); setLoading(true);
setError(null); setError(null);

View File

@@ -1,8 +1,9 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { useFormContext, useWatch } from 'react-hook-form'; import { useFormContext, useWatch, type UseFormReturn } from 'react-hook-form';
import { import {
Box, Box,
Typography, Typography,
Button,
} from '@mui/material'; } from '@mui/material';
import { DOCUMENT_PARSER_TYPES, type DocumentParserType } from '@/constants/knowledge'; import { DOCUMENT_PARSER_TYPES, type DocumentParserType } from '@/constants/knowledge';
import { type IParserConfig } from '@/interfaces/database/knowledge'; import { type IParserConfig } from '@/interfaces/database/knowledge';
@@ -59,8 +60,45 @@ export interface ConfigFormData extends IParserConfig {
parser_id: DocumentParserType; parser_id: DocumentParserType;
} }
function ChunkMethodForm() { interface ChunkMethodFormProps {
const { control } = useFormContext(); form?: UseFormReturn;
onSubmit?: (data: any) => void;
isSubmitting?: boolean;
onCancel?: () => void;
submitButtonText?: string;
cancelButtonText?: string;
}
function ChunkMethodForm({
form: propForm,
onSubmit,
isSubmitting,
onCancel,
submitButtonText = '保存',
cancelButtonText = '取消',
}: ChunkMethodFormProps = {}) {
// 优先使用props传递的form否则使用FormProvider的context
let contextForm;
try {
contextForm = useFormContext();
} catch (error) {
contextForm = null;
}
const form = propForm || contextForm;
if (!form) {
console.error('ChunkMethodForm: No form context found. Component must be used within a FormProvider or receive a form prop.');
return (
<Box sx={{ p: 2, textAlign: 'center' }}>
<Typography color="error">
FormProvider中使用或传递form参数
</Typography>
</Box>
);
}
const { control } = form;
// 监听parser_id变化 // 监听parser_id变化
const parser_id = useWatch({ const parser_id = useWatch({
@@ -79,6 +117,28 @@ function ChunkMethodForm() {
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
{/* 动态配置内容 */} {/* 动态配置内容 */}
<ConfigurationComponent /> <ConfigurationComponent />
{/* 表单操作按钮 - 仅在有onSubmit回调时显示 */}
{onSubmit && (
<Box sx={{ mt: 4, display: 'flex', justifyContent: 'flex-end', gap: 2 }}>
{onCancel && (
<Button
variant="outlined"
onClick={onCancel}
disabled={isSubmitting}
>
{cancelButtonText}
</Button>
)}
<Button
variant="contained"
onClick={form ? form.handleSubmit(onSubmit) : undefined}
disabled={isSubmitting || !form}
>
{isSubmitting ? '提交中...' : submitButtonText}
</Button>
</Box>
)}
</Box> </Box>
); );
} }

View File

@@ -1,5 +1,5 @@
import React, { useRef } from 'react'; import React, { useRef } from 'react';
import { useFormContext, Controller } from 'react-hook-form'; import { useFormContext, Controller, type UseFormReturn } from 'react-hook-form';
import { import {
Box, Box,
Typography, Typography,
@@ -18,30 +18,69 @@ import {
Delete as DeleteIcon, Delete as DeleteIcon,
} from '@mui/icons-material'; } from '@mui/icons-material';
function GeneralForm() { interface GeneralFormProps {
const { control, watch, setValue } = useFormContext(); form?: UseFormReturn;
onSubmit?: (data: any) => void;
isSubmitting?: boolean;
onCancel?: () => void;
submitButtonText?: string;
cancelButtonText?: string;
}
function GeneralForm({
form: propForm,
onSubmit,
isSubmitting,
onCancel,
submitButtonText = '保存',
cancelButtonText = '取消',
}: GeneralFormProps = {}) {
// 优先使用props传递的form否则使用FormProvider的context
let contextForm: UseFormReturn | null = null;
try {
contextForm = useFormContext();
} catch (error) {
contextForm = null;
}
const form = propForm || contextForm;
if (!form) {
console.error('GeneralForm: No form context found. Component must be used within a FormProvider or receive a form prop.');
return (
<Box sx={{ p: 2, textAlign: 'center' }}>
<Typography color="error">
FormProvider中使用或传递form参数
</Typography>
</Box>
);
}
const { control, watch, setValue, handleSubmit } = form;
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const handleAvatarUpload = (event: React.ChangeEvent<HTMLInputElement>) => { const handleAvatarUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]; const file = event.target.files?.[0];
if (file) { if (file && form) {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
setValue('avatar', e.target?.result as string); form.setValue('avatar', e.target?.result as string);
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
} }
}; };
const handleAvatarDelete = () => { const handleAvatarDelete = () => {
setValue('avatar', undefined); if (form) {
form.setValue('avatar', undefined);
}
}; };
const handleAvatarClick = () => { const handleAvatarClick = () => {
fileInputRef.current?.click(); fileInputRef.current?.click();
}; };
const avatar = watch('avatar'); const avatar = watch('avatar', '');
return ( return (
<Box sx={{ p: 3 }}> <Box sx={{ p: 3 }}>
@@ -146,6 +185,28 @@ function GeneralForm() {
</Grid> </Grid>
</Grid> </Grid>
</Grid> </Grid>
{/* 表单操作按钮 - 仅在有onSubmit回调时显示 */}
{onSubmit && (
<Box sx={{ mt: 4, display: 'flex', justifyContent: 'flex-end', gap: 2 }}>
{onCancel && (
<Button
variant="outlined"
onClick={onCancel}
disabled={isSubmitting}
>
{cancelButtonText}
</Button>
)}
<Button
variant="contained"
onClick={form ? form.handleSubmit(onSubmit) : undefined}
disabled={isSubmitting || !form}
>
{isSubmitting ? '提交中...' : submitButtonText}
</Button>
</Box>
)}
</Box> </Box>
); );
} }

View File

@@ -1,6 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useForm, type UseFormReturn } from 'react-hook-form'; import { useForm, FormProvider, useWatch, Form } from 'react-hook-form';
import { import {
Box, Box,
Typography, Typography,
@@ -9,46 +9,29 @@ import {
Button, Button,
Alert, Alert,
Divider, Divider,
CircularProgress,
Stepper, Stepper,
Step, Step,
StepLabel, StepLabel,
} from '@mui/material'; } from '@mui/material';
import { import {
ArrowBack as ArrowBackIcon, ArrowBack as ArrowBackIcon,
Settings as SettingsIcon,
CheckCircle as CheckCircleIcon, CheckCircle as CheckCircleIcon,
SkipNext as SkipNextIcon, SkipNext as SkipNextIcon,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { useKnowledgeOperations } from '@/hooks/knowledge-hooks'; import { useKnowledgeOperations, useKnowledgeDetail } from '@/hooks/knowledge-hooks';
import GeneralForm from './components/GeneralForm'; import GeneralForm from './components/GeneralForm';
import ChunkMethodForm from './components/ChunkMethodForm'; import ChunkMethodForm from './components/ChunkMethodForm';
import { useSnackbar } from '@/components/Provider/SnackbarProvider';
import { DOCUMENT_PARSER_TYPES } from '@/constants/knowledge';
// 基础信息表单数据 // 统一表单数据类型
interface BasicFormData { interface BaseFormData {
name: string; name: string;
description: string; description: string;
permission: string; permission: string;
avatar?: string; avatar?: string;
} }
// 配置表单数据
interface ConfigFormData {
parser_id: string;
embd_id?: string;
chunk_token_num?: number;
layout_recognize?: string;
delimiter?: string;
auto_keywords?: number;
auto_questions?: number;
html4excel?: boolean;
topn_tags?: number;
use_raptor?: boolean;
use_graphrag?: boolean;
graphrag_method?: string;
pagerank?: number;
}
const steps = ['基础信息', '配置设置']; const steps = ['基础信息', '配置设置'];
function KnowledgeBaseCreate() { function KnowledgeBaseCreate() {
@@ -65,77 +48,111 @@ function KnowledgeBaseCreate() {
clearError clearError
} = useKnowledgeOperations(); } = useKnowledgeOperations();
// 基础信息表单 const { showMessage } = useSnackbar();
const basicForm = useForm<BasicFormData>({
// 获取知识库详情的hook
const { knowledge: knowledgeDetail, refresh: refreshKnowledgeDetail } = useKnowledgeDetail(createdKbId || '');
// 统一表单管理
const form = useForm<any>({
defaultValues: { defaultValues: {
name: '', name: '',
description: '', description: '',
permission: 'me', permission: 'me',
avatar: '', avatar: undefined,
}, parser_id: DOCUMENT_PARSER_TYPES.Naive,
});
// 配置表单
const configForm = useForm<ConfigFormData>({
defaultValues: {
parser_id: 'naive',
embd_id: 'text-embedding-v3@Tongyi-Qianwen', embd_id: 'text-embedding-v3@Tongyi-Qianwen',
parser_config: {
chunk_token_num: 512, chunk_token_num: 512,
layout_recognize: 'DeepDOC',
delimiter: '\n', delimiter: '\n',
auto_keywords: 0, auto_keywords: 0,
auto_questions: 0, auto_questions: 0,
html4excel: false, html4excel: false,
topn_tags: 10, topn_tags: 10,
raptor: {
use_raptor: false, use_raptor: false,
prompt: '请总结以下段落。小心数字,不要编造。段落如下:\n {cluster_content}\n以上就是你需要总结的内容。',
max_token: 256,
threshold: 0.1,
max_cluster: 64,
random_seed: 0,
},
graphrag: {
use_graphrag: false, use_graphrag: false,
graphrag_method: 'light', entity_types: ['organization', 'person', 'geo', 'event', 'category'],
pagerank: 0.3, method: 'light',
},
},
}, },
}); });
// 处理基础信息提交 // 处理表单提交
const handleBasicSubmit = async (data: BasicFormData) => { const handleSubmit = async ({ data }: { data: any }) => {
clearError(); clearError();
console.log('提交数据:', data);
try { try {
const result = await createKnowledge(data); if (activeStep === 0) {
setCreatedKbId(result.kb_id); // 第一步:创建知识库基础信息
setActiveStep(1); const basicData = {
} catch (err) { name: data.name,
console.error('创建知识库失败:', err); description: data.description,
} permission: data.permission,
avatar: data.avatar,
}; };
// 处理配置提交 const result = await createKnowledge(basicData);
const handleConfigSubmit = async (data: ConfigFormData) => { setCreatedKbId(result.kb_id);
// 获取创建后的知识库详情,确保获取到正确的名称
setTimeout(async () => {
await refreshKnowledgeDetail();
}, 500);
setActiveStep(1);
showMessage.success('知识库创建成功,请配置解析设置');
} else {
// 第二步:配置知识库解析设置
if (!createdKbId) return; if (!createdKbId) return;
clearError(); // 使用知识库详情中的正确名称,如果没有则使用表单中的名称
const correctName = knowledgeDetail?.name || data.name;
try { const configData = {
await updateKnowledgeModelConfig({ kb_id: createdKbId,
id: createdKbId, name: correctName, // 使用正确的名称
...data, description: data.description,
}); permission: data.permission,
avatar: data.avatar,
parser_id: data.parser_id,
embd_id: data.embd_id,
parser_config: data.parser_config,
};
await updateKnowledgeModelConfig(configData);
showMessage.success('知识库配置完成');
navigate('/knowledge'); navigate('/knowledge');
}
} catch (err) { } catch (err) {
console.error('配置知识库失败:', err); console.error('操作失败:', err);
showMessage.error(activeStep === 0 ? '创建知识库失败' : '配置知识库失败');
} }
}; };
// 跳过配置
const handleSkipConfig = () => {
navigate('/knowledge');
};
// 返回上一步 // 跳过配置,直接完成创建
const handleBack = () => { const handleSkipConfig = async () => {
setActiveStep(0); try {
showMessage.success('知识库创建完成,您可以稍后在设置页面配置解析参数');
navigate('/knowledge');
} catch (err) {
console.error('跳过配置失败:', err);
showMessage.error('操作失败');
}
}; };
return ( return (
<Box sx={{ maxWidth: 800, mx: 'auto', p: 3 }}> <Box sx={{ p: 3 }}>
{/* 页面标题 */} {/* 页面头部 */}
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
<Button <Button
startIcon={<ArrowBackIcon />} startIcon={<ArrowBackIcon />}
@@ -150,13 +167,17 @@ function KnowledgeBaseCreate() {
</Box> </Box>
{/* 步骤指示器 */} {/* 步骤指示器 */}
<Stepper activeStep={activeStep} sx={{ mb: 4 }}> <Card sx={{ mb: 3 }}>
<CardContent>
<Stepper activeStep={activeStep} alternativeLabel>
{steps.map((label) => ( {steps.map((label) => (
<Step key={label}> <Step key={label}>
<StepLabel>{label}</StepLabel> <StepLabel>{label}</StepLabel>
</Step> </Step>
))} ))}
</Stepper> </Stepper>
</CardContent>
</Card>
{/* 错误提示 */} {/* 错误提示 */}
{error && ( {error && (
@@ -165,22 +186,14 @@ function KnowledgeBaseCreate() {
</Alert> </Alert>
)} )}
{/* 表单内容 */}
<FormProvider {...form}>
<Form onSubmit={handleSubmit}>
<Card> <Card>
<CardContent> <CardContent>
{/* 第一步:基础信息 */} {activeStep === 0 ? (
{activeStep === 0 && ( <GeneralForm form={form} />
<GeneralForm ) : (
form={basicForm}
onSubmit={handleBasicSubmit}
isSubmitting={isSubmitting}
onCancel={() => navigate('/knowledge')}
submitButtonText="下一步"
cancelButtonText="取消"
/>
)}
{/* 第二步:配置设置 */}
{activeStep === 1 && (
<Box> <Box>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
<CheckCircleIcon color="success" sx={{ mr: 1 }} /> <CheckCircleIcon color="success" sx={{ mr: 1 }} />
@@ -192,31 +205,60 @@ function KnowledgeBaseCreate() {
</Typography> </Typography>
<Divider sx={{ mb: 3 }} /> <Divider sx={{ mb: 3 }} />
<ChunkMethodForm form={form} />
</Box>
)}
<ChunkMethodForm <Divider sx={{ my: 3 }} />
form={configForm as any}
onSubmit={(data) => handleConfigSubmit(data as any)}
isSubmitting={isSubmitting}
onCancel={handleBack}
submitButtonText="完成配置"
cancelButtonText="返回上一步"
/>
{/* 跳过配置按钮 */} {/* 操作按钮 */}
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 2 }}> <Box sx={{ display: 'flex', justifyContent: 'flex-end' }}>
<Box sx={{ display: 'flex', gap: 2 }}>
{activeStep === 0 && (
<Button <Button
variant="text" variant="outlined"
startIcon={<SkipNextIcon />} onClick={() => navigate('/knowledge')}
>
</Button>
)}
{/* 第二步显示跳过配置按钮 */}
{activeStep === 1 && (
<Button
variant="outlined"
onClick={handleSkipConfig} onClick={handleSkipConfig}
disabled={isSubmitting} disabled={isSubmitting}
startIcon={<SkipNextIcon />}
> >
</Button>
)}
<Button
type="submit"
variant="contained"
disabled={isSubmitting}
startIcon={
activeStep === steps.length - 1 ? (
<CheckCircleIcon />
) : (
<SkipNextIcon />
)
}
>
{isSubmitting
? '处理中...'
: activeStep === steps.length - 1
? '完成创建'
: '下一步'}
</Button> </Button>
</Box> </Box>
</Box> </Box>
)}
</CardContent> </CardContent>
</Card> </Card>
</Form>
</FormProvider>
</Box> </Box>
); );
} }