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") &&