diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index 1a801118..221a648a 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -4,7 +4,9 @@ import clsx from "clsx"; import Link from "next/link"; import { Activity, + ChevronDown, ChevronLeft, + ChevronRight, GraduationCap, LineChart, Menu, @@ -12,7 +14,7 @@ import { Table2, UserRound, } from "lucide-react"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { Fragment, useEffect, useMemo, useRef, useState } from "react"; import type { ProAccessState, ScanOpportunityRow } from "@/lib/dashboard-types"; import { getInitialLocaleFromNavigator } from "@/lib/i18n"; import { isBrowserLocalFullAccess } from "@/lib/local-dev-access"; @@ -267,7 +269,7 @@ function KoyfinMarketPanel({ }) { return ( - void; + rows: ScanOpportunityRow[]; + selectedId?: string | null; +}) { + const [expandedCities, setExpandedCities] = useState>(new Set()); + + const cityGroups = useMemo(() => { + const map = new Map(); + rows.forEach((row) => { + const key = String(row.city || "").toLowerCase(); + if (!map.has(key)) map.set(key, []); + map.get(key)!.push(row); + }); + return Array.from(map.entries()) + .map(([city, cityRows]) => { + const best = cityRows.reduce((a, b) => + Math.abs(Number(b.edge_percent || 0)) > Math.abs(Number(a.edge_percent || 0)) ? b : a + ); + const buckets = [...cityRows].sort((a, b) => + Number(b.edge_percent || 0) - Number(a.edge_percent || 0) + ); + return { city, displayName: rowName(best), best, buckets }; + }) + .sort((a, b) => + Math.abs(Number(b.best.edge_percent || 0)) - Math.abs(Number(a.best.edge_percent || 0)) + ); + }, [rows]); + + const toggle = (city: string) => { + setExpandedCities((prev) => { + const next = new Set(prev); + if (next.has(city)) next.delete(city); + else next.add(city); + return next; + }); + }; + + const labelExpand = isEn ? "Expand" : "展开"; + const labelBuckets = isEn ? "buckets" : "档位"; + + return ( +
+ + + + + + + + + + + + + + {cityGroups.map(({ city, displayName, best, buckets }) => { + const isExpanded = expandedCities.has(city); + const hasMultiple = buckets.length > 1; + return ( + + onSelect(best)} + > + + + + + + + + + {isExpanded && buckets.map((row) => ( + onSelect(row)} + > + + + + + + + + ))} + + ); + })} + +
+ + + {isEn ? "City / Best Signal" : "城市 / 最佳信号"} + {isEn ? "Edge" : "优势"}LiveDEB{isEn ? "Mkt" : "市场"}{isEn ? "Liq" : "流动性"}
+ + +
{displayName}
+
+ {best.target_label || ""} + {hasMultiple && !isExpanded ? ` +${buckets.length - 1}` : ""} +
+
+ {pct(best.edge_percent)} + {temp(best.current_temp || best.current_max_so_far, best.temp_symbol)}{temp(best.deb_prediction, best.temp_symbol)}{pct(best.market_probability ?? best.market_event_probability)}{money(best.book_liquidity ?? best.market_liquidity ?? best.volume)}
+ + {row.target_label || "--"} + + {pct(row.edge_percent)} + {temp(row.current_temp || row.current_max_so_far, row.temp_symbol)}{temp(row.deb_prediction, row.temp_symbol)}{pct(row.market_probability ?? row.market_event_probability)}{money(row.book_liquidity ?? row.market_liquidity ?? row.volume)}
+
+ ); +} + function PolyWeatherTerminal({ generatedText, isEn,