"use client"; import clsx from "clsx"; import { ChevronRight, Search, Table2 } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { ScanOpportunityRow } from "@/lib/dashboard-types"; import { MODEL_SUMMARY_MODEL_COLUMNS, buildModelSummaryRows, filterModelSummaryRows, formatModelSummaryProbability, formatModelSummaryLocalTime, formatModelSummaryTemp, hasModelSummaryForecastData, type ModelSummaryRow, } from "@/lib/model-summary"; type ModelSummaryDashboardProps = { rows: ScanOpportunityRow[]; isEn: boolean; generatedText?: string; }; const SUMMARY_TEXT = { title: { en: "Model Summary", zh: "模型汇总" }, subtitle: { en: "Today high-temperature view across DEB and model cluster sources.", zh: "按今日最高温口径汇总 DEB 与多模型预测。", }, search: { en: "Search city or model", zh: "搜索城市或模型" }, city: { en: "City", zh: "城市" }, region: { en: "Region", zh: "区域" }, localTime: { en: "Local Time", zh: "当地时间" }, gaussianMu: { en: "Gaussian μ", zh: "高斯 μ" }, detailedProbability: { en: "Gaussian Distribution Probability", zh: "高斯分布概率" }, noProbabilityDistribution: { en: "No probability distribution", zh: "暂无详细概率" }, median: { en: "Median", zh: "模型中位数" }, spread: { en: "Spread", zh: "分歧范围" }, empty: { en: "No model summary rows match the current filters.", zh: "当前筛选下没有模型汇总数据。" }, generated: { en: "Generated", zh: "生成" }, total: { en: "rows", zh: "行" }, } as const; const MODEL_SUMMARY_COLUMN_COUNT = MODEL_SUMMARY_MODEL_COLUMNS.length + 7; function copy(key: keyof typeof SUMMARY_TEXT, isEn: boolean) { return SUMMARY_TEXT[key][isEn ? "en" : "zh"]; } function TemperatureCell({ value, symbol, emphasis, }: { value: number | null; symbol: string; emphasis?: "deb" | "median"; }) { const missing = value == null; return ( {formatModelSummaryTemp(value, symbol)} ); } function FilterToggle({ checked, label, onChange, }: { checked: boolean; label: string; onChange: (checked: boolean) => void; }) { return ( ); } function ModelSummaryRowView({ row, nowMs, isEn, expanded, onToggle, }: { row: ModelSummaryRow; nowMs: number | null; isEn: boolean; expanded: boolean; onToggle: () => void; }) { return ( <> {row.regionLabel} {formatModelSummaryLocalTime(row, nowMs)} {MODEL_SUMMARY_MODEL_COLUMNS.map((column) => ( ))} {expanded ? (
{copy("detailedProbability", isEn)} μ {formatModelSummaryTemp(row.gaussianMu, row.tempSymbol)}
{row.probabilityBuckets.length ? (
{row.probabilityBuckets.map((bucket) => { const isTopBucket = bucket.key === row.topProbabilityBucketKey; return ( {bucket.label} {formatModelSummaryProbability(bucket.probability)} ); })}
) : ( {copy("noProbabilityDistribution", isEn)} )}
) : null} ); } function mergeWithLastGoodSummaryRows( incomingRows: ModelSummaryRow[], lastGoodRows: ModelSummaryRow[], ) { if (!lastGoodRows.length) return incomingRows; if (!incomingRows.length) return lastGoodRows; const incomingCityKeys = new Set(); const lastGoodByCity = new Map(lastGoodRows.map((row) => [row.cityKey, row])); const mergedRows = incomingRows.map((incomingRow) => { incomingCityKeys.add(incomingRow.cityKey); const lastGoodRow = lastGoodByCity.get(incomingRow.cityKey); if (!lastGoodRow) return incomingRow; return { ...lastGoodRow, cityName: incomingRow.cityName, regionLabel: incomingRow.regionLabel, regionLabelZh: incomingRow.regionLabelZh, regionSort: incomingRow.regionSort, tempSymbol: incomingRow.tempSymbol || lastGoodRow.tempSymbol, localTime: incomingRow.localTime || lastGoodRow.localTime, timezoneOffsetSeconds: incomingRow.timezoneOffsetSeconds ?? lastGoodRow.timezoneOffsetSeconds, searchText: incomingRow.searchText || lastGoodRow.searchText, }; }); return [ ...mergedRows, ...lastGoodRows.filter((row) => !incomingCityKeys.has(row.cityKey)), ].sort((a, b) => { if (a.regionSort !== b.regionSort) return a.regionSort - b.regionSort; return a.cityName.localeCompare(b.cityName, "en", { sensitivity: "base" }); }); } export function ModelSummaryDashboard({ rows, isEn, generatedText, }: ModelSummaryDashboardProps) { const [query, setQuery] = useState(""); const [debOnly, setDebOnly] = useState(false); const [wideSpreadOnly, setWideSpreadOnly] = useState(false); const [nowMs, setNowMs] = useState(null); const [expandedCityKeys, setExpandedCityKeys] = useState>(() => new Set()); const lastGoodSummaryRowsRef = useRef([]); useEffect(() => { const syncClock = () => setNowMs(Date.now()); syncClock(); const timer = window.setInterval(syncClock, 30_000); return () => window.clearInterval(timer); }, []); const incomingSummaryRows = useMemo(() => buildModelSummaryRows(rows, isEn), [rows, isEn]); const incomingHasForecastData = hasModelSummaryForecastData(incomingSummaryRows); const summaryRows = useMemo( () => incomingHasForecastData ? incomingSummaryRows : mergeWithLastGoodSummaryRows(incomingSummaryRows, lastGoodSummaryRowsRef.current), [incomingSummaryRows, incomingHasForecastData], ); useEffect(() => { if (incomingHasForecastData) { lastGoodSummaryRowsRef.current = incomingSummaryRows; } }, [incomingSummaryRows, incomingHasForecastData]); const visibleRows = useMemo( () => filterModelSummaryRows(summaryRows, { query, debOnly, wideSpreadOnly, }), [summaryRows, query, debOnly, wideSpreadOnly], ); const toggleExpandedCity = useCallback((cityKey: string) => { setExpandedCityKeys((current) => { const next = new Set(current); if (next.has(cityKey)) { next.delete(cityKey); } else { next.add(cityKey); } return next; }); }, []); useEffect(() => { const visibleCityKeys = new Set(visibleRows.map((row) => row.cityKey)); setExpandedCityKeys((current) => { let changed = false; const next = new Set(); current.forEach((cityKey) => { if (visibleCityKeys.has(cityKey)) { next.add(cityKey); } else { changed = true; } }); return changed ? next : current; }); }, [visibleRows]); return (

{copy("title", isEn)}

{visibleRows.length}/{summaryRows.length} {copy("total", isEn)}

{copy("subtitle", isEn)} {generatedText ? ( {copy("generated", isEn)} {generatedText} ) : null}

setQuery(event.target.value)} placeholder={copy("search", isEn)} className="h-8 w-full rounded border border-slate-300 bg-white pl-8 pr-2 text-xs font-semibold text-slate-700 outline-none transition focus:border-blue-400 focus:ring-2 focus:ring-blue-100" />
{MODEL_SUMMARY_MODEL_COLUMNS.map((column) => ( ))} {visibleRows.map((row) => ( toggleExpandedCity(row.cityKey)} /> ))}
{copy("city", isEn)} {copy("region", isEn)} {copy("localTime", isEn)} DEB {column.label} {copy("median", isEn)} {copy("spread", isEn)} {copy("gaussianMu", isEn)}
{!visibleRows.length && (
{copy("empty", isEn)}
)}
); }