清理最后一批 Polymarket 注释+死代码 WeatherDecisionBand
This commit is contained in:
@@ -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 (
|
||||
<div
|
||||
onClick={onSelectSlot}
|
||||
onClick={() => {
|
||||
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({
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-400 text-center mb-3 max-w-[180px]">
|
||||
{isEn
|
||||
? "Select this card and click a city on the left, or select below:"
|
||||
: "激活此卡片并从左侧选择城市,或从下方直接选择:"}
|
||||
? "Click to choose a city weather chart for this slot."
|
||||
: "点击为该槽位选择一个城市天气图表。"}
|
||||
</div>
|
||||
<select
|
||||
value=""
|
||||
onClick={(e) => e.stopPropagation()} // prevent triggering onSelectSlot
|
||||
onChange={(e) => {
|
||||
if (e.target.value) onSelectCity(e.target.value);
|
||||
}}
|
||||
className="text-[11px] font-semibold text-slate-600 px-2 py-1 rounded border border-slate-300 bg-white outline-none max-w-[160px]"
|
||||
<button
|
||||
type="button"
|
||||
className="text-[11px] font-semibold text-white px-3 py-1.5 rounded bg-blue-600 hover:bg-blue-700 transition-colors shadow-sm outline-none"
|
||||
>
|
||||
<option value="">{isEn ? "Choose city..." : "选择城市..."}</option>
|
||||
{availableCities.map((c) => (
|
||||
<option key={c.city} value={c.city}>
|
||||
{c.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{isEn ? "Choose City..." : "选择城市..."}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -376,6 +369,7 @@ function PolyWeatherTerminal({
|
||||
});
|
||||
const [activeSlotIndex, setActiveSlotIndex] = useState<number>(0);
|
||||
const [maximizedSlotIndex, setMaximizedSlotIndex] = useState<number | null>(null);
|
||||
const [activeSearchSlotIndex, setActiveSearchSlotIndex] = useState<number | null>(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({
|
||||
</div>
|
||||
|
||||
{/* Desktop layout */}
|
||||
<div className="hidden h-full min-h-0 lg:grid lg:grid-cols-[0.96fr_1.72fr] gap-2">
|
||||
<div className="flex min-h-0 flex-col gap-2">
|
||||
<CityRegionList
|
||||
isEn={isEn}
|
||||
rows={filteredRegionRows}
|
||||
selectedCity={slots[activeSlotIndex]}
|
||||
onSelectCity={(city) => handleSelectCityForSlot(activeSlotIndex, city)}
|
||||
slots={slots}
|
||||
activeSlotIndex={activeSlotIndex}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="min-h-0">
|
||||
<div className="hidden h-full min-h-0 lg:block">
|
||||
<div className="h-full w-full min-h-0">
|
||||
{maximizedSlotIndex !== null ? (
|
||||
// Maximized view
|
||||
<div
|
||||
onClick={() => 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 */}
|
||||
<div className="absolute right-[110px] top-[6px] z-20 flex items-center gap-1.5">
|
||||
<div className="absolute right-2 top-[38px] z-20 flex items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
@@ -736,7 +726,21 @@ function PolyWeatherTerminal({
|
||||
row={filteredRegionRows.find((r) => String(r.city || "").toLowerCase() === slots[maximizedSlotIndex]) || null}
|
||||
allRows={filteredRegionRows}
|
||||
compact={false}
|
||||
onSearchClick={() => setActiveSearchSlotIndex(maximizedSlotIndex)}
|
||||
/>
|
||||
|
||||
{activeSearchSlotIndex === maximizedSlotIndex && (
|
||||
<CitySelectorDropdown
|
||||
isEn={isEn}
|
||||
rows={filteredRegionRows}
|
||||
onSelectCity={(city) => {
|
||||
handleSelectCityForSlot(maximizedSlotIndex, city);
|
||||
setActiveSearchSlotIndex(null);
|
||||
}}
|
||||
onClose={() => setActiveSearchSlotIndex(null)}
|
||||
className="absolute left-3 top-9 z-50 w-[280px] bg-white border border-slate-200 rounded shadow-lg p-2"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// 2x2 grid layout
|
||||
@@ -747,15 +751,28 @@ function PolyWeatherTerminal({
|
||||
|
||||
if (!cityInSlot) {
|
||||
return (
|
||||
<EmptySlotCard
|
||||
key={slotIndex}
|
||||
slotIndex={slotIndex}
|
||||
isActive={isSlotActive}
|
||||
isEn={isEn}
|
||||
availableCities={availableCities}
|
||||
onSelectSlot={() => setActiveSlotIndex(slotIndex)}
|
||||
onSelectCity={(city) => handleSelectCityForSlot(slotIndex, city)}
|
||||
/>
|
||||
<div key={slotIndex} className="relative h-full">
|
||||
<EmptySlotCard
|
||||
slotIndex={slotIndex}
|
||||
isActive={isSlotActive}
|
||||
isEn={isEn}
|
||||
onSelectSlot={() => setActiveSlotIndex(slotIndex)}
|
||||
onOpenSearch={() => setActiveSearchSlotIndex(slotIndex)}
|
||||
/>
|
||||
|
||||
{activeSearchSlotIndex === slotIndex && (
|
||||
<CitySelectorDropdown
|
||||
isEn={isEn}
|
||||
rows={filteredRegionRows}
|
||||
onSelectCity={(city) => {
|
||||
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"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 */}
|
||||
<div className="absolute right-[110px] top-[6px] z-20 flex items-center gap-1.5">
|
||||
<div className="absolute right-2 top-[38px] z-20 flex items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
@@ -812,7 +830,21 @@ function PolyWeatherTerminal({
|
||||
row={rowForSlot}
|
||||
allRows={filteredRegionRows}
|
||||
compact={true}
|
||||
onSearchClick={() => setActiveSearchSlotIndex(slotIndex)}
|
||||
/>
|
||||
|
||||
{activeSearchSlotIndex === slotIndex && (
|
||||
<CitySelectorDropdown
|
||||
isEn={isEn}
|
||||
rows={filteredRegionRows}
|
||||
onSelectCity={(city) => {
|
||||
handleSelectCityForSlot(slotIndex, city);
|
||||
setActiveSearchSlotIndex(null);
|
||||
}}
|
||||
onClose={() => setActiveSearchSlotIndex(null)}
|
||||
className="absolute left-3 top-9 z-50 w-[280px] bg-white border border-slate-200 rounded shadow-lg p-2"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
@@ -845,9 +877,9 @@ function ScanTerminalScreen() {
|
||||
hydrated && (proAccess.subscriptionActive || canUseLocalFullAccess);
|
||||
const userLocalTime = useUserLocalClock();
|
||||
const { themeMode } = useScanTerminalTheme();
|
||||
const [selectedRegionKey, setSelectedRegionKey] = useState<string>("east_asia");
|
||||
const [selectedRegionKey, setSelectedRegionKey] = useState<string>("all");
|
||||
const [localTimezoneOffsetSeconds, setLocalTimezoneOffsetSeconds] = useState<number | null>(null);
|
||||
const [useLocalTimezoneDefault, setUseLocalTimezoneDefault] = useState(true);
|
||||
const [useLocalTimezoneDefault, setUseLocalTimezoneDefault] = useState(false);
|
||||
const [visibleRegions, setVisibleRegions] = useState<Set<string>>(() => {
|
||||
try {
|
||||
const stored = localStorage.getItem("polyweather_visible_regions");
|
||||
@@ -938,7 +970,7 @@ function ScanTerminalScreen() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedRegionKey(getDefaultRegion());
|
||||
setSelectedRegionKey("all");
|
||||
setLocalTimezoneOffsetSeconds(-new Date().getTimezoneOffset() * 60);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -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<string, Array<[string, string]>> = {
|
||||
shanghai: [["17L", "35R"]],
|
||||
beijing: [["01", "19"]],
|
||||
|
||||
@@ -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<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [activeTab, setActiveTab] = useState<string>("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 (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={clsx(
|
||||
"flex flex-col bg-white border border-slate-200 rounded-md shadow-2xl overflow-hidden text-xs text-[#202833] animate-in fade-in-50 zoom-in-95 duration-100",
|
||||
className
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()} // Prevent card activation
|
||||
>
|
||||
{/* Search Input Area */}
|
||||
<div className="flex items-center gap-2 p-2 border-b border-slate-100 bg-[#f8fafc]">
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="text-slate-400 hover:text-slate-600 px-1 font-mono"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Region filter tabs */}
|
||||
<div className="flex items-center gap-1.5 px-2.5 py-1.5 border-b border-slate-100 bg-slate-50/50 overflow-x-auto scrollbar-none">
|
||||
{tabs.map((tab) => {
|
||||
const isActive = activeTab === tab.key;
|
||||
return (
|
||||
<button
|
||||
key={tab.key}
|
||||
type="button"
|
||||
onClick={() => setActiveTab(tab.key)}
|
||||
className={clsx(
|
||||
"px-2 py-0.5 text-[10px] font-bold rounded whitespace-nowrap transition-all",
|
||||
isActive
|
||||
? "bg-blue-600 text-white shadow-sm"
|
||||
: "bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-700"
|
||||
)}
|
||||
>
|
||||
{isEn ? tab.labelEn : tab.labelZh}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Scrollable list */}
|
||||
<div className="flex-1 overflow-y-auto max-h-[220px] divide-y divide-slate-50">
|
||||
{filteredRows.length === 0 ? (
|
||||
<div className="p-4 text-center text-slate-400 font-medium">
|
||||
{isEn ? "No matching cities" : "无匹配城市"}
|
||||
</div>
|
||||
) : (
|
||||
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 (
|
||||
<button
|
||||
key={row.id}
|
||||
type="button"
|
||||
onClick={() => onSelectCity(String(row.city || "").toLowerCase())}
|
||||
className="flex w-full items-center justify-between px-3 py-2 text-left hover:bg-blue-50/50 transition-colors group"
|
||||
>
|
||||
<div className="min-w-0 flex-1 pr-3">
|
||||
<div className="font-bold text-slate-800 group-hover:text-blue-600 truncate">
|
||||
{cityName}
|
||||
</div>
|
||||
{row.airport && (
|
||||
<div className="text-[10px] text-slate-400 font-mono truncate">
|
||||
{row.airport}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Weather info */}
|
||||
<div className="flex items-center gap-3 font-mono text-[11px] shrink-0 text-slate-500">
|
||||
{obsTemp !== undefined && obsTemp !== null && (
|
||||
<div className="flex flex-col items-end">
|
||||
<span className="text-[8px] text-slate-400 font-sans scale-90 uppercase">Obs</span>
|
||||
<strong className="text-slate-700 font-bold">
|
||||
{obsTemp}
|
||||
<span className="text-[9px] font-normal font-sans">{symbol}</span>
|
||||
</strong>
|
||||
</div>
|
||||
)}
|
||||
{debPrediction !== undefined && debPrediction !== null && (
|
||||
<div className="flex flex-col items-end">
|
||||
<span className="text-[8px] text-slate-400 font-sans scale-90 uppercase">DEB</span>
|
||||
<strong className="text-orange-600 font-bold">
|
||||
{debPrediction}
|
||||
<span className="text-[9px] font-normal font-sans">{symbol}</span>
|
||||
</strong>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<HourlyForecast>(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 ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onSearchClick}
|
||||
className={clsx(
|
||||
"flex items-center gap-1.5 px-1.5 py-0.5 rounded text-left transition-colors font-bold text-slate-800 outline-none select-none",
|
||||
onSearchClick ? "hover:bg-slate-200/80 cursor-pointer" : ""
|
||||
)}
|
||||
>
|
||||
<span>{rowName(row)}</span>
|
||||
{onSearchClick && <span className="text-[8px] text-slate-400">▼</span>}
|
||||
</button>
|
||||
<span className="text-slate-400 font-normal">·</span>
|
||||
<span className="text-slate-500 font-normal">{subtitle}</span>
|
||||
</div>
|
||||
) : isEn ? (
|
||||
"Temperature Chart"
|
||||
) : (
|
||||
"气温图表"
|
||||
);
|
||||
|
||||
const timeframeActions = (
|
||||
<div className="flex items-center gap-1 rounded bg-[#eef2f6] p-0.5 border border-slate-200">
|
||||
|
||||
@@ -10,7 +10,7 @@ export function Panel({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
title: string;
|
||||
title: React.ReactNode;
|
||||
actions?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
|
||||
@@ -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 (
|
||||
<section className={clsx("scan-ai-decision-band", decisionView.tone)}>
|
||||
<div className="scan-ai-decision-main">
|
||||
<span>{decisionView.kicker}</span>
|
||||
<strong>{decisionView.action}</strong>
|
||||
<p className="scan-ai-decision-why">{decisionWhyText}</p>
|
||||
</div>
|
||||
<div className="scan-ai-decision-metrics">
|
||||
<span>
|
||||
{isEn ? "Weather range" : "天气区间"}
|
||||
<b>{decisionView.targetRange}</b>
|
||||
</span>
|
||||
<span>
|
||||
{isEn ? "Path delta" : "路径偏差"} <b>{paceDeltaText}</b>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -27,8 +27,6 @@ const LABELS: Record<string, string> = {
|
||||
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 (美国结算)",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Polymarket 城市温度市场 - 数据偏差风险档案
|
||||
# 城市温度市场 - 数据偏差风险档案
|
||||
# 基于 METAR 机场站与市区实际温度的系统性差异
|
||||
|
||||
from src.data_collection.city_registry import CITY_REGISTRY
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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")},
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user