'use client' /** * CostPanel — AI 執行效能統計面板 (不含 AppLayout) * ================================================== * Sprint 5: 從 /cost/page.tsx 抽取 * 供原始頁面和整合頁面 (/operations) 共用 * * 建立時間: 2026-04-09 (台北時區) * 更新時間: 2026-07-03 — UI/UX 全面升級,Tailwind + Stat Cards + 視覺化分佈圖 */ import { useState, useEffect } from 'react' import { useTranslations } from 'next-intl' import { TrendingUp, Zap, CheckCircle2, Star, AlertCircle, RefreshCw } from 'lucide-react' import { cn } from '@/lib/utils' const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? '' interface AIPerformance { total_proposals: number executed_count: number execution_rate: number success_count: number success_rate: number avg_effectiveness: number | null effectiveness_distribution: Record } const SCORE_CONFIG = [ { score: 1, color: 'text-[#cc2200]', bg: 'bg-[#cc2200]', label: '極差' }, { score: 2, color: 'text-[#F59E0B]', bg: 'bg-[#F59E0B]', label: '差' }, { score: 3, color: 'text-[#87867f]', bg: 'bg-[#87867f]', label: '普通' }, { score: 4, color: 'text-[#4A90D9]', bg: 'bg-[#4A90D9]', label: '好' }, { score: 5, color: 'text-[#22C55E]', bg: 'bg-[#22C55E]', label: '極優' }, ] export function CostPanel() { const t = useTranslations('cost') const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const fetchData = () => { setLoading(true) setError(null) fetch(`${API_BASE}/api/v1/stats/ai-performance?days=30`) .then(r => r.json()) .then((d: AIPerformance) => { setData(d); setLoading(false) }) .catch(err => { setError(String(err)); setLoading(false) }) } useEffect(() => { fetchData() }, []) // eslint-disable-line react-hooks/exhaustive-deps const statCards = data ? [ { label: t('totalProposals'), value: data.total_proposals.toLocaleString(), icon: TrendingUp, accent: 'text-[#4A90D9]', bg: 'bg-[#f0f6ff]', border: 'border-[#c7dcf7]', }, { label: t('executionRate'), value: `${data.execution_rate.toFixed(1)}%`, icon: Zap, accent: 'text-[#d97757]', bg: 'bg-[#fff7f3]', border: 'border-[#f5cdb8]', }, { label: t('successRate'), value: `${data.success_rate.toFixed(1)}%`, icon: CheckCircle2, accent: 'text-[#17602a]', bg: 'bg-[#f0faf2]', border: 'border-[#8fc29a]', }, { label: t('avgEffectiveness'), value: data.avg_effectiveness != null ? data.avg_effectiveness.toFixed(2) : '—', icon: Star, accent: 'text-[#8a5a08]', bg: 'bg-[#fff7e8]', border: 'border-[#d9b36f]', }, ] : [] const maxDistCount = data ? Math.max(...[1,2,3,4,5].map(s => data.effectiveness_distribution[String(s)] ?? 0), 1) : 1 return (
{/* Header */}

AI Performance

{t('title')}

{t('subtitle')}

{/* Loading */} {loading && (
{[0,1,2,3].map(i => (
))}
)} {/* Error */} {!loading && error && (
)} {/* Data */} {!loading && !error && data && (
{/* Stat Cards */}
{statCards.map(({ label, value, icon: Icon, accent, bg, border }) => (

{label}

{value}

))}
{/* Effectiveness Distribution */} {Object.keys(data.effectiveness_distribution).length > 0 && (
{SCORE_CONFIG.map(({ score, color, bg, label }) => { const count = data.effectiveness_distribution[String(score)] ?? 0 const pct = maxDistCount > 0 ? (count / maxDistCount) * 100 : 0 return (
{label} {/* Mini bar */}
{count} ★{score}
) })}
)}
)} {/* No data */} {!loading && !error && !data && (
)}
) }