feat(web+mp): AI 分析结果增强展示
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

Web 端 AiAnalysisList:
- 分析结果 Markdown 风格渲染(标题/列表/粗体/代码)
- 趋势分析类型显示统计方法提示
- 自动分析结果显示「系统自动分析」标签

小程序 ai-report/detail:
- 新增 result_metadata 字段
- 自动分析标记(紫色标签)
- 趋势分析统计方法说明卡片
This commit is contained in:
iven
2026-04-28 20:12:34 +08:00
parent 10c79c5e39
commit f99892ee16
4 changed files with 170 additions and 9 deletions

View File

@@ -1,8 +1,13 @@
import { useEffect, useState, useCallback, useMemo } from 'react';
import { Table, Select, Tag, Space, message, Typography } from 'antd';
import {
RobotOutlined,
} from '@ant-design/icons';
import { useThemeMode } from '../../hooks/useThemeMode';
import { analysisApi, type AnalysisItem } from '../../api/ai/analysis';
const { Text } = Typography;
const ANALYSIS_TYPE_MAP: Record<string, string> = {
lab_report_interpretation: '化验单解读',
health_trend_analysis: '趋势分析',
@@ -22,6 +27,88 @@ const TYPE_OPTIONS = Object.entries(ANALYSIS_TYPE_MAP).map(([value, label]) => (
label,
}));
// ---------------------------------------------------------------------------
// 分析结果渲染Markdown 风格)
// ---------------------------------------------------------------------------
function AnalysisContent({ content, isDark }: { content: string; isDark: boolean }) {
// 简单的 Markdown 风格渲染
const lines = content.split('\n');
const rendered = lines.map((line, i) => {
// 标题行
if (line.startsWith('### ')) {
return (
<Text key={i} strong style={{ display: 'block', fontSize: 14, marginTop: 12, marginBottom: 4 }}>
{line.slice(4)}
</Text>
);
}
if (line.startsWith('## ')) {
return (
<Text key={i} strong style={{ display: 'block', fontSize: 15, marginTop: 16, marginBottom: 6, borderBottom: '1px solid ' + (isDark ? '#1e293b' : '#f0f0f0'), paddingBottom: 4 }}>
{line.slice(3)}
</Text>
);
}
// 列表项
if (line.startsWith('- ') || line.startsWith('* ')) {
return (
<div key={i} style={{ paddingLeft: 16, position: 'relative', lineHeight: 1.8 }}>
<span style={{ position: 'absolute', left: 4, color: '#3b82f6' }}></span>
{renderInlineStyles(line.slice(2))}
</div>
);
}
// 有序列表
const orderedMatch = line.match(/^(\d+)\.\s/);
if (orderedMatch) {
return (
<div key={i} style={{ paddingLeft: 16, lineHeight: 1.8 }}>
<Text type="secondary" style={{ marginRight: 4 }}>{orderedMatch[1]}.</Text>
{renderInlineStyles(line.slice(orderedMatch[0].length))}
</div>
);
}
// 空行
if (line.trim() === '') {
return <div key={i} style={{ height: 8 }} />;
}
// 普通段落
return <div key={i} style={{ lineHeight: 1.8 }}>{renderInlineStyles(line)}</div>;
});
return <div style={{ fontSize: 13 }}>{rendered}</div>;
}
/** 简单行内样式渲染(粗体、代码) */
function renderInlineStyles(text: string) {
// 拆分 **bold** 和 `code` 模式
const parts = text.split(/(\*\*[^*]+\*\*|`[^`]+`)/g);
return parts.map((part, i) => {
if (part.startsWith('**') && part.endsWith('**')) {
return <Text key={i} strong>{part.slice(2, -2)}</Text>;
}
if (part.startsWith('`') && part.endsWith('`')) {
return (
<code key={i} style={{
background: 'rgba(59, 130, 246, 0.1)',
padding: '1px 4px',
borderRadius: 3,
fontSize: 12,
fontFamily: 'monospace',
}}>
{part.slice(1, -1)}
</code>
);
}
return <span key={i}>{part}</span>;
});
}
// ---------------------------------------------------------------------------
// 主组件
// ---------------------------------------------------------------------------
export default function AiAnalysisList() {
const [data, setData] = useState<AnalysisItem[]>([]);
const [total, setTotal] = useState(0);
@@ -69,6 +156,15 @@ export default function AiAnalysisList() {
}
};
// 解析 metadata 中的趋势统计信息
const trendMetrics = useMemo(() => {
if (!detail?.result_metadata) return null;
const meta = detail.result_metadata as Record<string, unknown>;
// 自动分析结果中可能包含 metrics 统计
// 也从 sanitized_input 中解析(如果有的话)
return meta;
}, [detail]);
const columns = useMemo(() => [
{
title: '分析类型',
@@ -151,36 +247,55 @@ export default function AiAnalysisList() {
onExpand: handleExpand,
expandedRowRender: () => {
if (!detail) return null;
const isTrendAnalysis = detail.analysis_type === 'trend';
return (
<div style={{ padding: '8px 0' }}>
{/* 自动分析标记 */}
{trendMetrics && (trendMetrics as Record<string, unknown>).auto_analysis === true && (
<div style={{ marginBottom: 8 }}>
<Tag color="purple" style={{ fontSize: 11 }}>
<RobotOutlined style={{ marginRight: 4 }} />
</Tag>
</div>
)}
{detail.error_message && (
<div style={{ marginBottom: 12 }}>
<Typography.Text type="danger">: {detail.error_message}</Typography.Text>
<Text type="danger">: {detail.error_message}</Text>
</div>
)}
{detail.result_content && (
<div>
<Typography.Text strong style={{ display: 'block', marginBottom: 8 }}>
<Text strong style={{ display: 'block', marginBottom: 8 }}>
</Typography.Text>
</Text>
<div
style={{
background: isDark ? '#0f172a' : '#f8fafc',
padding: 16,
borderRadius: 8,
whiteSpace: 'pre-wrap',
fontSize: 13,
lineHeight: 1.8,
maxHeight: 400,
maxHeight: 600,
overflow: 'auto',
}}
>
{detail.result_content}
<AnalysisContent content={detail.result_content} isDark={isDark} />
</div>
{/* 趋势分析类型显示统计摘要提示 */}
{isTrendAnalysis && (
<div style={{ marginTop: 12, padding: '8px 12px', background: isDark ? '#0f172a' : '#fffbeb', borderRadius: 6, border: `1px solid ${isDark ? '#1e293b' : '#fde68a'}` }}>
<Text type="secondary" style={{ fontSize: 11 }}>
线 2 R² 1
</Text>
</div>
)}
</div>
)}
{!detail.result_content && !detail.error_message && (
<Typography.Text type="secondary"></Typography.Text>
<Text type="secondary"></Text>
)}
</div>
);