"use client"; import clsx from "clsx"; import Link from "next/link"; import { Bell, Menu, RefreshCw, UserRound, X, } from "lucide-react"; import { startTransition, useDeferredValue, useEffect, useMemo, useState, } from "react"; import styles from "./Dashboard.module.css"; import detailChromeStyles from "./DetailPanelChrome.module.css"; import modalChromeStyles from "./ModalChrome.module.css"; import { FilterState, ScanFilterPanel, } from "@/components/dashboard/ScanFilterPanel"; import { MapCanvas } from "@/components/dashboard/MapCanvas"; import { getWindowPhaseMeta } from "@/components/dashboard/OpportunityTable"; import { ScanKPIBar } from "@/components/dashboard/ScanKPIBar"; import { OpportunityTable } from "@/components/dashboard/OpportunityTable"; import { DashboardStoreProvider, useDashboardStore, } from "@/hooks/useDashboardStore"; import { I18nProvider, useI18n } from "@/hooks/useI18n"; import { dashboardClient } from "@/lib/dashboard-client"; import type { DistributionPreviewPoint, MarketScan, PrimarySignal, ScanOpportunityRow, ScanTerminalResponse, } from "@/lib/dashboard-types"; import { getLocalizedAirportName, getLocalizedCityName, } from "@/lib/dashboard-home-copy"; import { formatTemperatureValue, normalizeTemperatureLabel, } from "@/lib/dashboard-utils"; const DEFAULT_FILTERS: FilterState = { scan_mode: "tradable", min_price: 0.05, max_price: 0.95, min_edge_pct: 2, min_liquidity: 1000, high_liquidity_only: false, market_type: "maxtemp", time_range: "today", limit: 28, }; const NAV_ITEMS = [ { zh: "扫描台", en: "Terminal" }, { zh: "市场", en: "Markets" }, { zh: "分析", en: "Analysis" }, { zh: "组合", en: "Portfolio" }, { zh: "监控", en: "Monitor" }, { zh: "设置", en: "Settings" }, ]; type TopSection = "terminal" | "markets" | "analysis" | "portfolio" | "monitor" | "settings"; type ContentView = "list" | "map" | "calendar"; const TOP_SECTION_ORDER: TopSection[] = [ "terminal", "markets", "analysis", "portfolio", "monitor", "settings", ]; function formatPercent(value?: number | null, signed = false) { if (value == null || Number.isNaN(Number(value))) return "--"; const numeric = Number(value); return `${signed && numeric >= 0 ? "+" : ""}${numeric.toFixed(1)}%`; } function formatProbability(value?: number | null) { if (value == null || Number.isNaN(Number(value))) return "--"; return `${(Number(value) * 100).toFixed(0)}%`; } function formatPrice(value?: number | null) { if (value == null || Number.isNaN(Number(value))) return "--"; return `${Math.round(Number(value) * 100)}¢`; } function formatVolume(value?: number | null) { const numeric = Number(value || 0); if (!Number.isFinite(numeric) || numeric <= 0) return "--"; if (numeric >= 1_000_000) return `$${(numeric / 1_000_000).toFixed(1)}M`; if (numeric >= 1_000) return `$${(numeric / 1_000).toFixed(0)}K`; return `$${numeric.toFixed(0)}`; } function formatRemainingWindow(value?: number | null, locale = "zh-CN") { if (value == null || !Number.isFinite(Number(value))) return "--"; const numeric = Math.max(0, Math.round(Number(value))); const hours = Math.floor(numeric / 60); const minutes = numeric % 60; if (locale === "en-US") { if (hours <= 0) return `${minutes}m`; return `${hours}h ${minutes}m`; } if (hours <= 0) return `${minutes} 分钟`; return `${hours}h ${minutes}m`; } function scoreTone(score?: number | null) { const numeric = Number(score || 0); if (numeric >= 85) return "green"; if (numeric >= 70) return "yellow"; return "red"; } function confidenceLabel(score?: number | null, locale = "zh-CN") { const numeric = Number(score || 0); if (locale === "en-US") { if (numeric >= 85) return "High"; if (numeric >= 70) return "Medium"; return "Watch"; } if (numeric >= 85) return "高"; if (numeric >= 70) return "中"; return "观察"; } function formatShortDate(value?: string | null, locale = "zh-CN") { const text = String(value || "").trim(); if (!text) return "--"; const date = new Date(`${text}T00:00:00`); if (Number.isNaN(date.getTime())) return text; return locale === "en-US" ? date.toLocaleDateString("en-US", { month: "short", day: "numeric" }) : date.toLocaleDateString("zh-CN", { month: "numeric", day: "numeric" }); } function getSideRow( marketScan: MarketScan | null | undefined, selectedRow: ScanOpportunityRow, side: "yes" | "no", ) { const rows = Array.isArray(marketScan?.scan_rows) ? marketScan.scan_rows : []; const matched = rows.find( (row) => row.market_slug === selectedRow.market_slug && row.selected_date === selectedRow.selected_date && row.side === side, ); if (matched) return matched; if (selectedRow.side === side) return selectedRow; return null; } function getDetailSideAsk( scanRow: ScanOpportunityRow | null, marketScan: MarketScan | null | undefined, side: "yes" | "no", ) { if (scanRow?.ask != null) return scanRow.ask; if (side === "yes") { if (scanRow?.yes_ask != null) return scanRow.yes_ask; return marketScan?.yes_buy ?? null; } if (scanRow?.no_ask != null) return scanRow.no_ask; return marketScan?.no_buy ?? null; } function getDetailSideBid( scanRow: ScanOpportunityRow | null, marketScan: MarketScan | null | undefined, side: "yes" | "no", ) { if (scanRow?.bid != null) return scanRow.bid; if (side === "yes") { if (scanRow?.yes_bid != null) return scanRow.yes_bid; return marketScan?.yes_sell ?? null; } if (scanRow?.no_bid != null) return scanRow.no_bid; return marketScan?.no_sell ?? null; } function buildComparisonBuckets( marketScan: MarketScan | null | undefined, row: ScanOpportunityRow | null, ) { const rowPreview = Array.isArray(row?.distribution_preview) ? row.distribution_preview.filter( (item): item is DistributionPreviewPoint => Boolean(item && (item.label || item.value != null)), ) : []; if (rowPreview.length) { return rowPreview.slice(0, 6).map((item) => ({ label: String(item.label ?? item.value ?? "--"), model: Number(item.model_probability ?? 0) * 100, market: Number(item.market_probability ?? 0) * 100, highlighted: Boolean(item.highlighted), })); } const buckets = Array.isArray(marketScan?.top_buckets) ? marketScan?.top_buckets : Array.isArray(marketScan?.all_buckets) ? marketScan?.all_buckets?.slice(0, 6) : []; if (buckets.length) { return buckets .slice(0, 6) .map((bucket) => ({ label: String(bucket.temp ?? bucket.value ?? bucket.label ?? "--"), model: Number(bucket.probability ?? 0) * 100, market: Number(bucket.market_price ?? bucket.yes_buy ?? 0) * 100, highlighted: false, })) .filter((bucket) => bucket.label !== "--"); } if (!row) return []; return [ { label: row.target_label || "--", model: Number(row.model_event_probability || 0) * 100, market: Number(row.market_event_probability || 0) * 100, highlighted: true, }, ]; } function DetailPanel({ row, marketScan, loading, }: { row: ScanOpportunityRow | null; marketScan?: MarketScan | null; loading?: boolean; }) { const { locale } = useI18n(); const isEn = locale === "en-US"; if (!row) { return ( ); } const localizedCityName = getLocalizedCityName( row.city, row.city_display_name || row.display_name || row.city, locale, ); const localizedAirport = getLocalizedAirportName(row.city, row.airport || "", locale); const detailSignal = marketScan?.primary_signal as PrimarySignal | null | undefined; const displayRow = detailSignal && detailSignal.market_slug === row.market_slug ? detailSignal : row; const tempSymbol = row.temp_symbol || displayRow.temp_symbol || "°C"; const yesRow = getSideRow(marketScan, row, "yes"); const noRow = getSideRow(marketScan, row, "no"); const comparisonBuckets = buildComparisonBuckets(marketScan, row); const maxBar = Math.max( 1, ...comparisonBuckets.flatMap((bucket) => [bucket.model, bucket.market]), ); const scoreClass = scoreTone(displayRow.final_score); const phaseMeta = getWindowPhaseMeta(displayRow, locale); return ( ); } function CalendarView({ rows, locale, selectedRowId, onSelectRow, }: { rows: ScanOpportunityRow[]; locale: string; selectedRowId: string | null; onSelectRow: (row: ScanOpportunityRow) => void; }) { const groups = useMemo(() => { const byDate = new Map(); rows.forEach((row) => { const key = String(row.selected_date || row.local_date || "unknown"); const list = byDate.get(key) || []; list.push(row); byDate.set(key, list); }); return Array.from(byDate.entries()).sort(([left], [right]) => left.localeCompare(right)); }, [rows]); if (!groups.length) { return (
{locale === "en-US" ? "No dated opportunities" : "当前没有日期机会"}
); } return (
{groups.map(([date, items]) => (
{formatShortDate(date, locale)}
{locale === "en-US" ? `${items.length} rows` : `${items.length} 条`}
{items.map((row) => ( ))}
))}
); } function OverviewMapView({ locale }: { locale: string }) { const store = useDashboardStore(); return (
{locale === "en-US" ? `Monitoring ${store.cities.length} cities on the original map canvas.` : `正在用原地图画布监控 ${store.cities.length} 个城市。`}
); } function PortfolioView({ rows, locale, }: { rows: ScanOpportunityRow[]; locale: string; }) { const yesCount = rows.filter((row) => row.side === "yes").length; const noCount = rows.filter((row) => row.side === "no").length; const avgScore = rows.length > 0 ? rows.reduce((sum, row) => sum + Number(row.final_score || 0), 0) / rows.length : null; return (
{locale === "en-US" ? "YES Ideas" : "YES 机会"}
{yesCount}
{locale === "en-US" ? "NO Ideas" : "NO 机会"}
{noCount}
{locale === "en-US" ? "Avg Score" : "平均评分"}
{avgScore != null ? `${avgScore.toFixed(0)}/100` : "--"}
); } function MonitorView({ rows, locale, }: { rows: ScanOpportunityRow[]; locale: string; }) { const groups = useMemo(() => { const result = new Map(); rows.forEach((row) => { const label = getWindowPhaseMeta(row, locale).label; result.set(label, (result.get(label) || 0) + 1); }); return Array.from(result.entries()); }, [rows, locale]); return (
{groups.map(([label, count]) => (
{label}
{count}
))}
); } function SettingsView({ locale }: { locale: string }) { return (
{locale === "en-US" ? "Market Discovery" : "市场发现"}
{locale === "en-US" ? "Gamma REST refreshes every 60s." : "Gamma REST 每 60 秒刷新一次。"}
{locale === "en-US" ? "Price Layer" : "价格层"}
{locale === "en-US" ? "CLOB REST prices and books refresh every 30s." : "CLOB REST 价格和盘口每 30 秒刷新一次。"}
{locale === "en-US" ? "Server Profile" : "服务器配置"}
{locale === "en-US" ? "2GB profile: one polling loop, cached REST, no websocket fan-out." : "2G 配置:单轮询、REST 缓存优先、不做 websocket 扇出。"}
); } function ScanTerminalScreen() { const store = useDashboardStore(); const { locale, toggleLocale } = useI18n(); const isEn = locale === "en-US"; const accountHref = store.proAccess.authenticated ? "/account" : "/auth/login?next=%2Faccount"; const [draftFilters, setDraftFilters] = useState(DEFAULT_FILTERS); const [activeFilters, setActiveFilters] = useState(DEFAULT_FILTERS); const [terminalData, setTerminalData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [selectedRowId, setSelectedRowId] = useState(null); const [detailByRowId, setDetailByRowId] = useState>({}); const [detailLoadingId, setDetailLoadingId] = useState(null); const [activeSection, setActiveSection] = useState("terminal"); const [activeView, setActiveView] = useState("list"); const deferredRows = useDeferredValue(terminalData?.rows || []); const selectedRow = useMemo(() => { if (!deferredRows.length) return null; return deferredRows.find((row) => row.id === selectedRowId) || deferredRows[0] || null; }, [deferredRows, selectedRowId]); const fetchTerminal = async (filters: FilterState, force = false) => { setLoading(true); setError(null); try { const response = await dashboardClient.getScanTerminal(filters, { force }); startTransition(() => { setTerminalData(response); setActiveFilters(filters); setSelectedRowId((current) => { if (current && response.rows.some((row) => row.id === current)) { return current; } return response.top_signal?.id || response.rows[0]?.id || null; }); }); } catch (fetchError) { setError(String(fetchError)); } finally { setLoading(false); } }; const fetchDetail = async (row: ScanOpportunityRow) => { if (!row.market_slug || !row.selected_date) return; if (detailByRowId[row.id] !== undefined) return; setDetailLoadingId(row.id); try { const response = await dashboardClient.getCityMarketScan(row.city, { force: false, marketSlug: row.market_slug, targetDate: row.selected_date, }); setDetailByRowId((current) => ({ ...current, [row.id]: response.market_scan || null, })); } catch { setDetailByRowId((current) => ({ ...current, [row.id]: null, })); } finally { setDetailLoadingId((current) => (current === row.id ? null : current)); } }; useEffect(() => { void fetchTerminal(DEFAULT_FILTERS, false); }, []); useEffect(() => { const intervalId = window.setInterval(() => { void fetchTerminal(activeFilters, false); }, 30_000); return () => window.clearInterval(intervalId); }, [activeFilters]); useEffect(() => { if (!selectedRow) return; void fetchDetail(selectedRow); }, [selectedRow, detailByRowId]); const selectedDetail = selectedRow ? detailByRowId[selectedRow.id] : null; const resolvedView: ContentView = activeSection === "markets" ? "map" : activeSection === "analysis" ? "calendar" : activeView; const renderMainView = () => { if (activeSection === "portfolio") { return ; } if (activeSection === "monitor") { return ; } if (activeSection === "settings") { return ; } if (resolvedView === "map") { return ; } if (resolvedView === "calendar") { return ( setSelectedRowId(row.id)} /> ); } return ( <> setSelectedRowId(row.id)} /> {deferredRows.length ? (
) : null} ); }; return (
{ setDraftFilters(filters); void fetchTerminal(filters, true); }} isScanning={loading} />
{NAV_ITEMS.map((item, index) => ( ))}
{selectedRow?.local_time || terminalData?.generated_at?.replace("T", " ").slice(11, 19) || "--"}

{isEn ? "Tradable Opportunities" : "可交易机会"}

{isEn ? "Use EMOS distribution, live order book, and timing windows to isolate the one actionable signal." : "基于当前时间、实况数据和模型预测,筛选出最具交易价值的市场。"}

{isEn ? resolvedView === "calendar" ? "Sort: Date" : resolvedView === "map" ? "Sort: City Map" : "Sort: Score" : resolvedView === "calendar" ? "排序:日期" : resolvedView === "map" ? "排序:地图" : "排序:综合得分"}
{error ? (
{isEn ? "Scan failed" : "扫描失败"}
{error}
) : ( renderMainView() )}
); } export function ScanTerminalDashboard() { return ( ); }