"use client"; import clsx from "clsx"; import dynamic from "next/dynamic"; import Link from "next/link"; import { LogIn, MessageCircle, Moon, RefreshCw, Sun, UserRound, } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState, } from "react"; import styles from "./Dashboard.module.css"; import { scanRootClass } from "./scan-root-styles"; import { ProFeaturePaywall } from "@/components/dashboard/ProFeaturePaywall"; import { DashboardStoreProvider, useDashboardStore, } from "@/hooks/useDashboardStore"; import { I18nProvider, useI18n } from "@/hooks/useI18n"; import type { CityDetail, ScanOpportunityRow, } from "@/lib/dashboard-types"; import { AiPinnedForecastView } from "@/components/dashboard/scan-terminal/AiPinnedForecastView"; import { LoadingSignal } from "@/components/dashboard/scan-terminal/LoadingSignal"; import { WelcomeOverlay } from "@/components/dashboard/scan-terminal/WelcomeOverlay"; import { findDetailForCity } from "@/components/dashboard/scan-terminal/city-detail-utils"; import { findRowForCity, normalizeCityKey, rowMatchesCity, sortRowsByUserTime, } from "@/components/dashboard/scan-terminal/decision-utils"; import { useAiPinnedCityWorkspace } from "@/components/dashboard/scan-terminal/use-ai-pinned-city-workspace"; import { useScanTerminalQuery } from "@/components/dashboard/scan-terminal/use-scan-terminal-query"; import { useScanTerminalTheme, useUserLocalClock, } from "@/components/dashboard/scan-terminal/use-scan-terminal-ui-state"; type ContentView = "analysis" | "map"; const CityDetailPanel = dynamic( () => import("@/components/dashboard/DetailPanel").then( (module) => module.DetailPanel, ), { ssr: false }, ); const FutureForecastModal = dynamic( () => import("@/components/dashboard/FutureForecastModal").then( (module) => module.FutureForecastModal, ), { ssr: false }, ); const MapCanvas = dynamic( () => import("@/components/dashboard/MapCanvas").then( (module) => module.MapCanvas, ), { ssr: false }, ); 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 { refreshScanTerminalManually, scanError, scanLoading, terminalData, } = useScanTerminalQuery({ isPro, proAccessLoading: store.proAccess.loading, }); const [selectedRowId, setSelectedRowId] = useState(null); const [activeView, setActiveView] = useState("map"); const [mapSelectedCityName, setMapSelectedCityName] = useState(null); const [showScanPaywall, setShowScanPaywall] = useState(false); const [showAnnouncement, setShowAnnouncement] = useState(false); useEffect(() => { const key = "polyweather_v156_announcement_seen_at"; const seen = localStorage.getItem(key); const now = Date.now(); if (!seen) { localStorage.setItem(key, String(now)); setShowAnnouncement(true); return; } const elapsed = now - Number(seen); setShowAnnouncement(elapsed < 3 * 24 * 60 * 60 * 1000); }, []); const userLocalTime = useUserLocalClock(); const { setThemeMode, themeMode } = useScanTerminalTheme(); const lastMapSelectedCityRef = useRef(""); const scanTerminalRootClassName = clsx( styles.root, scanRootClass, themeMode === "light" && "light", ); const timeSortedRows = useMemo( () => sortRowsByUserTime(terminalData?.rows || []), [terminalData?.rows], ); const { addAiPinnedCity, aiPinnedCities, refreshAiPinnedCityDetail, removeAiPinnedCity, } = useAiPinnedCityWorkspace({ locale, store, timeSortedRows, }); 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 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, ]); 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 handleMapCitySelect = useCallback((cityName: string) => { setMapSelectedCityName(cityName); lastMapSelectedCityRef.current = normalizeCityKey(cityName); const matchedRow = findRowForCity(timeSortedRows, cityName); if (matchedRow) store.preloadCityFromRow(matchedRow); setSelectedRowId(matchedRow?.id || null); addAiPinnedCity(cityName); setActiveView("analysis"); }, [addAiPinnedCity, store, 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); store.preloadCityFromRow(row); 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); store.preloadCityFromRow(row); addAiPinnedCity(cityName); setActiveView("analysis"); void store.selectCity(cityName); }, [addAiPinnedCity, store]); const openScanPaywall = useCallback(() => { setShowScanPaywall(true); }, []); const renderMainView = () => { 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." : "分布视图和右侧城市简报仍可查看。"}
); } return null; }; if (store.proAccess.loading) { return (
{isEn ? "AI Weather Decision Terminal" : "AI 天气交易决策台"} {isEn ? "Start from the map, then open city cards to verify weather evidence" : "从地图选城市,再打开决策卡验证天气证据"}
{userLocalTime}
); } return (
{isEn ? "AI Weather Decision Terminal" : "AI 天气交易决策台"} {isEn ? "Start from the map, then open city cards to verify weather evidence" : "从地图选城市,再打开决策卡验证天气证据"}
{userLocalTime} {isPro ? null : store.proAccess.authenticated ? ( ) : ( {isEn ? "Sign in" : "登录"} )} {store.proAccess.authenticated ? ( ) : null}
{showAnnouncement ? (
{isEn ? "v1.5.6 upgrade" : "v1.5.6 升级公告"} {isEn ? "Scan terminal is upgraded to v1.5.6" : "决策终端已升级到 v1.5.6"}

{isEn ? "City decision cards have been redesigned with a compact hero layout and consistent DEB data source. Sticky headers are removed for smoother scrolling." : "城市决策卡 hero 布局重新设计,三指标并列对比更直观;DEB 数据源统一不再出现不一致;去除顶部固定效果滚动更流畅。"}

  • {isEn ? "Redesigned decision card hero" : "重设计城市决策卡 hero 布局"}
  • {isEn ? "Unified DEB data source" : "统一 DEB 数据源"}
  • {isEn ? "Light theme coverage" : "亮色主题补全覆盖"}
  • {isEn ? "HKO observatory AI read" : "香港天文台观测 AI 解读"}
  • {isEn ? "Smoother scrolling experience" : "滚动体验优化"}
) : null}
{terminalData?.generated_at ? ( {isEn ? "Updated" : "已更新"}{" "} {new Date(terminalData.generated_at).toLocaleTimeString( isEn ? "en-US" : "zh-CN", { hour: "2-digit", minute: "2-digit", }, )} ) : null} {terminalData?.stale ? ( {isEn ? "Delayed snapshot" : "延迟快照"} ) : null} {isPro ? ( ) : null}
{scanStatus === "failed" && !terminalData ? (
{isEn ? "Scan failed" : "扫描失败"}
{staleReason}
) : ( renderMainView() )}
{}} /> {showScanPaywall ? (
{ if (event.target === event.currentTarget) { setShowScanPaywall(false); } }} > setShowScanPaywall(false)} />
) : null}
); } export function ScanTerminalDashboard() { return ( ); }