import { useMemo } from "react"; import clsx from "clsx"; import type { ScanOpportunityRow, ScanTerminalResponse } from "@/lib/dashboard-types"; import { formatTemperatureValue } from "@/lib/dashboard-utils"; import { LoadingSignal } from "@/components/dashboard/scan-terminal/LoadingSignal"; import { formatRowPrice, formatRowProbability, formatRowSignedPercent, getPeakCountdownMeta, getRowDecisionMeta, getRowTemperatureBucket, pickOpportunitySections, } from "@/components/dashboard/scan-terminal/decision-utils"; function OpportunityDecisionCard({ row, locale, selected, onOpenDecision, onSelectRow, }: { row: ScanOpportunityRow; locale: string; selected: boolean; onOpenDecision: (row: ScanOpportunityRow) => void; onSelectRow: (row: ScanOpportunityRow) => void; }) { const isEn = locale === "en-US"; const displayName = row.city_display_name || row.display_name || row.city; const unit = row.temp_symbol || row.target_unit || "°C"; const decision = getRowDecisionMeta(row, locale); const phase = getPeakCountdownMeta(row, locale); const modelProb = row.model_event_probability ?? row.model_probability ?? row.peak_probability ?? null; const marketProb = row.market_event_probability ?? row.market_probability ?? null; const price = row.yes_ask ?? row.ask ?? row.yes_bid ?? row.bid ?? row.midpoint ?? null; const confidence = row.ai_confidence || row.ai_city_confidence || row.ai_forecast_confidence || (row.signal_confidence != null ? formatRowProbability(row.signal_confidence) : "--"); const predicted = row.ai_predicted_max ?? row.deb_prediction ?? row.cluster_center ?? row.current_max_so_far ?? null; const reason = decision.reason.length > 128 ? `${decision.reason.slice(0, 125)}…` : decision.reason; return (
onSelectRow(row)} >
{phase.title} {displayName}
{decision.action}
{isEn ? "Forecast high" : "预测最高温"} {predicted != null ? formatTemperatureValue(predicted, unit, { digits: 1 }) : "--"} {isEn ? "Bucket" : "推荐温度桶"} {getRowTemperatureBucket(row)} {isEn ? "Edge" : "概率差"} {formatRowSignedPercent(row.edge_percent ?? row.gap ?? row.signed_gap)}

{reason}

{isEn ? "Model" : "模型"} {formatRowProbability(modelProb)} {isEn ? "Market" : "市场"} {formatRowProbability(marketProb)} {isEn ? "YES" : "YES"} {formatRowPrice(price)} {isEn ? "Confidence" : "信心"} {confidence}
); } export function OpportunityOverview({ rows, terminalData, loading, error, locale, selectedRowId, onOpenDecision, onSelectRow, onOpenMap, }: { rows: ScanOpportunityRow[]; terminalData: ScanTerminalResponse | null; loading: boolean; error: string | null; locale: string; selectedRowId: string | null; onOpenDecision: (row: ScanOpportunityRow) => void; onSelectRow: (row: ScanOpportunityRow) => void; onOpenMap: () => void; }) { const isEn = locale === "en-US"; const sections = useMemo(() => pickOpportunitySections(rows, locale), [locale, rows]); const visibleSections = sections.filter((section) => section.rows.length > 0); const summary = terminalData?.summary; if (loading) { return (
); } if (error || rows.length === 0) { return (
{isEn ? "No opportunity snapshot yet" : "暂无机会快照"}

{error || (isEn ? "Use the map to add cities, or refresh after the scan backend is ready." : "可以先用地图添加城市;扫描后端就绪后会显示今日机会榜。")}

); } return (
{isEn ? "Today AI opportunity board" : "今日 AI 机会榜"} {isEn ? "Decide first, verify second" : "先看决策,再展开证据"}

{isEn ? "Cards translate weather, METAR and Polymarket pricing into action states." : "把天气、METAR 与 Polymarket 报价先翻译成行动状态,再让你展开验证。"}

{isEn ? "Candidates" : "候选"} {summary?.candidate_total ?? rows.length} {isEn ? "Tradable" : "可交易市场"} {summary?.tradable_market_count ?? "--"} {isEn ? "Avg edge" : "平均概率差"} {formatRowSignedPercent(summary?.avg_edge_percent)} {isEn ? "Updated" : "更新时间"}{" "} {terminalData?.generated_at ? new Date(terminalData.generated_at).toLocaleTimeString(isEn ? "en-US" : "zh-CN", { hour: "2-digit", minute: "2-digit", }) : "--"}
{visibleSections.map((section) => (
{section.title}

{section.subtitle}

{section.rows.length}
{section.rows.map((row) => ( ))}
))}
); }