Add 3 new data-driven insight cards (daily cases, district risk comparison, weather impact) with real parquet data. Fix season card to use current date instead of data date. Expand to 11 cards. Add POST /api/chat endpoint proxying to ai.2890.ltd with JWT auth. Create ChatBot frontend component with collapsible chat panel, message bubbles, and auto-scroll. Chat API key stored in .env only. Clean up duplicate typing imports in insights.py, export cachedPost.
202 lines
6.4 KiB
TypeScript
202 lines
6.4 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { useAnalysisStore } from '@/stores/analysisStore';
|
|
import { ErrorBanner } from '@/components/ErrorBanner';
|
|
import { ChatBot } from '@/components/ChatBot';
|
|
import {
|
|
Lightbulb,
|
|
AlertTriangle,
|
|
CheckCircle,
|
|
Info,
|
|
XCircle,
|
|
TrendingUp,
|
|
TrendingDown,
|
|
Clock,
|
|
} from 'lucide-react';
|
|
|
|
const TYPE_CONFIG = {
|
|
warning: {
|
|
icon: AlertTriangle,
|
|
bg: 'bg-warning-light',
|
|
border: 'border-warning',
|
|
iconColor: 'text-warning',
|
|
badge: 'bg-warning text-white',
|
|
},
|
|
danger: {
|
|
icon: XCircle,
|
|
bg: 'bg-danger-light',
|
|
border: 'border-danger',
|
|
iconColor: 'text-danger',
|
|
badge: 'bg-danger text-white',
|
|
},
|
|
success: {
|
|
icon: CheckCircle,
|
|
bg: 'bg-success-light',
|
|
border: 'border-success',
|
|
iconColor: 'text-success',
|
|
badge: 'bg-success text-white',
|
|
},
|
|
info: {
|
|
icon: Info,
|
|
bg: 'bg-primary-muted',
|
|
border: 'border-primary',
|
|
iconColor: 'text-primary',
|
|
badge: 'bg-primary text-white',
|
|
},
|
|
};
|
|
|
|
export function Insights() {
|
|
const { insights, isLoading, error, clearError, fetchInsights } = useAnalysisStore();
|
|
|
|
useEffect(() => {
|
|
fetchInsights();
|
|
}, []);
|
|
|
|
const stats = insights
|
|
? [
|
|
{
|
|
label: '总洞察数',
|
|
value: insights.total_insights || 0,
|
|
icon: Lightbulb,
|
|
color: 'text-primary',
|
|
bg: 'bg-primary-muted',
|
|
},
|
|
{
|
|
label: '预警',
|
|
value: (insights.warning_count || 0) + ((insights as any).danger_count || 0),
|
|
icon: AlertTriangle,
|
|
color: 'text-warning',
|
|
bg: 'bg-warning-light',
|
|
},
|
|
{
|
|
label: '正常',
|
|
value: insights.success_count || 0,
|
|
icon: CheckCircle,
|
|
color: 'text-success',
|
|
bg: 'bg-success-light',
|
|
},
|
|
{
|
|
label: '信息',
|
|
value: insights.info_count || 0,
|
|
icon: Info,
|
|
color: 'text-primary',
|
|
bg: 'bg-primary-muted',
|
|
},
|
|
]
|
|
: [];
|
|
|
|
return (
|
|
<div>
|
|
{error && (
|
|
<ErrorBanner
|
|
error={error}
|
|
onRetry={() => { clearError(); fetchInsights(); }}
|
|
onDismiss={clearError}
|
|
/>
|
|
)}
|
|
<div className="mb-5">
|
|
<h1 className="font-display text-[18px] font-semibold mb-1 flex items-center gap-2">
|
|
<Lightbulb className="w-5 h-5 text-primary" />
|
|
智能洞察
|
|
</h1>
|
|
<p className="text-[12px] text-text-muted">
|
|
基于数据分析自动生成的风险洞察与建议
|
|
</p>
|
|
</div>
|
|
|
|
{isLoading && (
|
|
<div className="mb-4 text-center py-8 bg-bg-card rounded-lg border border-border">
|
|
<span className="text-text-secondary">数据加载中...</span>
|
|
</div>
|
|
)}
|
|
|
|
{insights && (
|
|
<div className="grid grid-cols-4 gap-4 mb-4">
|
|
{stats.map((stat) => (
|
|
<div key={stat.label} className="card p-4">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<div className={`w-8 h-8 rounded-lg ${stat.bg} flex items-center justify-center`}>
|
|
<stat.icon className={`w-4 h-4 ${stat.color}`} />
|
|
</div>
|
|
<span className="text-[11px] font-medium text-text-muted uppercase tracking-wide">
|
|
{stat.label}
|
|
</span>
|
|
</div>
|
|
<div className="font-display text-[26px] font-bold text-text-primary">
|
|
{stat.value}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{insights && (
|
|
<div className="grid grid-cols-2 gap-4">
|
|
{(insights.cards || []).map((card) => {
|
|
const config = TYPE_CONFIG[card.type];
|
|
const Icon = config.icon;
|
|
return (
|
|
<div
|
|
key={card.id}
|
|
className={`card p-4 border-l-4 ${config.border} hover:shadow-md transition-shadow`}
|
|
>
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div className="flex items-center gap-2">
|
|
<div className={`w-8 h-8 rounded-lg ${config.bg} flex items-center justify-center`}>
|
|
<Icon className={`w-4 h-4 ${config.iconColor}`} />
|
|
</div>
|
|
<div>
|
|
<h3 className="text-[14px] font-semibold text-text-primary">
|
|
{card.title}
|
|
</h3>
|
|
<span className="text-[11px] text-text-muted flex items-center gap-1">
|
|
<Clock className="w-3 h-3" />
|
|
{card.timestamp}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<span className={`text-[10px] font-semibold px-2 py-0.5 rounded ${config.badge}`}>
|
|
{card.type === 'warning' ? '预警' : card.type === 'danger' ? '紧急' : card.type === 'success' ? '正常' : '信息'}
|
|
</span>
|
|
</div>
|
|
|
|
<p className="text-[13px] text-text-secondary leading-relaxed mb-3">
|
|
{card.description}
|
|
</p>
|
|
|
|
{card.metric && card.metricValue && (
|
|
<div className="flex items-center gap-2 pt-3 border-t border-border">
|
|
<span className="text-[12px] text-text-muted">{card.metric}:</span>
|
|
<span className={`text-[14px] font-bold flex items-center gap-1 ${
|
|
card.type === 'warning' || card.type === 'danger'
|
|
? 'text-danger'
|
|
: card.type === 'success'
|
|
? 'text-success'
|
|
: 'text-primary'
|
|
}`}>
|
|
{card.metricValue.includes('+') ? (
|
|
<TrendingUp className="w-3.5 h-3.5" />
|
|
) : card.metricValue.includes('-') ? (
|
|
<TrendingDown className="w-3.5 h-3.5" />
|
|
) : null}
|
|
{card.metricValue}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{!insights && !isLoading && (
|
|
<div className="card p-8 text-center">
|
|
<Lightbulb className="w-12 h-12 text-text-muted mx-auto mb-3" />
|
|
<p className="text-text-secondary">暂无洞察数据</p>
|
|
</div>
|
|
)}
|
|
|
|
<ChatBot />
|
|
</div>
|
|
);
|
|
}
|