feat(knowledge): add configuration components and refactor general form
feat(hooks): add llm-related hooks and constants docs: add architecture analysis for reference projects
This commit is contained in:
@@ -1,539 +1,84 @@
|
||||
import React from 'react';
|
||||
import { type UseFormReturn } from 'react-hook-form';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useFormContext, useWatch } from 'react-hook-form';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
Grid,
|
||||
TextField,
|
||||
FormHelperText,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Switch,
|
||||
FormControlLabel,
|
||||
Chip,
|
||||
Slider,
|
||||
Accordion,
|
||||
AccordionSummary,
|
||||
AccordionDetails,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Save as SaveIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
Add as AddIcon,
|
||||
} from '@mui/icons-material';
|
||||
import { DOCUMENT_PARSER_TYPES, type DocumentParserType } from '@/constants/knowledge';
|
||||
import { type IParserConfig } from '@/interfaces/database/knowledge';
|
||||
import {
|
||||
NaiveConfiguration,
|
||||
QAConfiguration,
|
||||
PaperConfiguration,
|
||||
ResumeConfiguration,
|
||||
ManualConfiguration,
|
||||
TableConfiguration,
|
||||
BookConfiguration,
|
||||
LawsConfiguration,
|
||||
PresentationConfiguration,
|
||||
OneConfiguration,
|
||||
TagConfiguration,
|
||||
ChunkMethodItem,
|
||||
} from '../configuration';
|
||||
|
||||
/**
|
||||
{
|
||||
"kb_id": "dcc2871aa4cd11f08d4116ac85b1de0a",
|
||||
"name": "k1123",
|
||||
"description": " 213213",
|
||||
"permission": "team",
|
||||
"parser_id": "naive",
|
||||
"embd_id": "",
|
||||
"parser_config": {
|
||||
"layout_recognize": "Plain Text",
|
||||
"chunk_token_num": 512,
|
||||
"delimiter": "\n",
|
||||
"auto_keywords": 0,
|
||||
"auto_questions": 0,
|
||||
"html4excel": false,
|
||||
"topn_tags": 3,
|
||||
"raptor": {
|
||||
"use_raptor": true,
|
||||
"prompt": "请总结以下段落。 小心数字,不要编造。 段落如下:\n {cluster_content}\n以上就是你需要总结的内容。",
|
||||
"max_token": 256,
|
||||
"threshold": 0.1,
|
||||
"max_cluster": 64,
|
||||
"random_seed": 0
|
||||
},
|
||||
"graphrag": {
|
||||
"use_graphrag": true,
|
||||
"entity_types": [
|
||||
"organization",
|
||||
"person",
|
||||
"geo",
|
||||
"event",
|
||||
"category"
|
||||
],
|
||||
"method": "light"
|
||||
}
|
||||
},
|
||||
"pagerank": 0
|
||||
// 配置组件映射表
|
||||
const ConfigurationComponentMap = {
|
||||
[DOCUMENT_PARSER_TYPES.Naive]: NaiveConfiguration,
|
||||
[DOCUMENT_PARSER_TYPES.Qa]: QAConfiguration,
|
||||
[DOCUMENT_PARSER_TYPES.Resume]: ResumeConfiguration,
|
||||
[DOCUMENT_PARSER_TYPES.Manual]: ManualConfiguration,
|
||||
[DOCUMENT_PARSER_TYPES.Table]: TableConfiguration,
|
||||
[DOCUMENT_PARSER_TYPES.Paper]: PaperConfiguration,
|
||||
[DOCUMENT_PARSER_TYPES.Book]: BookConfiguration,
|
||||
[DOCUMENT_PARSER_TYPES.Laws]: LawsConfiguration,
|
||||
[DOCUMENT_PARSER_TYPES.Presentation]: PresentationConfiguration,
|
||||
[DOCUMENT_PARSER_TYPES.One]: OneConfiguration,
|
||||
[DOCUMENT_PARSER_TYPES.Tag]: TagConfiguration,
|
||||
[DOCUMENT_PARSER_TYPES.KnowledgeGraph]: TagConfiguration,
|
||||
[DOCUMENT_PARSER_TYPES.Picture]: TagConfiguration,
|
||||
[DOCUMENT_PARSER_TYPES.Audio]: TagConfiguration,
|
||||
[DOCUMENT_PARSER_TYPES.Email]: TagConfiguration,
|
||||
// [DOCUMENT_PARSER_TYPES.KnowledgeGraph]: KnowledgeGraphConfiguration,
|
||||
// [DOCUMENT_PARSER_TYPES.Picture]: PictureConfiguration,
|
||||
// [DOCUMENT_PARSER_TYPES.Audio]: AudioConfiguration,
|
||||
// [DOCUMENT_PARSER_TYPES.Email]: GraphragConfiguration,
|
||||
};
|
||||
|
||||
// 空组件
|
||||
function EmptyComponent() {
|
||||
return (
|
||||
<Box sx={{ textAlign: 'center', py: 4 }}>
|
||||
<Typography color="text.secondary">
|
||||
请选择一个解析方法来配置相关参数
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
// 解析器选项配置
|
||||
const parserOptions = [
|
||||
{ value: DOCUMENT_PARSER_TYPES.Naive, label: '通用解析器', description: '适用于大多数文档类型' },
|
||||
{ value: DOCUMENT_PARSER_TYPES.Qa, label: 'Q&A解析器', description: '适用于问答格式文档' },
|
||||
{ value: DOCUMENT_PARSER_TYPES.Resume, label: '简历解析器', description: '专门用于解析简历文档' },
|
||||
{ value: DOCUMENT_PARSER_TYPES.Manual, label: '手册解析器', description: '适用于技术手册和说明书' },
|
||||
{ value: DOCUMENT_PARSER_TYPES.Table, label: '表格解析器', description: '专门处理表格数据' },
|
||||
{ value: DOCUMENT_PARSER_TYPES.Paper, label: '论文解析器', description: '适用于学术论文' },
|
||||
{ value: DOCUMENT_PARSER_TYPES.Book, label: '书籍解析器', description: '适用于书籍和长文档' },
|
||||
{ value: DOCUMENT_PARSER_TYPES.Laws, label: '法律解析器', description: '专门处理法律文档' },
|
||||
{ value: DOCUMENT_PARSER_TYPES.Presentation, label: '演示文稿解析器', description: '适用于PPT等演示文档' },
|
||||
{ value: DOCUMENT_PARSER_TYPES.Picture, label: '图片解析器', description: '处理图片中的文字内容' },
|
||||
{ value: DOCUMENT_PARSER_TYPES.One, label: '整体解析器', description: '将整个文档作为一个块处理' },
|
||||
{ value: DOCUMENT_PARSER_TYPES.Audio, label: '音频解析器', description: '处理音频文件转录' },
|
||||
{ value: DOCUMENT_PARSER_TYPES.Email, label: '邮件解析器', description: '专门处理邮件格式' },
|
||||
{ value: DOCUMENT_PARSER_TYPES.Tag, label: '标签解析器', description: '基于标签的文档解析' },
|
||||
{ value: DOCUMENT_PARSER_TYPES.KnowledgeGraph, label: '知识图谱解析器', description: '构建知识图谱结构' },
|
||||
];
|
||||
|
||||
export interface ConfigFormData extends IParserConfig {
|
||||
parser_id: DocumentParserType;
|
||||
}
|
||||
|
||||
interface ChunkMethodFormProps {
|
||||
form: UseFormReturn<ConfigFormData>;
|
||||
onSubmit: (data: ConfigFormData) => void;
|
||||
isSubmitting?: boolean;
|
||||
onCancel?: () => void;
|
||||
disabled?: boolean;
|
||||
submitButtonText?: string;
|
||||
cancelButtonText?: string;
|
||||
}
|
||||
function ChunkMethodForm() {
|
||||
const { control } = useFormContext();
|
||||
|
||||
function ChunkMethodForm({
|
||||
form,
|
||||
onSubmit,
|
||||
isSubmitting = false,
|
||||
onCancel,
|
||||
disabled = false,
|
||||
submitButtonText = '保存',
|
||||
cancelButtonText = '取消'
|
||||
}: ChunkMethodFormProps) {
|
||||
const selectedParser: DocumentParserType = form.watch('parser_id');
|
||||
const [entityTypes, setEntityTypes] = React.useState<string[]>(['organization', 'person', 'geo', 'event', 'category']);
|
||||
// 监听parser_id变化
|
||||
const parser_id = useWatch({
|
||||
control,
|
||||
name: 'parser_id',
|
||||
});
|
||||
|
||||
// 通用配置部分
|
||||
const renderGeneralConfig = () => (
|
||||
<Accordion defaultExpanded>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography variant="h6">通用</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid container spacing={3}>
|
||||
{/* 切片方法 */}
|
||||
<Grid size={12}>
|
||||
<FormControl fullWidth disabled={disabled}>
|
||||
<InputLabel>切片方法</InputLabel>
|
||||
<Select
|
||||
label="切片方法"
|
||||
defaultValue={DOCUMENT_PARSER_TYPES.Naive}
|
||||
{...form.register('parser_id')}
|
||||
value={form.watch('parser_id') || DOCUMENT_PARSER_TYPES.Naive}
|
||||
>
|
||||
{parserOptions.map((option) => (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
<Box>
|
||||
<Typography variant="body1">{option.label}</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{option.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<FormHelperText>选择适合您文档类型的解析器</FormHelperText>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
{/* PDF解析器 */}
|
||||
<Grid size={12}>
|
||||
<FormControl fullWidth disabled={disabled}>
|
||||
<InputLabel>PDF解析器</InputLabel>
|
||||
<Select
|
||||
label="PDF解析器"
|
||||
defaultValue="Naive"
|
||||
disabled={disabled}
|
||||
>
|
||||
<MenuItem value="Naive">Naive</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
{/* 嵌入模型 */}
|
||||
<Grid size={12}>
|
||||
<FormControl fullWidth disabled={disabled}>
|
||||
<InputLabel>嵌入模型</InputLabel>
|
||||
<Select
|
||||
label="嵌入模型"
|
||||
defaultValue=""
|
||||
disabled={disabled}
|
||||
>
|
||||
<MenuItem value="">请选择嵌入模型</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
{/* 建议文本块大小 */}
|
||||
<Grid size={12}>
|
||||
<Typography gutterBottom>建议文本块大小</Typography>
|
||||
<Box sx={{ px: 2 }}>
|
||||
<Slider
|
||||
defaultValue={512}
|
||||
min={50}
|
||||
max={2048}
|
||||
step={1}
|
||||
valueLabelDisplay="on"
|
||||
disabled={disabled}
|
||||
{...form.register('chunk_token_num', { valueAsNumber: true })}
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ ml: 2 }}>
|
||||
512
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
{/* 文本分段标识符 */}
|
||||
<Grid size={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="文本分段标识符"
|
||||
placeholder="\\n"
|
||||
disabled={disabled}
|
||||
{...form.register('delimiter')}
|
||||
helperText="用于分割文本的分隔符"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
|
||||
// 页面排名配置部分
|
||||
const renderPageRankConfig = () => (
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography variant="h6">页面排名</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid container spacing={3}>
|
||||
{/* 页面排名 */}
|
||||
<Grid size={12}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Typography>页面排名</Typography>
|
||||
<Switch disabled={disabled} />
|
||||
<TextField
|
||||
type="number"
|
||||
size="small"
|
||||
defaultValue={0}
|
||||
disabled={disabled}
|
||||
sx={{ width: 100 }}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* 自动关键词提取 */}
|
||||
<Grid size={12}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Typography>自动关键词提取</Typography>
|
||||
<Switch disabled={disabled} />
|
||||
<TextField
|
||||
type="number"
|
||||
size="small"
|
||||
defaultValue={0}
|
||||
disabled={disabled}
|
||||
sx={{ width: 100 }}
|
||||
{...form.register('auto_keywords', { valueAsNumber: true })}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* 自动问题提取 */}
|
||||
<Grid size={12}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Typography>自动问题提取</Typography>
|
||||
<Switch disabled={disabled} />
|
||||
<TextField
|
||||
type="number"
|
||||
size="small"
|
||||
defaultValue={0}
|
||||
disabled={disabled}
|
||||
sx={{ width: 100 }}
|
||||
{...form.register('auto_questions', { valueAsNumber: true })}
|
||||
/>
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* 表格转HTML */}
|
||||
<Grid size={12}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
disabled={disabled}
|
||||
{...form.register('html4excel')}
|
||||
/>
|
||||
}
|
||||
label="表格转HTML"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* 标签集 */}
|
||||
<Grid size={12}>
|
||||
<FormControl fullWidth disabled={disabled}>
|
||||
<InputLabel>标签集</InputLabel>
|
||||
<Select
|
||||
label="标签集"
|
||||
defaultValue=""
|
||||
disabled={disabled}
|
||||
>
|
||||
<MenuItem value="">请选择</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
|
||||
// RAPTOR集成配置部分
|
||||
const renderRaptorConfig = () => (
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography variant="h6">使用召回增强RAPTOR集成</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid container spacing={3}>
|
||||
{/* 启用RAPTOR */}
|
||||
<Grid size={12}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
disabled={disabled}
|
||||
{...form.register('raptor.use_raptor')}
|
||||
/>
|
||||
}
|
||||
label="使用召回增强RAPTOR集成"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* 提示词 */}
|
||||
<Grid size={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={4}
|
||||
label="提示词"
|
||||
placeholder="请总结以下段落。小心数字,不要编造。段落如下: {cluster_content} 以上就是你需要总结的内容。"
|
||||
disabled={disabled}
|
||||
{...form.register('raptor.prompt')}
|
||||
helperText="用于RAPTOR集成的提示词模板"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* 最大token数 */}
|
||||
<Grid size={{xs:12,sm:6}}>
|
||||
<Typography gutterBottom>最大token数</Typography>
|
||||
<Box sx={{ px: 2 }}>
|
||||
<Slider
|
||||
defaultValue={256}
|
||||
min={50}
|
||||
max={1000}
|
||||
step={1}
|
||||
valueLabelDisplay="on"
|
||||
disabled={disabled}
|
||||
{...form.register('raptor.max_token', { valueAsNumber: true })}
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ ml: 2 }}>
|
||||
256
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
{/* 阈值 */}
|
||||
<Grid size={{xs:12,sm:6}}>
|
||||
<Typography gutterBottom>阈值</Typography>
|
||||
<Box sx={{ px: 2 }}>
|
||||
<Slider
|
||||
defaultValue={0.1}
|
||||
min={0 as number}
|
||||
max={1}
|
||||
step={0.01}
|
||||
valueLabelDisplay="on"
|
||||
disabled={disabled}
|
||||
{...form.register('raptor.threshold', { valueAsNumber: true })}
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ ml: 2 }}>
|
||||
0.1
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
{/* 最大聚类数 */}
|
||||
<Grid size={{xs:12,sm:6}}>
|
||||
<Typography gutterBottom>最大聚类数</Typography>
|
||||
<Box sx={{ px: 2 }}>
|
||||
<Slider
|
||||
defaultValue={64}
|
||||
min={1}
|
||||
max={200 as number}
|
||||
step={1}
|
||||
valueLabelDisplay="on"
|
||||
disabled={disabled}
|
||||
{...form.register('raptor.max_cluster', { valueAsNumber: true })}
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ ml: 2 }}>
|
||||
64
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
{/* 随机种子 */}
|
||||
<Grid size={{xs:12,sm:6}}>
|
||||
<TextField
|
||||
fullWidth
|
||||
type="number"
|
||||
label="随机种子"
|
||||
defaultValue={0}
|
||||
disabled={disabled}
|
||||
{...form.register('raptor.random_seed', { valueAsNumber: true })}
|
||||
InputProps={{
|
||||
endAdornment: (
|
||||
<Button size="small" disabled={disabled}>
|
||||
+
|
||||
</Button>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
|
||||
// 提取知识图谱配置部分
|
||||
const renderGraphRagConfig = () => (
|
||||
<Accordion>
|
||||
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
|
||||
<Typography variant="h6">提取知识图谱</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Grid container spacing={3}>
|
||||
{/* 启用知识图谱 */}
|
||||
<Grid size={12}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
disabled={disabled}
|
||||
{...form.register('graphrag.use_graphrag')}
|
||||
/>
|
||||
}
|
||||
label="提取知识图谱"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* 添加实体类型 */}
|
||||
<Grid size={12}>
|
||||
<Button
|
||||
startIcon={<AddIcon />}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
// 添加新实体类型的逻辑
|
||||
}}
|
||||
>
|
||||
+ 实体类型
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
{/* 实体类型标签 */}
|
||||
<Grid size={12}>
|
||||
<Typography variant="subtitle2" gutterBottom>实体类型</Typography>
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
|
||||
{entityTypes.map((type, index) => (
|
||||
<Chip
|
||||
key={index}
|
||||
label={type}
|
||||
onDelete={disabled ? undefined : () => {
|
||||
setEntityTypes(prev => prev.filter((_, i) => i !== index));
|
||||
}}
|
||||
disabled={disabled}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
{/* 方法 */}
|
||||
<Grid size={12}>
|
||||
<FormControl fullWidth disabled={disabled}>
|
||||
<InputLabel>方法</InputLabel>
|
||||
<Select
|
||||
label="方法"
|
||||
defaultValue="Light"
|
||||
{...form.register('graphrag.method')}
|
||||
>
|
||||
<MenuItem value="Light">Light</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
{/* 实体归一化 */}
|
||||
<Grid size={12}>
|
||||
<FormControlLabel
|
||||
control={<Switch disabled={disabled} />}
|
||||
label="实体归一化"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* 社区报告生成 */}
|
||||
<Grid size={12}>
|
||||
<FormControlLabel
|
||||
control={<Switch disabled={disabled} />}
|
||||
label="社区报告生成"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
// 根据parser_id动态选择配置组件
|
||||
const ConfigurationComponent = useMemo(() => {
|
||||
const parser = parser_id as DocumentParserType;
|
||||
const component = ConfigurationComponentMap[parser] || EmptyComponent;
|
||||
return component || EmptyComponent;
|
||||
}, [parser_id]);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h5" gutterBottom>
|
||||
配置
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" gutterBottom sx={{ mb: 3 }}>
|
||||
在这里更新您的知识库详细信息,并更改切片方法。
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mb: 3 }}>
|
||||
{renderGeneralConfig()}
|
||||
{renderPageRankConfig()}
|
||||
{renderRaptorConfig()}
|
||||
{renderGraphRagConfig()}
|
||||
</Box>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'flex-end' }}>
|
||||
{onCancel && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={onCancel}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{cancelButtonText}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={form.handleSubmit(onSubmit)}
|
||||
disabled={disabled || isSubmitting}
|
||||
startIcon={isSubmitting ? <CircularProgress size={20} /> : <SaveIcon />}
|
||||
>
|
||||
{submitButtonText}
|
||||
</Button>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
{/* 动态配置内容 */}
|
||||
<ConfigurationComponent />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useRef } from 'react';
|
||||
import { type UseFormReturn } from 'react-hook-form';
|
||||
import { useFormContext, Controller } from 'react-hook-form';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
@@ -12,211 +12,138 @@ import {
|
||||
Avatar,
|
||||
Button,
|
||||
IconButton,
|
||||
CircularProgress,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
PhotoCamera as PhotoCameraIcon,
|
||||
Delete as DeleteIcon,
|
||||
Save as SaveIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
export interface BasicFormData {
|
||||
name: string;
|
||||
description: string;
|
||||
permission: string;
|
||||
avatar?: string;
|
||||
}
|
||||
|
||||
interface GeneralFormProps {
|
||||
form: UseFormReturn<BasicFormData>;
|
||||
onSubmit: (data: BasicFormData) => void;
|
||||
isSubmitting?: boolean;
|
||||
onCancel?: () => void;
|
||||
disabled?: boolean;
|
||||
submitButtonText?: string;
|
||||
cancelButtonText?: string;
|
||||
}
|
||||
|
||||
function GeneralForm({
|
||||
form,
|
||||
onSubmit,
|
||||
isSubmitting = false,
|
||||
onCancel,
|
||||
disabled = false,
|
||||
submitButtonText = '提交',
|
||||
cancelButtonText = '取消'
|
||||
}: GeneralFormProps) {
|
||||
function GeneralForm() {
|
||||
const { control, watch, setValue } = useFormContext();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// 处理头像上传
|
||||
const handleAvatarUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
// 检查文件类型
|
||||
if (!file.type.startsWith('image/')) {
|
||||
alert('请选择图片文件');
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查文件大小 (限制为 2MB)
|
||||
if (file.size > 2 * 1024 * 1024) {
|
||||
alert('图片大小不能超过 2MB');
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const base64String = e.target?.result as string;
|
||||
form.setValue('avatar', base64String);
|
||||
setValue('avatar', e.target?.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除头像
|
||||
const handleAvatarDelete = () => {
|
||||
form.setValue('avatar', undefined);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = '';
|
||||
}
|
||||
setValue('avatar', undefined);
|
||||
};
|
||||
|
||||
const avatarValue = form.watch('avatar');
|
||||
const handleAvatarClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
const avatar = watch('avatar');
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box sx={{ p: 3 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
基础信息
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
{/* 头像上传 */}
|
||||
<Grid size={12}>
|
||||
<Typography variant="subtitle2" gutterBottom>
|
||||
头像
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Grid size={{xs:12, md:6}}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 2 }}>
|
||||
<Avatar
|
||||
src={avatarValue}
|
||||
sx={{ width: 80, height: 80 }}
|
||||
src={avatar}
|
||||
sx={{ width: 120, height: 120, cursor: 'pointer' }}
|
||||
onClick={handleAvatarClick}
|
||||
>
|
||||
{!avatarValue && form.watch('name')?.charAt(0)?.toUpperCase()}
|
||||
{!avatar && <PhotoCameraIcon sx={{ fontSize: 40 }} />}
|
||||
</Avatar>
|
||||
<Box>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleAvatarUpload}
|
||||
disabled={disabled}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<PhotoCameraIcon />}
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={disabled}
|
||||
size="small"
|
||||
sx={{ mr: 1 }}
|
||||
startIcon={<PhotoCameraIcon />}
|
||||
onClick={handleAvatarClick}
|
||||
>
|
||||
选择图片
|
||||
上传头像
|
||||
</Button>
|
||||
{avatarValue && (
|
||||
{avatar && (
|
||||
<IconButton
|
||||
onClick={handleAvatarDelete}
|
||||
disabled={disabled}
|
||||
size="small"
|
||||
color="error"
|
||||
onClick={handleAvatarDelete}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleAvatarUpload}
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
支持 PNG、JPG 格式,文件大小不超过 2MB
|
||||
</Typography>
|
||||
</Grid>
|
||||
|
||||
{/* 知识库名称 */}
|
||||
<Grid size={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
label="知识库名称"
|
||||
placeholder="请输入知识库名称"
|
||||
disabled={disabled}
|
||||
{...form.register('name', {
|
||||
required: '请输入知识库名称',
|
||||
minLength: {
|
||||
value: 2,
|
||||
message: '知识库名称至少需要2个字符',
|
||||
},
|
||||
maxLength: {
|
||||
value: 50,
|
||||
message: '知识库名称不能超过50个字符',
|
||||
},
|
||||
})}
|
||||
error={!!form.formState.errors.name}
|
||||
helperText={form.formState.errors.name?.message?.toString() || '请输入知识库名称'}
|
||||
/>
|
||||
</Grid>
|
||||
{/* 表单字段 */}
|
||||
<Grid size={{xs:12,md:8}}>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{xs:12}}>
|
||||
<Controller
|
||||
name="name"
|
||||
control={control}
|
||||
rules={{ required: '知识库名称不能为空' }}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
label="知识库名称"
|
||||
fullWidth
|
||||
required
|
||||
error={!!error}
|
||||
helperText={error?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* 描述 */}
|
||||
<Grid size={12}>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={4}
|
||||
label="描述"
|
||||
placeholder="请输入知识库描述"
|
||||
disabled={disabled}
|
||||
{...form.register('description', {
|
||||
maxLength: {
|
||||
value: 500,
|
||||
message: '描述不能超过500个字符',
|
||||
},
|
||||
})}
|
||||
error={!!form.formState.errors.description}
|
||||
helperText={form.formState.errors.description?.message?.toString() || '请输入知识库描述'}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid size={{xs:12}}>
|
||||
<Controller
|
||||
name="description"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
label="描述"
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
placeholder="请输入知识库描述..."
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{/* 权限设置 */}
|
||||
<Grid size={{xs:12,sm:6}}>
|
||||
<FormControl fullWidth disabled={disabled}>
|
||||
<InputLabel>权限设置</InputLabel>
|
||||
<Select
|
||||
label="权限设置"
|
||||
{...form.register('permission')}
|
||||
value={form.watch('permission') || 'me'}
|
||||
>
|
||||
<MenuItem value="me">仅自己</MenuItem>
|
||||
<MenuItem value="team">团队可见</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<Grid size={12}>
|
||||
<Box sx={{ display: 'flex', gap: 2, justifyContent: 'flex-end', mt: 2 }}>
|
||||
{onCancel && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={onCancel}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{cancelButtonText}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={form.handleSubmit(onSubmit)}
|
||||
disabled={disabled || isSubmitting}
|
||||
startIcon={isSubmitting ? <CircularProgress size={20} /> : <SaveIcon />}
|
||||
>
|
||||
{submitButtonText}
|
||||
</Button>
|
||||
</Box>
|
||||
<Grid size={{xs:12}}>
|
||||
<Controller
|
||||
name="permission"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>权限设置</InputLabel>
|
||||
<Select {...field} label="权限设置">
|
||||
<MenuItem value="me">仅自己</MenuItem>
|
||||
<MenuItem value="team">团队成员</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user