"use client"; import clsx from "clsx"; import Link from "next/link"; import { LogIn, Moon, Sun, UserRound, } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState, } from "react"; import styles from "./Dashboard.module.css"; import detailChromeStyles from "./DetailPanelChrome.module.css"; import modalChromeStyles from "./ModalChrome.module.css"; import { DetailPanel as CityDetailPanel } from "@/components/dashboard/DetailPanel"; import { FutureForecastModal } from "@/components/dashboard/FutureForecastModal"; import { MapCanvas } from "@/components/dashboard/MapCanvas"; import { ProFeaturePaywall } from "@/components/dashboard/ProFeaturePaywall"; import { DashboardStoreProvider, useDashboardStore, } from "@/hooks/useDashboardStore"; import { I18nProvider, useI18n } from "@/hooks/useI18n"; import type { CityDetail, ScanOpportunityRow, ScanTerminalResponse, } from "@/lib/dashboard-types"; import { getLocalizedCityName } from "@/lib/dashboard-home-copy"; import { AiPinnedForecastView } from "@/components/dashboard/scan-terminal/AiPinnedForecastView"; import { CalendarView } from "@/components/dashboard/scan-terminal/CalendarView"; import { AiForecastKPIBar } from "@/components/dashboard/scan-terminal/AiForecastKPIBar"; import { LoadingSignal } from "@/components/dashboard/scan-terminal/LoadingSignal"; import { OpportunityOverview } from "@/components/dashboard/scan-terminal/OpportunityOverview"; import { findDetailForCity, isFullEnoughForDeepAnalysis, waitForDeepAnalysisQueue, } from "@/components/dashboard/scan-terminal/city-detail-utils"; import type { AiPinnedCity } from "@/components/dashboard/scan-terminal/types"; import { findRowForCity, formatUserLocalTime, normalizeCityKey, prettifyCityName, rowMatchesCity, sortRowsByUserTime, } from "@/components/dashboard/scan-terminal/decision-utils"; type ContentView = "opportunities" | "analysis" | "map" | "calendar"; type ThemeMode = "dark" | "light"; function ScanTerminalScreen() { const store = useDashboardStore(); const { locale, toggleLocale } = useI18n(); const isEn = locale === "en-US"; const isPro = store.proAccess.subscriptionActive; const accountHref = store.proAccess.authenticated ? "/account" : "/auth/login?next=%2Faccount"; const [terminalData, setTerminalData] = useState(null); const [scanLoading, setScanLoading] = useState(false); const [scanError, setScanError] = useState(null); const [selectedRowId, setSelectedRowId] = useState(null); const [activeView, setActiveView] = useState("opportunities"); const [mapSelectedCityName, setMapSelectedCityName] = useState(null); const [aiPinnedCities, setAiPinnedCities] = useState([]); const [showScanPaywall, setShowScanPaywall] = useState(false); const [userLocalTime, setUserLocalTime] = useState("--"); const [themeMode, setThemeMode] = useState("dark"); const lastMapSelectedCityRef = useRef(""); const aiFullHydrationRef = useRef>(new Set()); const aiHydrationQueueRef = useRef([]); const aiHydrationRunningRef = useRef(false); const timeSortedRows = useMemo( () => sortRowsByUserTime(terminalData?.rows || []), [terminalData?.rows], ); const selectedRow = useMemo(() => { if (!timeSortedRows.length) return null; return timeSortedRows.find((row) => row.id === selectedRowId) || timeSortedRows[0] || null; }, [timeSortedRows, selectedRowId]); useEffect(() => { if (!timeSortedRows.length) return; if (selectedRowId && timeSortedRows.some((row) => row.id === selectedRowId)) return; setSelectedRowId(timeSortedRows[0].id); }, [selectedRowId, timeSortedRows]); const mapFocusedRow = useMemo(() => { return findRowForCity( timeSortedRows, mapSelectedCityName || store.selectedCity, ); }, [mapSelectedCityName, store.selectedCity, timeSortedRows]); const kpiCityName = mapSelectedCityName || store.selectedCity || aiPinnedCities[0]?.cityName || null; const kpiDetail = findDetailForCity(store.cityDetailsByName, kpiCityName) || (store.selectedDetail && normalizeCityKey(store.selectedDetail.name) === normalizeCityKey(kpiCityName) ? store.selectedDetail : null); const kpiRow = findRowForCity(timeSortedRows, kpiCityName); const mapFallbackRow = useMemo(() => { const rawCityName = mapSelectedCityName || store.selectedCity; const cityKey = normalizeCityKey(rawCityName); if (!cityKey || mapFocusedRow) return null; const selectedDetail = store.selectedDetail && normalizeCityKey(store.selectedDetail.name) === cityKey ? store.selectedDetail : Object.values(store.cityDetailsByName).find( (detail) => normalizeCityKey(detail?.name) === cityKey, ) || null; const selectedSummary = Object.values(store.citySummariesByName).find( (summary) => normalizeCityKey(summary?.name) === cityKey, ) || null; const selectedCityItem = store.cities.find( (city) => normalizeCityKey(city.name) === cityKey || normalizeCityKey(city.display_name) === cityKey, ) || null; const canonicalCity = selectedDetail?.name || selectedSummary?.name || selectedCityItem?.name || String(rawCityName || "").trim(); if (!canonicalCity) return null; const tempSymbol = selectedDetail?.temp_symbol || selectedSummary?.temp_symbol || (selectedCityItem?.temp_unit === "fahrenheit" ? "°F" : "°C"); const displayName = selectedDetail?.display_name || selectedSummary?.display_name || selectedCityItem?.display_name || canonicalCity; const currentTemp = selectedDetail?.current?.temp ?? selectedSummary?.current?.temp ?? null; return { id: `map-city:${canonicalCity}`, city: canonicalCity, city_display_name: displayName, display_name: displayName, selected_date: selectedDetail?.local_date || null, local_date: selectedDetail?.local_date || null, local_time: selectedDetail?.local_time || selectedSummary?.local_time || null, temp_symbol: tempSymbol, current_temp: currentTemp, current_max_so_far: selectedDetail?.current?.max_so_far ?? currentTemp ?? null, deb_prediction: selectedDetail?.deb?.prediction ?? selectedSummary?.deb?.prediction ?? null, airport: selectedDetail?.risk?.airport || selectedCityItem?.airport || selectedCityItem?.settlement_station_label || null, risk_level: selectedDetail?.risk?.level || selectedSummary?.risk?.level || selectedCityItem?.risk_level || "low", market_slug: null, market_question: isEn ? "City briefing" : "城市简报", target_label: isEn ? "City snapshot" : "城市概况", side: null, edge_percent: null, final_score: null, window_phase: "city_snapshot", tradable: false, active: false, closed: false, accepting_orders: false, } satisfies ScanOpportunityRow; }, [ isEn, mapFocusedRow, mapSelectedCityName, store.cityDetailsByName, store.citySummariesByName, store.cities, store.selectedCity, store.selectedDetail, ]); useEffect(() => { if (!store.proAccess.loading && !isPro && activeView === "calendar") { setActiveView("map"); } }, [activeView, isPro, store.proAccess.loading]); useEffect(() => { if (store.proAccess.loading) return; if (!isPro) { setScanLoading(false); setScanError(null); setTerminalData(null); return; } let cancelled = false; const controller = new AbortController(); setScanLoading(true); setScanError(null); const params = new URLSearchParams({ scan_mode: "tradable", min_price: "0.05", max_price: "0.95", min_edge_pct: "2", min_liquidity: "500", market_type: "maxtemp", time_range: "today", limit: "36", }); fetch(`/api/scan/terminal?${params.toString()}`, { cache: "no-store", signal: controller.signal, }) .then(async (response) => { if (!response.ok) { let message = `HTTP ${response.status}`; try { const payload = await response.json(); message = String(payload?.error || payload?.detail || message); } catch { // Keep HTTP status message. } throw new Error(message); } return response.json() as Promise; }) .then((payload) => { if (cancelled) return; setTerminalData(payload); setScanError(null); }) .catch((error) => { if (cancelled || controller.signal.aborted) return; setScanError(error instanceof Error ? error.message : String(error)); }) .finally(() => { if (!cancelled) { setScanLoading(false); } }); return () => { cancelled = true; controller.abort(); }; }, [isPro, store.proAccess.loading]); useEffect(() => { setUserLocalTime(formatUserLocalTime()); const intervalId = window.setInterval(() => { setUserLocalTime(formatUserLocalTime()); }, 10_000); return () => window.clearInterval(intervalId); }, []); useEffect(() => { const stored = window.localStorage.getItem("polyweather_scan_theme"); if (stored === "light") { setThemeMode("light"); } }, []); useEffect(() => { window.localStorage.setItem("polyweather_scan_theme", themeMode); }, [themeMode]); const resolvedView: ContentView = activeView; const mapFocusedCity = mapSelectedCityName || store.selectedCity; const activeDetailRow = resolvedView === "map" && mapFocusedCity ? mapFocusedRow || mapFallbackRow : selectedRow; const scanStatus = terminalData?.status || "ready"; const staleReason = terminalData?.stale_reason || null; useEffect(() => { if (!activeDetailRow) return; if (!findDetailForCity(store.cityDetailsByName, activeDetailRow.city)) { void store.ensureCityDetail(activeDetailRow.city, false, "panel").catch(() => {}); } }, [activeDetailRow, store.cityDetailsByName, store.ensureCityDetail]); const runAiHydrationQueue = useCallback(async () => { if (aiHydrationRunningRef.current) return; aiHydrationRunningRef.current = true; try { while (aiHydrationQueueRef.current.length > 0) { const nextCity = aiHydrationQueueRef.current.shift(); const key = normalizeCityKey(nextCity || ""); if (!nextCity || !key) continue; const existingDetail = findDetailForCity(store.cityDetailsByName, nextCity); try { const detail = await store.ensureCityDetail( nextCity, Boolean(existingDetail) && !isFullEnoughForDeepAnalysis(existingDetail), "full", ); if (!isFullEnoughForDeepAnalysis(detail)) { aiFullHydrationRef.current.delete(key); } } catch { aiFullHydrationRef.current.delete(key); } await waitForDeepAnalysisQueue(1200); } } finally { aiHydrationRunningRef.current = false; if (aiHydrationQueueRef.current.length > 0) { void runAiHydrationQueue(); } } }, [store.cityDetailsByName, store.ensureCityDetail]); const queueAiFullHydration = useCallback( (cityName: string) => { const key = normalizeCityKey(cityName); if (!key || aiFullHydrationRef.current.has(key)) return; aiFullHydrationRef.current.add(key); aiHydrationQueueRef.current.push(cityName); void runAiHydrationQueue(); }, [runAiHydrationQueue], ); const addAiPinnedCity = useCallback((cityName: string) => { const cleanName = String(cityName || "").trim(); const key = normalizeCityKey(cleanName); if (!key) return; const matchedRow = findRowForCity(timeSortedRows, cleanName); const prettyName = prettifyCityName(cleanName); const displayName = matchedRow?.city_display_name || matchedRow?.display_name || getLocalizedCityName(cleanName, prettyName || cleanName, locale) || prettyName || cleanName; setAiPinnedCities((current) => { const existing = current.findIndex( (item) => normalizeCityKey(item.cityName) === key, ); const nextItem = { cityName: matchedRow?.city || cleanName, displayName, addedAt: Date.now(), }; if (existing >= 0) { const next = [...current]; next[existing] = { ...next[existing], ...nextItem }; return [ next[existing], ...next.filter((_, index) => index !== existing), ]; } return [nextItem, ...current].slice(0, 8); }); queueAiFullHydration(matchedRow?.city || cleanName); }, [locale, queueAiFullHydration, timeSortedRows]); const removeAiPinnedCity = useCallback((cityName: string) => { const key = normalizeCityKey(cityName); aiFullHydrationRef.current.delete(key); aiHydrationQueueRef.current = aiHydrationQueueRef.current.filter( (queuedCity) => normalizeCityKey(queuedCity) !== key, ); setAiPinnedCities((current) => current.filter((item) => normalizeCityKey(item.cityName) !== key), ); }, []); useEffect(() => { aiPinnedCities.forEach((item) => { const key = normalizeCityKey(item.cityName); if (!key || aiFullHydrationRef.current.has(key)) return; const detail = findDetailForCity(store.cityDetailsByName, item.cityName); const needsFullHydration = !isFullEnoughForDeepAnalysis(detail); if (!needsFullHydration) return; queueAiFullHydration(item.cityName); }); }, [aiPinnedCities, queueAiFullHydration, store.cityDetailsByName]); const handleMapCitySelect = useCallback((cityName: string) => { setMapSelectedCityName(cityName); lastMapSelectedCityRef.current = normalizeCityKey(cityName); const matchedRow = findRowForCity(timeSortedRows, cityName); setSelectedRowId(matchedRow?.id || null); addAiPinnedCity(cityName); setActiveView("analysis"); }, [addAiPinnedCity, timeSortedRows]); useEffect(() => { if (activeView !== "map") return; const selectedCity = String(store.selectedCity || "").trim(); const selectedKey = normalizeCityKey(selectedCity); if (!selectedKey || selectedKey === lastMapSelectedCityRef.current) return; lastMapSelectedCityRef.current = selectedKey; setMapSelectedCityName(selectedCity); const matchedRow = findRowForCity(timeSortedRows, selectedCity); setSelectedRowId(matchedRow?.id || null); addAiPinnedCity(selectedCity); }, [activeView, addAiPinnedCity, store.selectedCity, timeSortedRows]); const handleSelectRow = useCallback((row: ScanOpportunityRow) => { const cityName = row.city || row.city_display_name || row.display_name || ""; if (!cityName) return; setSelectedRowId(row.id); const selectedCityKey = normalizeCityKey(store.selectedCity); const rowCityKey = normalizeCityKey(cityName); const hasCachedDetail = Boolean(findDetailForCity(store.cityDetailsByName, cityName)) || Object.values(store.cityDetailsByName).some((detail) => rowMatchesCity(row, detail?.name || detail?.display_name || ""), ); if (store.isPanelOpen && selectedCityKey === rowCityKey) { if (!hasCachedDetail) { void store.ensureCityDetail(cityName, false, "panel").catch(() => {}); } return; } void store.selectCity(cityName); }, [store]); const handleOpenDecisionRow = useCallback((row: ScanOpportunityRow) => { const cityName = row.city || row.city_display_name || row.display_name || ""; if (!cityName) return; setSelectedRowId(row.id); addAiPinnedCity(cityName); setActiveView("analysis"); void store.selectCity(cityName); }, [addAiPinnedCity, store]); const openScanPaywall = useCallback(() => { setShowScanPaywall(true); }, []); const renderMainView = () => { if (resolvedView === "opportunities") { if (!isPro) { return (
{isEn ? "Opportunity board is Pro" : "今日机会榜需 Pro 权限"}

{isEn ? "Map exploration and city briefing are still available." : "地图探索和城市简报仍可使用。"}

); } return ( setActiveView("map")} /> ); } if (resolvedView === "map") { return (
); } if (resolvedView === "analysis") { return ( ); } if (!isPro) { return (
{isEn ? "Scan is available on Pro" : "扫描功能需 Pro 权限"}
{isEn ? "Distribution view and city briefing remain available." : "分布视图和右侧城市简报仍可查看。"}
); } if (resolvedView === "calendar") { return ( ); } return null; }; if (store.proAccess.loading) { return (
); } return (
{isEn ? "AI Weather Decision Terminal" : "AI 天气交易决策台"} {isEn ? "Start from opportunities, then open city cards to verify weather evidence" : "先看今日机会榜,再打开城市决策卡验证天气证据"}
{userLocalTime} {isPro ? null : store.proAccess.authenticated ? ( ) : ( {isEn ? "Sign in" : "登录"} )} {store.proAccess.authenticated ? ( ) : null}
{terminalData?.stale ? ( {isEn ? "Delayed snapshot" : "延迟快照"} ) : null}
{scanStatus === "failed" && !terminalData ? (
{isEn ? "Scan failed" : "扫描失败"}
{staleReason}
) : ( renderMainView() )}
{showScanPaywall ? (
{ if (event.target === event.currentTarget) { setShowScanPaywall(false); } }} > setShowScanPaywall(false)} />
) : null}
); } export function ScanTerminalDashboard() { return ( ); }