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:
2025-10-15 16:24:53 +08:00
parent 486815d83e
commit fe8747983e
33 changed files with 2627 additions and 812 deletions

View File

@@ -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="请总结以下段落。小心数字,不要编造。段落如下:&#10; {cluster_content}&#10;以上就是你需要总结的内容。"
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>
);
}

View File

@@ -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">
PNGJPG 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>