From 7b8826dfa14f881e89c54549c57f8e7479ec05f1 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Mon, 25 May 2026 20:53:03 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B8=85=E7=90=86=E6=9C=80=E5=90=8E=E4=B8=80?= =?UTF-8?q?=E6=89=B9=20Polymarket=20=E6=B3=A8=E9=87=8A+=E6=AD=BB=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=20WeatherDecisionBand?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/ScanTerminalDashboard.tsx | 136 +++++++----- .../scan-terminal/AirportEvidencePanel.tsx | 2 +- .../scan-terminal/CitySelectorDropdown.tsx | 201 ++++++++++++++++++ .../LiveTemperatureThresholdChart.tsx | 46 ++-- .../dashboard/scan-terminal/Panel.tsx | 2 +- .../scan-terminal/WeatherDecisionBand.tsx | 35 --- .../ops/health/HealthPageClient.tsx | 2 - frontend/content/docs/docs.ts | 4 +- src/data_collection/city_registry.py | 2 +- src/data_collection/city_risk_profiles.py | 2 +- src/data_collection/weather_sources.py | 4 +- src/utils/telegram_push.py | 2 +- web/scan_terminal_city_row.py | 2 +- 13 files changed, 328 insertions(+), 112 deletions(-) create mode 100644 frontend/components/dashboard/scan-terminal/CitySelectorDropdown.tsx delete mode 100644 frontend/components/dashboard/scan-terminal/WeatherDecisionBand.tsx diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index b72384f0..74b263f3 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -46,6 +46,7 @@ import { TrainingDashboard } from "@/components/dashboard/scan-terminal/Training import { LiveTemperatureThresholdChart } from "@/components/dashboard/scan-terminal/LiveTemperatureThresholdChart"; import { KoyfinRowsTable } from "@/components/dashboard/scan-terminal/KoyfinRowsTable"; import { rowName, pct, money, temp, edgeClass } from "@/components/dashboard/scan-terminal/utils"; +import { CitySelectorDropdown } from "@/components/dashboard/scan-terminal/CitySelectorDropdown"; function createEmptyAccess(loading = true): ProAccessState { return { @@ -248,20 +249,21 @@ function EmptySlotCard({ slotIndex, isActive, isEn, - availableCities, onSelectSlot, - onSelectCity, + onOpenSearch, }: { slotIndex: number; isActive: boolean; isEn: boolean; - availableCities: { city: string; name: string }[]; onSelectSlot: () => void; - onSelectCity: (city: string) => void; + onOpenSearch: () => void; }) { return (
{ + onSelectSlot(); + onOpenSearch(); + }} className={clsx( "flex flex-col items-center justify-center h-full rounded-[4px] border-2 border-dashed p-6 cursor-pointer bg-slate-50/50 transition-all", isActive @@ -277,24 +279,15 @@ function EmptySlotCard({
{isEn - ? "Select this card and click a city on the left, or select below:" - : "激活此卡片并从左侧选择城市,或从下方直接选择:"} + ? "Click to choose a city weather chart for this slot." + : "点击为该槽位选择一个城市天气图表。"}
- + {isEn ? "Choose City..." : "选择城市..."} + ); } @@ -376,6 +369,7 @@ function PolyWeatherTerminal({ }); const [activeSlotIndex, setActiveSlotIndex] = useState(0); const [maximizedSlotIndex, setMaximizedSlotIndex] = useState(null); + const [activeSearchSlotIndex, setActiveSearchSlotIndex] = useState(null); const NAV_ITEMS = [ { key: "contracts", Icon: Table2, labelEn: "Contracts", labelZh: "天气合约" }, @@ -388,6 +382,7 @@ function PolyWeatherTerminal({ }, [selectedRegionKey, setSelectedCity]); const filteredRegionRows = useMemo(() => { + if (selectedRegionKey === "all") return rows; return rows.filter( (row) => getCityRegion(row) === selectedRegionKey, ); @@ -395,7 +390,12 @@ function PolyWeatherTerminal({ useEffect(() => { if (filteredRegionRows.length && slots.every((s) => s === null)) { - const next = [filteredRegionRows[0].city, null, null, null]; + const next = [ + filteredRegionRows[0]?.city || null, + filteredRegionRows[1]?.city || null, + filteredRegionRows[2]?.city || null, + filteredRegionRows[3]?.city || null, + ]; setSlots(next); try { localStorage.setItem("polyweather_terminal_slots", JSON.stringify(next)); @@ -677,29 +677,19 @@ function PolyWeatherTerminal({ {/* Desktop layout */} -
-
- handleSelectCityForSlot(activeSlotIndex, city)} - slots={slots} - activeSlotIndex={activeSlotIndex} - /> -
- -
+
+
{maximizedSlotIndex !== null ? ( // Maximized view
setActiveSlotIndex(maximizedSlotIndex)} className={clsx( - "relative h-full rounded-[4px] border overflow-hidden border-blue-500 ring-2 ring-blue-500/20 shadow-md z-10" + "relative h-full rounded-[4px] border border-blue-500 ring-2 ring-blue-500/20 shadow-md z-10", + activeSearchSlotIndex === maximizedSlotIndex ? "" : "overflow-hidden" )} > {/* Floating actions toolbar */} -
+
) : ( // 2x2 grid layout @@ -747,15 +751,28 @@ function PolyWeatherTerminal({ if (!cityInSlot) { return ( - setActiveSlotIndex(slotIndex)} - onSelectCity={(city) => handleSelectCityForSlot(slotIndex, city)} - /> +
+ setActiveSlotIndex(slotIndex)} + onOpenSearch={() => setActiveSearchSlotIndex(slotIndex)} + /> + + {activeSearchSlotIndex === slotIndex && ( + { + handleSelectCityForSlot(slotIndex, city); + setActiveSearchSlotIndex(null); + }} + onClose={() => setActiveSearchSlotIndex(null)} + className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-50 w-[280px] bg-white border border-slate-200 rounded shadow-lg p-2" + /> + )} +
); } @@ -768,14 +785,15 @@ function PolyWeatherTerminal({ key={slotIndex} onClick={() => setActiveSlotIndex(slotIndex)} className={clsx( - "relative h-full rounded-[4px] border overflow-hidden transition-all", + "relative h-full rounded-[4px] border transition-all", isSlotActive ? "border-blue-500 ring-2 ring-blue-500/20 shadow-md z-10" - : "border-[#d2d9e2] hover:border-slate-400" + : "border-[#d2d9e2] hover:border-slate-400", + activeSearchSlotIndex === slotIndex ? "" : "overflow-hidden" )} > {/* Floating actions toolbar */} -
+
); })} @@ -845,9 +877,9 @@ function ScanTerminalScreen() { hydrated && (proAccess.subscriptionActive || canUseLocalFullAccess); const userLocalTime = useUserLocalClock(); const { themeMode } = useScanTerminalTheme(); - const [selectedRegionKey, setSelectedRegionKey] = useState("east_asia"); + const [selectedRegionKey, setSelectedRegionKey] = useState("all"); const [localTimezoneOffsetSeconds, setLocalTimezoneOffsetSeconds] = useState(null); - const [useLocalTimezoneDefault, setUseLocalTimezoneDefault] = useState(true); + const [useLocalTimezoneDefault, setUseLocalTimezoneDefault] = useState(false); const [visibleRegions, setVisibleRegions] = useState>(() => { try { const stored = localStorage.getItem("polyweather_visible_regions"); @@ -938,7 +970,7 @@ function ScanTerminalScreen() { }, []); useEffect(() => { - setSelectedRegionKey(getDefaultRegion()); + setSelectedRegionKey("all"); setLocalTimezoneOffsetSeconds(-new Date().getTimezoneOffset() * 60); }, []); diff --git a/frontend/components/dashboard/scan-terminal/AirportEvidencePanel.tsx b/frontend/components/dashboard/scan-terminal/AirportEvidencePanel.tsx index b61798d6..389d9e67 100644 --- a/frontend/components/dashboard/scan-terminal/AirportEvidencePanel.tsx +++ b/frontend/components/dashboard/scan-terminal/AirportEvidencePanel.tsx @@ -4,7 +4,7 @@ import type { CityDetail } from "@/lib/dashboard-types"; import { getDisplayAirportPrimary } from "@/lib/airport-observation-display"; import { formatTemperatureValue } from "@/lib/temperature-utils"; -// Settlement runway mapping — matches Polymarket settlement anchors +// Settlement runway mapping — matches settlement anchors const SETTLEMENT_RUNWAY_PAIRS: Record> = { shanghai: [["17L", "35R"]], beijing: [["01", "19"]], diff --git a/frontend/components/dashboard/scan-terminal/CitySelectorDropdown.tsx b/frontend/components/dashboard/scan-terminal/CitySelectorDropdown.tsx new file mode 100644 index 00000000..069e0476 --- /dev/null +++ b/frontend/components/dashboard/scan-terminal/CitySelectorDropdown.tsx @@ -0,0 +1,201 @@ +"use client"; + +import { useEffect, useMemo, useRef, useState } from "react"; +import clsx from "clsx"; +import type { ScanOpportunityRow } from "@/lib/dashboard-types"; +import { REGIONS, getCityRegion } from "./continent-grouping"; +import { rowName, temp } from "./utils"; + +interface CitySelectorDropdownProps { + isEn: boolean; + rows: ScanOpportunityRow[]; + onSelectCity: (city: string) => void; + onClose: () => void; + className?: string; +} + +export function CitySelectorDropdown({ + isEn, + rows, + onSelectCity, + onClose, + className, +}: CitySelectorDropdownProps) { + const containerRef = useRef(null); + const inputRef = useRef(null); + const [searchQuery, setSearchQuery] = useState(""); + const [activeTab, setActiveTab] = useState("all"); + + // Auto-focus input on mount + useEffect(() => { + inputRef.current?.focus(); + }, []); + + // Handle click outside and Escape key + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (containerRef.current && !containerRef.current.contains(event.target as Node)) { + onClose(); + } + }; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") { + onClose(); + } + }; + + document.addEventListener("mousedown", handleClickOutside); + document.addEventListener("keydown", handleKeyDown); + return () => { + document.removeEventListener("mousedown", handleClickOutside); + document.removeEventListener("keydown", handleKeyDown); + }; + }, [onClose]); + + // Tab definitions + const tabs = useMemo(() => { + return [ + { key: "all", labelEn: "ALL", labelZh: "全部" }, + ...REGIONS.map((r) => ({ + key: r.key, + labelEn: r.labelEn.replace("Asia", "").replace("America", "").trim() || r.labelEn, + labelZh: r.labelZh, + })), + ]; + }, []); + + // Filter rows + const filteredRows = useMemo(() => { + const q = searchQuery.toLowerCase().trim(); + return rows.filter((row) => { + // 1. Region filter + if (activeTab !== "all") { + const region = getCityRegion(row); + if (region !== activeTab) return false; + } + + // 2. Query filter + if (!q) return true; + const haystack = [ + row.city, + row.city_display_name, + row.display_name, + row.airport, + ] + .filter(Boolean) + .map((s) => s!.toLowerCase()); + return haystack.some((s) => s.includes(q)); + }); + }, [rows, searchQuery, activeTab]); + + return ( +
e.stopPropagation()} // Prevent card activation + > + {/* Search Input Area */} +
+ setSearchQuery(e.target.value)} + placeholder={isEn ? "Search city or airport..." : "搜索城市、机场..."} + className="w-full px-2.5 py-1.5 border border-slate-300 rounded bg-white text-xs outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500/20" + /> + +
+ + {/* Region filter tabs */} +
+ {tabs.map((tab) => { + const isActive = activeTab === tab.key; + return ( + + ); + })} +
+ + {/* Scrollable list */} +
+ {filteredRows.length === 0 ? ( +
+ {isEn ? "No matching cities" : "无匹配城市"} +
+ ) : ( + filteredRows.map((row) => { + const cityName = rowName(row); + const obsTemp = row.current_temp ?? row.current_max_so_far; + const debPrediction = row.deb_prediction; + const symbol = row.temp_symbol || "°C"; + + return ( + + ); + }) + )} +
+
+ ); +} diff --git a/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx b/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx index 8abacdca..d914bff4 100644 --- a/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx +++ b/frontend/components/dashboard/scan-terminal/LiveTemperatureThresholdChart.tsx @@ -832,11 +832,13 @@ export function LiveTemperatureThresholdChart({ row, allRows = [], compact = false, + onSearchClick, }: { isEn: boolean; row: ScanOpportunityRow | null; allRows?: ScanOpportunityRow[]; compact?: boolean; + onSearchClick?: () => void; }) { const [hourly, setHourly] = useState(null); const city = String(row?.city || "").toLowerCase().trim(); @@ -1009,19 +1011,37 @@ export function LiveTemperatureThresholdChart({ [series, data], ); - const panelTitle = row - ? `${rowName(row)} · ${ - isEn - ? timeframe === "1D" - ? "Live & Forecast" - : `${timeframe} Forecast` - : timeframe === "1D" - ? "实测与预测" - : `${timeframe}预报` - }` - : isEn - ? "Temperature Chart" - : "气温图表"; + const subtitle = row + ? isEn + ? timeframe === "1D" + ? "Live & Forecast" + : `${timeframe} Forecast` + : timeframe === "1D" + ? "实测与预测" + : `${timeframe}预报` + : ""; + + const panelTitle = row ? ( +
+ + · + {subtitle} +
+ ) : isEn ? ( + "Temperature Chart" + ) : ( + "气温图表" + ); const timeframeActions = (
diff --git a/frontend/components/dashboard/scan-terminal/Panel.tsx b/frontend/components/dashboard/scan-terminal/Panel.tsx index 9cfb9740..fa77e8ab 100644 --- a/frontend/components/dashboard/scan-terminal/Panel.tsx +++ b/frontend/components/dashboard/scan-terminal/Panel.tsx @@ -10,7 +10,7 @@ export function Panel({ }: { children: React.ReactNode; className?: string; - title: string; + title: React.ReactNode; actions?: React.ReactNode; }) { return ( diff --git a/frontend/components/dashboard/scan-terminal/WeatherDecisionBand.tsx b/frontend/components/dashboard/scan-terminal/WeatherDecisionBand.tsx deleted file mode 100644 index fb7d48f0..00000000 --- a/frontend/components/dashboard/scan-terminal/WeatherDecisionBand.tsx +++ /dev/null @@ -1,35 +0,0 @@ -"use client"; - -import clsx from "clsx"; -import type { WeatherDecisionView } from "@/components/dashboard/scan-terminal/city-card-decision-utils"; - -export function WeatherDecisionBand({ - decisionView, - decisionWhyText, - isEn, - paceDeltaText, -}: { - decisionView: WeatherDecisionView; - decisionWhyText: string; - isEn: boolean; - paceDeltaText: string; -}) { - return ( -
-
- {decisionView.kicker} - {decisionView.action} -

{decisionWhyText}

-
-
- - {isEn ? "Weather range" : "天气区间"} - {decisionView.targetRange} - - - {isEn ? "Path delta" : "路径偏差"} {paceDeltaText} - -
-
- ); -} diff --git a/frontend/components/ops/health/HealthPageClient.tsx b/frontend/components/ops/health/HealthPageClient.tsx index 52625961..5e4008e7 100644 --- a/frontend/components/ops/health/HealthPageClient.tsx +++ b/frontend/components/ops/health/HealthPageClient.tsx @@ -27,8 +27,6 @@ const LABELS: Record = { hko: "HKO (香港)", singapore_mss: "Singapore MSS", cwa: "CWA (台湾)", - polymarket_gamma: "Polymarket Gamma", - polymarket_clob: "Polymarket CLOB", amos: "AMOS (韩国跑道)", amsc_awos: "AMSC AWOS (中国)", noaa_wrh: "NOAA WRH (美国结算)", diff --git a/frontend/content/docs/docs.ts b/frontend/content/docs/docs.ts index 725c4cf5..e781170b 100644 --- a/frontend/content/docs/docs.ts +++ b/frontend/content/docs/docs.ts @@ -492,7 +492,7 @@ export const DOCS_PAGES: DocsPage[] = [ { type: "bullets", items: [ - "自动识别当前 Polymarket 页面中的城市,也支持手动切换。", + "自动识别当前页面中的城市,也支持手动切换。", "展示城市档案:结算站点、站点距离、观测更新时间、周边站点数量。", "展示今日日内走势(简版):DEB 走势与机场主站实况 / 官方增强站网对照,可悬停查看时间与温度。", "展示多日最高温预报(简版),并提供一键刷新与跳转主站入口。", @@ -566,7 +566,7 @@ export const DOCS_PAGES: DocsPage[] = [ { type: "bullets", items: [ - "Auto-detects the current Polymarket page city, with manual switching also available.", + "Auto-detects the current page city, with manual switching also available.", "Shows a city profile with settlement station, station distance, observation timestamp, and nearby station count.", "Shows a compact intraday chart with DEB versus airport-primary observations and official nearby-network observations, including hoverable time and temperature.", "Shows a compact multi-day daily-high forecast, plus refresh and jump-to-site actions.", diff --git a/src/data_collection/city_registry.py b/src/data_collection/city_registry.py index a6f9d3bf..780d0f29 100644 --- a/src/data_collection/city_registry.py +++ b/src/data_collection/city_registry.py @@ -151,7 +151,7 @@ CITY_REGISTRY = { "risk_emoji": "🟡", "airport_name": "流浮山天文台站", "distance_km": 0.0, - "warning": "香港天文台流浮山站分钟级观测,Polymarket 深圳温度市场结算源;非机场 METAR,不可与 ZGSZ/VHHH 报文混用。", + "warning": "香港天文台流浮山站分钟级观测,深圳温度市场结算源;非机场 METAR,不可与 ZGSZ/VHHH 报文混用。", }, "taipei": { "name": "Taipei", diff --git a/src/data_collection/city_risk_profiles.py b/src/data_collection/city_risk_profiles.py index a98d4caa..b831a728 100644 --- a/src/data_collection/city_risk_profiles.py +++ b/src/data_collection/city_risk_profiles.py @@ -1,4 +1,4 @@ -# Polymarket 城市温度市场 - 数据偏差风险档案 +# 城市温度市场 - 数据偏差风险档案 # 基于 METAR 机场站与市区实际温度的系统性差异 from src.data_collection.city_registry import CITY_REGISTRY diff --git a/src/data_collection/weather_sources.py b/src/data_collection/weather_sources.py index a5063bff..afb9cf31 100644 --- a/src/data_collection/weather_sources.py +++ b/src/data_collection/weather_sources.py @@ -37,7 +37,7 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour - NWS (US National Weather Service) - MGM (Turkish Meteorological Service) - JMA / HKO / CWA (country official networks) - - Polymarket (weather derivative markets) + - Weather derivative markets """ from src.data_collection.city_registry import CITY_REGISTRY @@ -612,7 +612,7 @@ class WeatherDataCollector(OpenMeteoCacheMixin, SettlementSourceMixin, MetarSour def extract_city_from_question(self, question: str) -> Optional[str]: """ - 从 Polymarket 问题描述或 Slug 中提取城市名称 + 从市场问题描述或 Slug 中提取城市名称 """ q = question.lower() diff --git a/src/utils/telegram_push.py b/src/utils/telegram_push.py index 9d9899bb..65204460 100644 --- a/src/utils/telegram_push.py +++ b/src/utils/telegram_push.py @@ -595,7 +595,7 @@ HIGH_FREQ_AIRPORT_ICAO = { "san francisco": "KSFO", "houston": "KHOU", "dallas": "KDAL", "austin": "KAUS", "seattle": "KSEA", } -# Settlement runway mapping — matches Polymarket settlement anchor stations. +# Settlement runway mapping — matches settlement anchor stations. # Format: (low_number, high_number) order-independent; stored sorted for lookup. SETTLEMENT_RUNWAY_PAIRS: Dict[str, Set[Tuple[str, str]]] = { "shanghai": {("17L", "35R")}, diff --git a/web/scan_terminal_city_row.py b/web/scan_terminal_city_row.py index 98992033..ff5e03b3 100644 --- a/web/scan_terminal_city_row.py +++ b/web/scan_terminal_city_row.py @@ -148,7 +148,7 @@ def _scan_city_terminal_rows_quick( *, force_refresh: bool = False, ) -> Dict[str, Any]: - """Fast path that skips Polymarket matching — returns a single row per city + """Fast path that returns cached analysis rows only — returns a single row per city with cached analysis data (Obs, DEB, probabilities) but no market prices.""" data = _analyze( city,