- chat_handler 添加 log_usage 精确记录 token 消耗(provider + model) - SSE build_sse_stream 添加估算 token 用量记录(4 字符 ≈ 1 token) - 新增 GET /ai/health-summary 端点聚合患者洞察+分析记录 - 小程序 AiHealthSummaryCard 组件(风险等级+洞察统计+摘要列表) - 小程序 services/ai-analysis 新增 getHealthSummary API
98 lines
3.2 KiB
TypeScript
98 lines
3.2 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { View, Text } from '@tarojs/components';
|
|
import ContentCard from '../ContentCard';
|
|
import { getHealthSummary, type HealthSummary } from '../../../services/ai-analysis';
|
|
import './index.scss';
|
|
|
|
interface AiHealthSummaryCardProps {
|
|
patientId: string;
|
|
}
|
|
|
|
const RISK_COLORS: Record<string, string> = {
|
|
critical: 'var(--tk-color-danger, #ff4d4f)',
|
|
high: 'var(--tk-color-warning, #faad14)',
|
|
medium: 'var(--tk-color-info, #1890ff)',
|
|
low: 'var(--tk-color-success, #52c41a)',
|
|
};
|
|
|
|
const RISK_LABELS: Record<string, string> = {
|
|
critical: '高风险',
|
|
high: '较高风险',
|
|
medium: '中等风险',
|
|
low: '低风险',
|
|
};
|
|
|
|
const AiHealthSummaryCard: React.FC<AiHealthSummaryCardProps> = ({ patientId }) => {
|
|
const [summary, setSummary] = useState<HealthSummary | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
if (!patientId) return;
|
|
setLoading(true);
|
|
getHealthSummary(patientId)
|
|
.then((data) => setSummary(data))
|
|
.catch(() => setSummary(null))
|
|
.finally(() => setLoading(false));
|
|
}, [patientId]);
|
|
|
|
if (loading) {
|
|
return (
|
|
<ContentCard>
|
|
<View className='ai-summary-loading'>
|
|
<Text className='ai-summary-loading-text'>AI 健康摘要加载中...</Text>
|
|
</View>
|
|
</ContentCard>
|
|
);
|
|
}
|
|
|
|
if (!summary) return null;
|
|
|
|
const riskColor = RISK_COLORS[summary.risk_level] || RISK_COLORS.low;
|
|
const riskLabel = RISK_LABELS[summary.risk_level] || '低风险';
|
|
|
|
return (
|
|
<ContentCard>
|
|
<View className='ai-summary-header'>
|
|
<Text className='ai-summary-title'>AI 健康摘要</Text>
|
|
<View className='ai-summary-risk' style={{ backgroundColor: riskColor }}>
|
|
<Text className='ai-summary-risk-text'>{riskLabel}</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{summary.latest_insight_title && (
|
|
<View className='ai-summary-insight'>
|
|
<Text className='ai-summary-insight-label'>最新洞察</Text>
|
|
<Text className='ai-summary-insight-text'>{summary.latest_insight_title}</Text>
|
|
</View>
|
|
)}
|
|
|
|
<View className='ai-summary-stats'>
|
|
<View className='ai-summary-stat'>
|
|
<Text className='ai-summary-stat-value'>{summary.active_insights_count}</Text>
|
|
<Text className='ai-summary-stat-label'>活跃洞察</Text>
|
|
</View>
|
|
<View className='ai-summary-stat'>
|
|
<Text className='ai-summary-stat-value'>{summary.recent_analyses_count}</Text>
|
|
<Text className='ai-summary-stat-label'>AI 分析</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{summary.summary_items.length > 0 && (
|
|
<View className='ai-summary-items'>
|
|
{summary.summary_items.slice(0, 3).map((item, idx) => (
|
|
<View key={idx} className='ai-summary-item'>
|
|
<View
|
|
className='ai-summary-item-dot'
|
|
style={{ backgroundColor: item.severity ? (RISK_COLORS[item.severity] || riskColor) : riskColor }}
|
|
/>
|
|
<Text className='ai-summary-item-title'>{item.title}</Text>
|
|
</View>
|
|
))}
|
|
</View>
|
|
)}
|
|
</ContentCard>
|
|
);
|
|
};
|
|
|
|
export default React.memo(AiHealthSummaryCard);
|