"use client"; import clsx from "clsx"; import Link from "next/link"; import { LogIn, Moon, RefreshCw, 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 scanTerminalStyles from "./ScanTerminal.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, } from "@/lib/dashboard-types"; 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 } 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 = "opportunities" | "analysis" | "map" | "calendar"; 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("opportunities"); const [mapSelectedCityName, setMapSelectedCityName] = useState(null); const [showScanPaywall, setShowScanPaywall] = useState(false); const userLocalTime = useUserLocalClock(); const { setThemeMode, themeMode } = useScanTerminalTheme(); const lastMapSelectedCityRef = useRef(""); 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 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]); 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); 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}
{isEn ? "v1.5.5 upgrade" : "v1.5.5 升级公告"} {isEn ? "PolyWeather is now upgraded to v1.5.5" : "网站已升级到 v1.5.5"}

{isEn ? "All members received an extra 7 days. The decision terminal now explains more evidence without making unavailable quotes look like a system failure." : "所有会员已额外延长 7 天。新版会把证据讲得更清楚,也不会让暂无报价看起来像系统故障。"}

  • {isEn ? "DeepSeek airport bulletin read" : "DeepSeek 机场报文解读"}
  • {isEn ? "Action calendar view" : "日历行动视图"}
  • {isEn ? "Local-time peak window" : "本地时间峰值窗口"}
  • {isEn ? "AI evidence guardrails" : "AI 证据护栏"}
{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 ( ); }