diff --git a/frontend/components/dashboard/scan-terminal/ModelSummaryDashboard.tsx b/frontend/components/dashboard/scan-terminal/ModelSummaryDashboard.tsx index c0d992ad..ed069d4c 100644 --- a/frontend/components/dashboard/scan-terminal/ModelSummaryDashboard.tsx +++ b/frontend/components/dashboard/scan-terminal/ModelSummaryDashboard.tsx @@ -1,8 +1,8 @@ "use client"; import clsx from "clsx"; -import { Search, Table2 } from "lucide-react"; -import { useEffect, useMemo, useRef, useState } from "react"; +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, @@ -32,6 +32,8 @@ const SUMMARY_TEXT = { region: { en: "Region", zh: "区域" }, localTime: { en: "Local Time", zh: "当地时间" }, gaussianMu: { en: "Gaussian μ", zh: "高斯 μ" }, + detailedProbability: { en: "Detailed Probability", zh: "详细概率分布" }, + noProbabilityDistribution: { en: "No probability distribution", zh: "暂无详细概率" }, marketMatch: { en: "Market Match", zh: "市场匹配" }, median: { en: "Median", zh: "模型中位数" }, spread: { en: "Spread", zh: "分歧范围" }, @@ -40,6 +42,8 @@ const SUMMARY_TEXT = { total: { en: "rows", zh: "行" }, } as const; +const MODEL_SUMMARY_COLUMN_COUNT = MODEL_SUMMARY_MODEL_COLUMNS.length + 8; + function copy(key: keyof typeof SUMMARY_TEXT, isEn: boolean) { return SUMMARY_TEXT[key][isEn ? "en" : "zh"]; } @@ -101,71 +105,133 @@ function ModelSummaryRowView({ row, nowMs, isEn, + expanded, + onToggle, }: { row: ModelSummaryRow; nowMs: number | null; isEn: boolean; + expanded: boolean; + onToggle: () => void; }) { return ( - - - {row.cityName} - - - {row.regionLabel} - - - {formatModelSummaryLocalTime(row, nowMs)} - - - - - {MODEL_SUMMARY_MODEL_COLUMNS.map((column) => ( - - + <> + + + + + + {row.regionLabel} - ))} - - - - - - - - - - - {row.marketMatches.length ? ( - - ) : ( - - )} - - + + {formatModelSummaryLocalTime(row, nowMs)} + + + + + {MODEL_SUMMARY_MODEL_COLUMNS.map((column) => ( + + + + ))} + + + + + + + + + + + {row.marketMatches.length ? ( + + ) : ( + + )} + + + {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} + ); } @@ -215,6 +281,7 @@ export function ModelSummaryDashboard({ 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(() => { @@ -250,6 +317,34 @@ export function ModelSummaryDashboard({ [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 (
@@ -331,6 +426,8 @@ export function ModelSummaryDashboard({ row={row} nowMs={nowMs} isEn={isEn} + expanded={expandedCityKeys.has(row.cityKey)} + onToggle={() => toggleExpandedCity(row.cityKey)} /> ))} diff --git a/frontend/components/dashboard/scan-terminal/__tests__/modelSummaryDashboard.test.ts b/frontend/components/dashboard/scan-terminal/__tests__/modelSummaryDashboard.test.ts index d2f1f161..271fe8f0 100644 --- a/frontend/components/dashboard/scan-terminal/__tests__/modelSummaryDashboard.test.ts +++ b/frontend/components/dashboard/scan-terminal/__tests__/modelSummaryDashboard.test.ts @@ -221,6 +221,13 @@ export function runTests() { modelSummarySource.includes("hasModelSummaryForecastData") && modelSummarySource.includes("Gaussian μ") && modelSummarySource.includes("高斯 μ") && + modelSummarySource.includes("Detailed Probability") && + modelSummarySource.includes("详细概率分布") && + modelSummarySource.includes("expandedCityKeys") && + modelSummarySource.includes("toggleExpandedCity") && + modelSummarySource.includes("aria-expanded") && + modelSummarySource.includes("ChevronRight") && + modelSummarySource.includes("probabilityBuckets.map") && modelSummarySource.includes("Market Match") && modelSummarySource.includes("市场匹配") && modelSummarySource.includes("marketMatches.map") && @@ -228,7 +235,6 @@ export function runTests() { !modelSummarySource.includes("marketProbability") && !modelSummarySource.includes("edgePercent") && !modelSummarySource.includes("Probability Distribution") && - !modelSummarySource.includes("概率分布") && !modelSummarySource.includes("probabilityColumns") && modelSummarySource.includes("min-w-[96px]") && modelSummarySource.includes("Local Time") &&