// Page 1: 總覽 — KPIs, 異常橫條, 堆疊面積圖, Top brands 排行 function gotoBrand(b) { if (b && b.id) window.location.hash = '#/brands/' + encodeURIComponent(b.id); } function PageOverview({ filters, density, chartStyle }) { const { BRANDS, DAILY_AGG, TODAY, DAYS } = window.MOCK; // 真實 KPI 直接拿後端聚合結果,不要在前端用 BRANDS.dailyCost 推算 // (cost-trend 只回 top5 + 'other',對其他品牌都是 0,會失真) const live = (window.LIVE && window.LIVE.kpis) || {}; const todayCost = (DAILY_AGG[DAYS - 1] && DAILY_AGG[DAYS - 1].total) || 0; const yesterdayCost = (DAILY_AGG[DAYS - 2] && DAILY_AGG[DAYS - 2].total) || 0; const todayConvos = BRANDS.reduce((s, b) => s + (b.dailyConvos[DAYS - 1] || 0), 0); const yesterdayConvos = BRANDS.reduce((s, b) => s + (b.dailyConvos[DAYS - 2] || 0), 0); const periodCost = +(live.total_cost_usd || 0); const periodConvos = +(live.total_convos || 0); const activeBrandIds = +(live.active_brands || BRANDS.length); const totalTokens = +(live.total_tokens || 0); const avgCostPerConvo = +(live.avg_cost_per_convo || 0); const avgCacheHit = +(live.cache_hit_rate || 0); const deltaCost = +(live.delta_total_cost_usd || 0); const deltaConvos = +(live.delta_total_convos || 0); const deltaActive = +(live.delta_active_brands || 0); const deltaTokens = +(live.delta_total_tokens || 0); const deltaAvgCost = +(live.delta_avg_cost_per_convo || 0); const totalSpark = DAILY_AGG.map(d => d.total); const anomalous = BRANDS.filter(b => b.isAnomalous).sort((a, b) => b.anomalyRatio - a.anomalyRatio); const topBrands = [...BRANDS].sort((a, b) => b.totalCost - a.totalCost); const top5 = topBrands.slice(0, 5); const otherBrands = topBrands.slice(5); const stackSeries = top5.map((b) => ({ key: b.id, name: b.name.split(' ')[0], values: b.dailyCost, })).concat([{ key: 'other', name: '其他', values: DAILY_AGG.map((d, i) => otherBrands.reduce((s, b) => s + b.dailyCost[i], 0)), }]); return (
{anomalous.length > 0 && }
Object.values(d.perBrand).length)} sparkColor="#1D4ED8" inverse /> d.total * 220000)} sparkColor="#0891B2" inverse /> d.total / Math.max(1, BRANDS.reduce((s, b) => s + (b.dailyConvos[i] || 0), 0)))} sparkColor="#DB2777" inverseDelta />
{fmtUSD(periodCost, 0)} } />
{(w, h) => ( d.date)} colors={['#B45309', '#1D4ED8', '#6D28D9', '#15803D', '#DB2777', '#9098A3']} w={w} h={h} /> )} ({ label: s.name, color: ['#B45309', '#1D4ED8', '#6D28D9', '#15803D', '#DB2777', '#9098A3'][i], value: fmtUSD(s.values.reduce((a, b) => a + b, 0), 0), }))} />
0 ? (todayCost - yesterdayCost) / yesterdayCost : 0} /> 0 ? (todayConvos - yesterdayConvos) / yesterdayConvos : 0} inverseDelta /> (b.dailyCost[DAYS - 1] || 0) > 0.001).length} delta={0} inverseDelta />
逐時消耗
{ window.location.hash = '#/brands'; }} style={{ fontSize: 11, color: '#5C6470', padding: '4px 8px', border: '1px solid #E5E5E0', borderRadius: 4 }}>顯示全部 {BRANDS.length} 個 →} />
); } function PageHeader({ title, subtitle }) { return (

{title}

{subtitle && {subtitle}}
); } function Card({ children, style }) { return (
{children}
); } function CardHeader({ title, subtitle, right }) { return (
{title} {subtitle && {subtitle}}
{right}
); } function KPICard({ label, value, delta, spark, sparkColor, inverse, inverseDelta }) { const isGood = inverseDelta ? delta < 0 : delta > 0; const deltaColor = delta === 0 ? '#9098A3' : isGood ? '#15803D' : '#DC2626'; const arrow = delta > 0 ? '↑' : delta < 0 ? '↓' : '·'; return (
{label}
{value}
{arrow} {fmtDelta(Math.abs(delta))} vs 上期 {spark && }
); } function BigStat({ label, value, delta, inverseDelta }) { const isGood = inverseDelta ? delta < 0 : delta > 0; const deltaColor = isGood ? '#15803D' : '#DC2626'; return (
{label}
{value} {fmtDelta(delta)}
); } function HourlyBars() { const [hours, setHours] = React.useState(null); // null = loading React.useEffect(() => { let alive = true; window.AdminAPI.overview.hourly() .then((data) => { if (alive) setHours((data && data.hours) || []); }) .catch(() => { if (alive) setHours([]); }); return () => { alive = false; }; }, []); if (hours == null) { return
載入中…
; } const costs = hours.map(h => h.cost_usd || 0); const max = Math.max(0.0001, ...costs); const nowHour = new Date().getUTCHours(); // 後端用 UTC,前端 bar 也按 UTC 標當前小時 return (
{hours.map((h, i) => { const ratio = h.cost_usd ? Math.max(0.05, h.cost_usd / max) : 0; return (
0 ? `${ratio * 100}%` : '2px', background: i === nowHour ? '#FFE600' : i < nowHour ? '#5C6470' : '#E5E5E0', opacity: h.cost_usd === 0 ? 0.35 : 1, borderRadius: 1, }} title={`${String(i).padStart(2, '0')}:00 UTC · ${fmtUSD(h.cost_usd, 4)} · ${h.convos} 場`}>
); })}
); } function Legend({ items }) { return (
{items.map((it, i) => (
{it.label} {it.value}
))}
); } function ScaleToggle() { const [scale, setScale] = React.useState('linear'); const opts = [{ id: 'linear', label: '線性' }, { id: 'log', label: '對數' }]; return (
{opts.map(s => ( ))}
); } function AnomalyBanner({ anomalous, onPick }) { const top = anomalous[0]; return (
{anomalous.length} 個品牌今日費用超過 7 日均的 3 倍:{' '} {anomalous.slice(0, 3).map((b, i) => ( {' '} {fmtUSD(b.todayCost, 0)} ({b.anomalyRatio}×) {i < Math.min(2, anomalous.length - 1) ? '、' : ''} ))} {anomalous.length > 3 && 還有 {anomalous.length - 3} 個}
); } function BrandsTable({ brands, density, compact, onRowClick }) { const rowH = density === 'comfy' ? 38 : 32; const cellPad = density === 'comfy' ? '8px 12px' : '6px 12px'; return (
{brands.map((b, i) => ( onRowClick && onRowClick(b)} style={{ borderBottom: '1px solid #EEEEE9', background: i % 2 ? '#FAFAF9' : '#FFFFFF', cursor: onRowClick ? 'pointer' : 'default', height: rowH, }} onMouseEnter={(e) => e.currentTarget.style.background = '#FFFAB8'} onMouseLeave={(e) => e.currentTarget.style.background = i % 2 ? '#FAFAF9' : '#FFFFFF'} > ))}
排名 品牌 方案 對話數 Token 費用 平均/場 14 天趨勢 狀態
{i + 1}
{b.name}
{!compact &&
{b.id}
}
{fmtNum(b.totalConvos)} {fmtCompact(b.tokens)} {fmtUSD(b.totalCost, 2)} {fmtUSD(b.avgCostPerConvo, 3)}
); } function Th({ children, style }) { return {children}; } function BrandThumb({ id }) { const hash = id.split('').reduce((a, c) => a + c.charCodeAt(0), 0); const hue = (hash * 47) % 360; return (
{id.slice(0, 2).toUpperCase()}
); } window.PageOverview = PageOverview; window.PageHeader = PageHeader; window.Card = Card; window.CardHeader = CardHeader; window.KPICard = KPICard; window.BrandsTable = BrandsTable; window.BrandThumb = BrandThumb; window.Th = Th;