From 79b58a8b98d3129e8b2eb3842ccff868f2537d93 Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Fri, 24 Apr 2026 00:34:09 +0800 Subject: [PATCH] Add locale controls and distribution previews to scan terminal --- .../components/dashboard/Dashboard.module.css | 80 +++++++++---- .../components/dashboard/OpportunityTable.tsx | 112 ++++++++++++------ .../dashboard/ScanTerminalDashboard.tsx | 75 ++++++++++-- frontend/lib/dashboard-types.ts | 11 ++ src/data_collection/polymarket_readonly.py | 41 ++++++- web/scan_terminal_service.py | 1 + 6 files changed, 250 insertions(+), 70 deletions(-) diff --git a/frontend/components/dashboard/Dashboard.module.css b/frontend/components/dashboard/Dashboard.module.css index d058a901..35dcfeb3 100644 --- a/frontend/components/dashboard/Dashboard.module.css +++ b/frontend/components/dashboard/Dashboard.module.css @@ -9288,6 +9288,37 @@ font-size: 14px; } +.root :global(.scan-locale-switch) { + height: 36px; + padding: 3px; + border-radius: 12px; + border: 1px solid rgba(99, 132, 180, 0.18); + background: rgba(8, 19, 34, 0.9); + color: #a8bdd8; + display: inline-flex; + align-items: center; + gap: 2px; + font-size: 12px; + font-weight: 800; + cursor: pointer; +} + +.root :global(.scan-locale-switch span) { + min-width: 34px; + height: 28px; + padding: 0 8px; + border-radius: 8px; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.root :global(.scan-locale-switch span.active) { + color: #eef7ff; + background: rgba(63, 140, 255, 0.16); + box-shadow: inset 0 0 0 1px rgba(63, 140, 255, 0.22); +} + .root :global(.scan-topbar-time) { color: #b4c8e2; font-variant-numeric: tabular-nums; @@ -9321,6 +9352,24 @@ background: rgba(23, 217, 139, 0.12); } +.root :global(.scan-account-button) { + width: 42px; + height: 42px; + display: inline-flex; + align-items: center; + justify-content: center; + color: #d7e5f7; + text-decoration: none; + border: 1px solid rgba(99, 132, 180, 0.18); + background: rgba(8, 19, 34, 0.9); + border-radius: 12px; +} + +.root :global(.scan-account-button:hover) { + border-color: rgba(63, 140, 255, 0.28); + background: rgba(12, 26, 44, 0.96); +} + .root :global(.scan-ghost-button .spin) { animation: spin 1s linear infinite; } @@ -9532,27 +9581,7 @@ } .root :global(.scan-city-cell) { - display: flex; - align-items: center; - gap: 14px; -} - -.root :global(.scan-city-thumb) { - width: 78px; - height: 78px; - border-radius: 14px; - background: linear-gradient(135deg, #3269a9, #0e2946 55%, #09121e); - position: relative; - overflow: hidden; - box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08); -} - -.root :global(.scan-city-thumb-fill) { - position: absolute; - inset: 0; - background: - linear-gradient(180deg, rgba(255, 255, 255, 0.18), transparent 35%, rgba(0, 0, 0, 0.22)), - linear-gradient(135deg, #5f8fd6, #112842 60%, #09111d); + display: block; } .root :global(.scan-city-copy) { @@ -9626,7 +9655,7 @@ .root :global(.scan-distribution-preview) { display: grid; - grid-template-columns: repeat(4, 1fr); + grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 8px; } @@ -9752,7 +9781,7 @@ .root :global(.scan-detail-top) { display: flex; - gap: 14px; + gap: 0; align-items: center; } @@ -9931,6 +9960,11 @@ position: relative; } +.root :global(.scan-chart-group.highlighted .scan-chart-label) { + color: #22eb98; + font-weight: 800; +} + .root :global(.scan-chart-col) { width: 20px; border-radius: 8px 8px 0 0; diff --git a/frontend/components/dashboard/OpportunityTable.tsx b/frontend/components/dashboard/OpportunityTable.tsx index 1479ad9c..5acbd6f8 100644 --- a/frontend/components/dashboard/OpportunityTable.tsx +++ b/frontend/components/dashboard/OpportunityTable.tsx @@ -3,9 +3,17 @@ import React from "react"; import { Star } from "lucide-react"; import { useI18n } from "@/hooks/useI18n"; -import type { ScanOpportunityRow } from "@/lib/dashboard-types"; +import type { + DistributionPreviewPoint, + ScanOpportunityRow, +} from "@/lib/dashboard-types"; import { getLocalizedCityName } from "@/lib/dashboard-home-copy"; +type PhaseMeta = { + label: string; + tone: "green" | "amber" | "blue" | "red"; +}; + function formatPercent(value?: number | null, signed = false) { if (value == null || Number.isNaN(Number(value))) return "--"; const numeric = Number(value); @@ -44,12 +52,18 @@ function formatAction(row: ScanOpportunityRow, locale: string) { return "--"; } -function getPhaseMeta( - row: ScanOpportunityRow, +export function getWindowPhaseMeta( + row: Pick, locale: string, -): { label: string; tone: "green" | "amber" | "blue" | "red" } { +): PhaseMeta { const mode = String(row.window_phase || "").toLowerCase(); - if (mode === "active_peak" || mode === "setup_today") { + if (mode === "active_peak") { + return { + label: locale === "en-US" ? "Peak Window" : "峰值窗口", + tone: "red", + }; + } + if (mode === "setup_today" || mode === "early_today") { return { label: locale === "en-US" ? "Touch Play" : "触达博弈", tone: "red", @@ -61,6 +75,12 @@ function getPhaseMeta( tone: "blue", }; } + if (mode === "post_peak") { + return { + label: locale === "en-US" ? "Post Peak" : "峰后确认", + tone: "amber", + }; + } if (row.trend_alignment) { return { label: locale === "en-US" ? "Trend" : "趋势确认", @@ -87,35 +107,58 @@ function ProbabilityPreview({ row: ScanOpportunityRow; locale: string; }) { - const targetBase = - row.target_value ?? - row.target_threshold ?? - row.target_lower ?? - row.target_upper ?? - null; - const unit = row.target_unit || row.temp_symbol || ""; - const targetLabel = - targetBase != null - ? `${Math.round(Number(targetBase))}${unit}` - : row.target_label || "--"; + const preview = Array.isArray(row.distribution_preview) + ? row.distribution_preview.filter( + (item): item is DistributionPreviewPoint => + Boolean(item && (item.label || item.value != null)), + ) + : []; + + if (!preview.length) { + const targetBase = + row.target_value ?? + row.target_threshold ?? + row.target_lower ?? + row.target_upper ?? + null; + const unit = row.target_unit || row.temp_symbol || ""; + const targetLabel = + targetBase != null + ? `${Math.round(Number(targetBase))}${unit}` + : row.target_label || "--"; + preview.push({ + label: targetLabel, + model_probability: row.model_event_probability, + market_probability: row.market_event_probability, + highlighted: true, + }); + } + return (
-
- {targetLabel} - {locale === "en-US" ? "Target" : "目标"} -
-
- {formatPercent(row.model_event_probability != null ? row.model_event_probability * 100 : null)} - {locale === "en-US" ? "Model" : "模型"} -
-
- {formatPercent(row.market_event_probability != null ? row.market_event_probability * 100 : null)} - {locale === "en-US" ? "Market" : "市场"} -
-
- {formatPercent(row.distribution_bias_score)} - {row.distribution_bias_direction || (locale === "en-US" ? "Bias" : "偏移")} -
+ {preview.slice(0, 6).map((item) => ( +
+ {item.label || "--"} + + {locale === "en-US" ? "Model" : "模型"} +
+ {formatPercent( + item.model_probability != null ? item.model_probability * 100 : null, + )} +
+
+ + {locale === "en-US" ? "Market" : "市场"} +
+ {formatPercent( + item.market_probability != null ? item.market_probability * 100 : null, + )} +
+
+ ))}
); } @@ -172,7 +215,7 @@ export function OpportunityTable({
{rows.map((row, index) => { - const phaseMeta = getPhaseMeta(row, locale); + const phaseMeta = getWindowPhaseMeta(row, locale); const localizedCityName = getLocalizedCityName( row.city, row.city_display_name || row.display_name || row.city, @@ -194,9 +237,6 @@ export function OpportunityTable({
-
-
-
{localizedCityName}
diff --git a/frontend/components/dashboard/ScanTerminalDashboard.tsx b/frontend/components/dashboard/ScanTerminalDashboard.tsx index 37d0dd85..8b0da846 100644 --- a/frontend/components/dashboard/ScanTerminalDashboard.tsx +++ b/frontend/components/dashboard/ScanTerminalDashboard.tsx @@ -1,10 +1,12 @@ "use client"; import clsx from "clsx"; +import Link from "next/link"; import { Bell, Menu, RefreshCw, + UserRound, X, } from "lucide-react"; import { @@ -21,12 +23,17 @@ import { FilterState, ScanFilterPanel, } from "@/components/dashboard/ScanFilterPanel"; +import { getWindowPhaseMeta } from "@/components/dashboard/OpportunityTable"; import { ScanKPIBar } from "@/components/dashboard/ScanKPIBar"; import { OpportunityTable } from "@/components/dashboard/OpportunityTable"; -import { DashboardStoreProvider } from "@/hooks/useDashboardStore"; +import { + DashboardStoreProvider, + useDashboardStore, +} from "@/hooks/useDashboardStore"; import { I18nProvider, useI18n } from "@/hooks/useI18n"; import { dashboardClient } from "@/lib/dashboard-client"; import type { + DistributionPreviewPoint, MarketScan, PrimarySignal, ScanOpportunityRow, @@ -49,7 +56,14 @@ const DEFAULT_FILTERS: FilterState = { limit: 28, }; -const NAV_ITEMS = ["扫描台", "市场", "分析", "组合", "监控", "设置"]; +const NAV_ITEMS = [ + { zh: "扫描台", en: "Terminal" }, + { zh: "市场", en: "Markets" }, + { zh: "分析", en: "Analysis" }, + { zh: "组合", en: "Portfolio" }, + { zh: "监控", en: "Monitor" }, + { zh: "设置", en: "Settings" }, +]; function formatPercent(value?: number | null, signed = false) { if (value == null || Number.isNaN(Number(value))) return "--"; @@ -128,6 +142,20 @@ 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) @@ -140,6 +168,7 @@ function buildComparisonBuckets( 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 !== "--"); } @@ -150,6 +179,7 @@ function buildComparisonBuckets( label: row.target_label || "--", model: Number(row.model_event_probability || 0) * 100, market: Number(row.market_event_probability || 0) * 100, + highlighted: true, }, ]; } @@ -200,19 +230,19 @@ function DetailPanel({ ...comparisonBuckets.flatMap((bucket) => [bucket.model, bucket.market]), ); const scoreClass = scoreTone(displayRow.final_score); + const phaseMeta = getWindowPhaseMeta(displayRow, locale); return (