- 新增 entities 数据表(含迁移)、Entity 类型、DAO 层完整 CRUD
- AI 预处理管道接入真实模型(generateText),输出结构化 JSON(摘要+规范化内容+候选实体)
- 模板接口重构为 {systemPrompt, buildUserPrompt, parseOutput},general/meeting 模板真实化
- 新增 5 个实体路由端点 + 实体管理前端页面(列表/详情/编辑弹窗)
- 审核面板增强:展示 AI 预处理结构化结果+候选实体归一化面板(合并/新建/选择/放弃)
- 素材通过时根据用户确认的候选实体写入 entities 表
- 工作台菜单新增"实体"入口
- 新增 entities DAO 测试(16)、processor 测试(11)、路由测试(8),服务端 367 测试全部通过
- TypeScript 0 错误
116 lines
4.1 KiB
TypeScript
116 lines
4.1 KiB
TypeScript
import { Card, Descriptions, Flex, Tag, Typography } from "antd";
|
|
|
|
import type { EntityConfirmation, ProcessingResult } from "../../../../shared/api";
|
|
import type { Material, MaterialType } from "../types";
|
|
|
|
import { formatRelativeTime } from "../../../shared/utils/time";
|
|
import { STATUS_MAP } from "./constants";
|
|
import { EntityCandidatePanel } from "./EntityCandidatePanel";
|
|
|
|
interface MaterialContentProps {
|
|
material: Material;
|
|
projectId?: string;
|
|
onConfirmationsChange?: (confirmations: EntityConfirmation[]) => void;
|
|
}
|
|
|
|
const MATERIAL_TYPE_LABELS: Record<MaterialType, string> = {
|
|
general: "通用",
|
|
meeting: "会议",
|
|
};
|
|
|
|
function parseProcessingResult(raw: null | string): ProcessingResult | null {
|
|
if (!raw) return null;
|
|
try {
|
|
const parsed = JSON.parse(raw) as Partial<ProcessingResult>;
|
|
if (parsed && typeof parsed === "object") {
|
|
return {
|
|
candidateEntities: Array.isArray(parsed.candidateEntities) ? parsed.candidateEntities : [],
|
|
normalizedContent: typeof parsed.normalizedContent === "string" ? parsed.normalizedContent : "",
|
|
summary: typeof parsed.summary === "string" ? parsed.summary : "",
|
|
};
|
|
}
|
|
return null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function MaterialContent({ material, projectId, onConfirmationsChange }: MaterialContentProps) {
|
|
const statusInfo = STATUS_MAP[material.status] ?? { color: "default", label: material.status };
|
|
const typeLabel = MATERIAL_TYPE_LABELS[material.materialType] ?? material.materialType;
|
|
const processingResult = parseProcessingResult(material.processedContent);
|
|
|
|
return (
|
|
<Flex gap={12} vertical>
|
|
<Card size="small" title="原始文本">
|
|
<Typography.Paragraph>{material.description}</Typography.Paragraph>
|
|
</Card>
|
|
|
|
{processingResult && (
|
|
<>
|
|
<Card size="small" title="AI 摘要">
|
|
<Typography.Paragraph>{processingResult.summary}</Typography.Paragraph>
|
|
</Card>
|
|
|
|
<Card size="small" title="规范化内容">
|
|
<Typography.Paragraph
|
|
style={{
|
|
background: "var(--ant-color-fill-quaternary)",
|
|
borderRadius: 6,
|
|
padding: 12,
|
|
whiteSpace: "pre-wrap",
|
|
}}
|
|
>
|
|
{processingResult.normalizedContent}
|
|
</Typography.Paragraph>
|
|
</Card>
|
|
|
|
{material.status === "review" && projectId && onConfirmationsChange && (
|
|
<EntityCandidatePanel
|
|
candidates={processingResult.candidateEntities}
|
|
projectId={projectId}
|
|
onConfirmationsChange={onConfirmationsChange}
|
|
/>
|
|
)}
|
|
|
|
{material.status !== "review" && processingResult.candidateEntities.length > 0 && (
|
|
<Card size="small" title="候选实体(已确认)">
|
|
<Flex gap={4} wrap="wrap">
|
|
{processingResult.candidateEntities.map((ce: { name: string }, i: number) => (
|
|
<Tag key={i}>{ce.name}</Tag>
|
|
))}
|
|
</Flex>
|
|
</Card>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{!processingResult && material.processedContent && (
|
|
<Card size="small" title="处理结果">
|
|
<Typography.Paragraph
|
|
style={{
|
|
background: "var(--ant-color-fill-quaternary)",
|
|
borderRadius: 6,
|
|
padding: 12,
|
|
whiteSpace: "pre-wrap",
|
|
}}
|
|
>
|
|
{material.processedContent}
|
|
</Typography.Paragraph>
|
|
</Card>
|
|
)}
|
|
|
|
<Card size="small" title="基本信息">
|
|
<Descriptions column={1} size="small">
|
|
<Descriptions.Item label="状态">
|
|
<Tag color={statusInfo.color}>{statusInfo.label}</Tag>
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="素材类型">{typeLabel}</Descriptions.Item>
|
|
<Descriptions.Item label="关联时间">{material.associatedDate}</Descriptions.Item>
|
|
<Descriptions.Item label="创建时间">{formatRelativeTime(material.createdAt)}</Descriptions.Item>
|
|
</Descriptions>
|
|
</Card>
|
|
</Flex>
|
|
);
|
|
}
|