"use client";
import {
Cloud,
CloudFog,
CloudLightning,
CloudRain,
CloudSnow,
CloudSun,
Search,
Sun,
Wind,
} from "lucide-react";
import type { ChartConfiguration } from "chart.js";
import clsx from "clsx";
import { CSSProperties, useEffect, useMemo, useState } from "react";
import { useChart } from "@/hooks/useChart";
import { useDashboardStore } from "@/hooks/useDashboardStore";
import { useI18n } from "@/hooks/useI18n";
import { ProFeaturePaywall } from "@/components/dashboard/ProFeaturePaywall";
import {
ModelForecast,
ProbabilityDistribution,
} from "@/components/dashboard/PanelSections";
import {
getFutureModalView,
getModelView,
getProbabilityView,
getTodayPaceView,
getTemperatureChartData,
getWeatherSummary,
} from "@/lib/dashboard-utils";
import { dashboardClient } from "@/lib/dashboard-client";
import type { IntradayMeteorologySignal, MarketScan } from "@/lib/dashboard-types";
function normalizeMarketValue(value?: number | null) {
if (value == null) return null;
const numeric = Number(value);
if (!Number.isFinite(numeric)) return null;
if (numeric > 1) return Math.max(0, Math.min(1, numeric / 100));
return Math.max(0, Math.min(1, numeric));
}
function formatMinuteAxisLabel(value: number) {
if (!Number.isFinite(value)) return "";
const total = Math.max(0, Math.round(value));
const hour = Math.floor(total / 60) % 24;
const minute = total % 60;
return `${String(hour).padStart(2, "0")}:${String(minute).padStart(2, "0")}`;
}
function WeatherIcon({ emoji, size = 32 }: { emoji: string; size?: number }) {
if (emoji === "☀️") return ;
if (emoji === "⛅" || emoji === "🌤️")
return ;
if (emoji === "☁️") return ;
if (emoji === "🌧️" || emoji === "🌦️")
return ;
if (emoji === "⛈️") return ;
if (emoji === "❄️" || emoji === "🌨️")
return ;
if (emoji === "🌫️") return ;
if (emoji === "💨") return ;
return ;
}
function formatMarketPercent(value?: number | null) {
const normalized = normalizeMarketValue(value);
if (normalized == null) return "--";
return `${(normalized * 100).toFixed(1)}%`;
}
function formatBucketLabel(
bucket?: {
label?: string | null;
bucket?: string | null;
range?: string | null;
value?: number | null;
temp?: number | null;
} | null,
) {
if (!bucket) return "--";
const direct =
String(bucket.label || "").trim() ||
String(bucket.bucket || "").trim() ||
String(bucket.range || "").trim();
if (direct) {
let str = direct.toUpperCase().replace(/\s+/g, "");
str = str.replace(/°?C($|\+|-)/g, "℃$1");
if (!str.includes("℃") && /[0-9]/.test(str)) {
str += "℃";
}
return str;
}
const temp = Number(bucket.value ?? bucket.temp);
if (Number.isFinite(temp)) {
return `${Math.round(temp)}℃`;
}
return "--";
}
function parseBucketBoundaries(
bucket?: {
label?: string | null;
bucket?: string | null;
range?: string | null;
value?: number | null;
temp?: number | null;
} | null,
) {
if (!bucket) return null;
const raw =
String(bucket.label || "").trim() ||
String(bucket.bucket || "").trim() ||
String(bucket.range || "").trim();
if (!raw) return null;
const numbers = Array.from(raw.matchAll(/-?\d+(?:\.\d+)?/g)).map((match) =>
Number(match[0]),
);
if (!numbers.length) return null;
if (raw.includes("+")) {
return {
lower: numbers[0] ?? null,
upper: null as number | null,
boundaryLabel: `${numbers[0]}°C`,
};
}
if (numbers.length >= 2) {
return {
lower: numbers[0],
upper: numbers[1],
boundaryLabel: null as string | null,
};
}
return {
lower: numbers[0],
upper: null as number | null,
boundaryLabel: `${numbers[0]}°C`,
};
}
function clamp(value: number, min: number, max: number) {
return Math.min(Math.max(value, min), max);
}
function parseClockMinutes(value?: string | null) {
const text = String(value || "").trim();
const match = text.match(/^(\d{1,2}):(\d{2})$/);
if (!match) return null;
const hours = Number(match[1]);
const minutes = Number(match[2]);
if (!Number.isFinite(hours) || !Number.isFinite(minutes)) return null;
return hours * 60 + minutes;
}
function parseLeadingNumber(value?: string | number | null) {
if (typeof value === "number" && Number.isFinite(value)) return value;
const text = String(value || "").trim();
const match = text.match(/[-+]?\d+(?:\.\d+)?/);
if (!match) return null;
const numeric = Number(match[0]);
return Number.isFinite(numeric) ? numeric : null;
}
function parsePercentFromText(value?: string | number | null) {
if (typeof value === "number" && Number.isFinite(value)) {
return clamp(value, 0, 100);
}
const text = String(value || "").trim();
const percentMatch = text.match(/([-+]?\d+(?:\.\d+)?)\s*%/);
if (percentMatch) {
const numeric = Number(percentMatch[1]);
return Number.isFinite(numeric) ? clamp(numeric, 0, 100) : null;
}
return parseLeadingNumber(text);
}
function formatConfidenceLabel(value?: string | null, locale = "zh-CN") {
const normalized = String(value || "").trim().toLowerCase();
if (normalized === "high") return locale === "en-US" ? "High" : "高";
if (normalized === "medium") return locale === "en-US" ? "Medium" : "中";
if (normalized === "low") return locale === "en-US" ? "Low" : "低";
return locale === "en-US" ? "Pending" : "待确认";
}
function formatSignalDirection(value?: string | null, locale = "zh-CN") {
const normalized = String(value || "").trim().toLowerCase();
if (normalized === "support") return locale === "en-US" ? "Support" : "支持升温";
if (normalized === "suppress") return locale === "en-US" ? "Suppress" : "压制峰值";
return locale === "en-US" ? "Neutral" : "中性";
}
function formatSignalStrength(value?: string | null, locale = "zh-CN") {
const normalized = String(value || "").trim().toLowerCase();
if (normalized === "strong") return locale === "en-US" ? "Strong" : "强";
if (normalized === "medium") return locale === "en-US" ? "Medium" : "中";
return locale === "en-US" ? "Weak" : "弱";
}
function signalTone(signal?: IntradayMeteorologySignal | null) {
const direction = String(signal?.direction || "").trim().toLowerCase();
if (direction === "support") return "cyan";
if (direction === "suppress") return "amber";
return "blue";
}
function localizedText(
locale: string,
primary?: string | null,
english?: string | null,
) {
const en = String(english || "").trim();
const value = String(primary || "").trim();
if (locale === "en-US" && en) return en;
return value || en;
}
function localizedList(
locale: string,
primary?: string[] | null,
english?: string[] | null,
) {
const en = Array.isArray(english)
? english.filter((item) => String(item || "").trim())
: [];
const value = Array.isArray(primary)
? primary.filter((item) => String(item || "").trim())
: [];
if (locale === "en-US" && en.length) return en;
return value.length ? value : en;
}
function getTrendMetricVisual(metric: {
label?: string;
value?: string;
tone?: string;
}) {
const label = String(metric.label || "").toLowerCase();
const value = String(metric.value || "");
const numeric = parseLeadingNumber(value);
if (label.includes("降水") || label.includes("precip")) {
const precipPercent = parsePercentFromText(value);
if (precipPercent == null) return null;
return {
mode: "fill" as const,
percent: precipPercent,
tone: "cold" as const,
};
}
if (numeric == null) return null;
if (label.includes("温度") || label.includes("temp")) {
return {
mode: "center" as const,
percent: clamp(50 + (numeric / 4) * 50, 0, 100),
tone: numeric >= 0 ? "warm" as const : "cold" as const,
};
}
if (label.includes("露点") || label.includes("dew")) {
return {
mode: "center" as const,
percent: clamp(50 + (numeric / 3) * 50, 0, 100),
tone: numeric >= 0 ? "warm" as const : "cold" as const,
};
}
if (label.includes("气压") || label.includes("pressure")) {
return {
mode: "center" as const,
percent: clamp(50 + (numeric / 4) * 50, 0, 100),
tone: numeric >= 0 ? "warm" as const : "cold" as const,
};
}
if (label.includes("云量") || label.includes("cloud")) {
return {
mode: "center" as const,
percent: clamp(50 + (numeric / 40) * 50, 0, 100),
tone: numeric >= 0 ? "cold" as const : "warm" as const,
};
}
return null;
}
function DailyTemperatureChart({
dateStr,
forceToday = false,
}: {
dateStr: string;
forceToday?: boolean;
}) {
const store = useDashboardStore();
const { locale, t } = useI18n();
const detail = store.selectedDetail;
const view = detail ? getFutureModalView(detail, dateStr, locale) : null;
const isToday = forceToday || (detail ? dateStr === detail.local_date : false);
const todayChartData = useMemo(
() => (detail && isToday ? getTemperatureChartData(detail, locale) : null),
[detail, isToday, locale],
);
const canvasRef = useChart(() => {
if (!detail || !view) {
return {
data: { datasets: [], labels: [] },
type: "line",
} satisfies ChartConfiguration<"line">;
}
if (isToday && todayChartData) {
const datasets: NonNullable<
ChartConfiguration<"line">["data"]
>["datasets"] = [];
if (todayChartData.datasets.hasMgmHourly) {
datasets.push({
backgroundColor: "rgba(234, 179, 8, 0.05)",
borderColor: "rgba(234, 179, 8, 0.8)",
borderWidth: 2,
data: todayChartData.datasets.mgmHourlySeries,
fill: false,
label: locale === "en-US" ? "MGM Forecast" : "MGM 预测",
parsing: false,
pointHoverRadius: 6,
pointRadius: 3,
spanGaps: true,
tension: 0.3,
});
} else {
datasets.push({
backgroundColor: "rgba(52, 211, 153, 0.05)",
borderColor: "rgba(52, 211, 153, 0.6)",
borderWidth: 1.5,
data: todayChartData.datasets.debPastSeries,
fill: true,
label: locale === "en-US" ? "DEB Forecast" : "DEB 预测",
parsing: false,
pointHoverRadius: 3,
pointRadius: 0,
tension: 0.3,
});
datasets.push({
borderColor: "rgba(52, 211, 153, 0.35)",
borderDash: [5, 3],
borderWidth: 1.5,
data: todayChartData.datasets.debFutureSeries,
fill: false,
label: locale === "en-US" ? "DEB Forecast" : "DEB 预测",
parsing: false,
pointRadius: 0,
tension: 0.3,
});
}
datasets.push({
backgroundColor: "#22d3ee",
borderColor: "#22d3ee",
borderWidth: 0,
data: todayChartData.datasets.metarSeries,
fill: false,
label:
todayChartData.observationLabel ||
(locale === "en-US" ? "Observation" : "观测实况"),
order: 0,
parsing: false,
pointHoverRadius: 7,
pointRadius: 5,
showLine: false,
});
if (todayChartData.datasets.airportMetarSeries?.length > 0) {
datasets.push({
backgroundColor: "#60a5fa",
borderColor: "#60a5fa",
borderWidth: 1,
data: todayChartData.datasets.airportMetarSeries,
fill: false,
label:
locale === "en-US" ? "Airport METAR" : "机场 METAR",
order: 0,
parsing: false,
pointHoverRadius: 6,
pointRadius: 4,
showLine: false,
});
}
if (todayChartData.datasets.mgmSeries?.length > 0) {
datasets.push({
backgroundColor: "#facc15",
borderColor: "#facc15",
borderWidth: 0,
data: todayChartData.datasets.mgmSeries,
fill: false,
label: locale === "en-US" ? "MGM Observation" : "MGM 实测",
order: -1,
parsing: false,
pointHoverRadius: 9,
pointRadius: 7,
showLine: false,
});
}
if (
!todayChartData.datasets.hasMgmHourly &&
Math.abs(todayChartData.datasets.offset) > 0.3
) {
datasets.push({
borderColor: "rgba(99, 102, 241, 0.2)",
borderDash: [2, 4],
borderWidth: 1,
data: todayChartData.datasets.tempsSeries,
fill: false,
label: locale === "en-US" ? "OM Raw" : "OM 原始",
parsing: false,
pointRadius: 0,
tension: 0.3,
});
}
if ((todayChartData.tafMarkers || []).length > 0) {
datasets.push({
backgroundColor: "#f59e0b",
borderColor: "#f59e0b",
borderWidth: 0,
data: todayChartData.datasets.tafCurrentMarkerSeries,
fill: false,
label: locale === "en-US" ? "Current TAF" : "当前 TAF",
order: -3,
parsing: false,
pointHoverRadius: 8,
pointRadius: 6,
showLine: false,
});
datasets.push({
backgroundColor: "rgba(250, 204, 21, 0.72)",
borderColor: "rgba(250, 204, 21, 0.72)",
borderWidth: 0,
data: todayChartData.datasets.tafPeakWindowMarkerSeries,
fill: false,
label: locale === "en-US" ? "Peak-window TAF" : "峰值窗口 TAF",
order: -2,
parsing: false,
pointHoverRadius: 7,
pointRadius: 4,
showLine: false,
});
datasets.push({
backgroundColor: "#f59e0b",
borderColor: "#f59e0b",
borderWidth: 0,
data: todayChartData.datasets.tafMarkerSeries,
fill: false,
label: locale === "en-US" ? "TAF Timing" : "TAF 时段",
order: -4,
parsing: false,
pointHoverRadius: 0,
pointRadius: 0,
showLine: false,
});
}
return {
data: {
datasets,
labels: [],
},
options: {
interaction: { intersect: false, mode: "nearest" },
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: "#94a3b8",
filter: (legendItem, chartData) => {
const text = String(legendItem.text || "");
if (!text) return false;
if (text === "TAF Timing" || text === "TAF 时段") return false;
if (!text.includes("DEB")) return true;
const firstDebIndex = (chartData.datasets || []).findIndex(
(dataset) => String(dataset.label || "").includes("DEB"),
);
return legendItem.datasetIndex === firstDebIndex;
},
font: { family: "Inter", size: 11 },
},
},
tooltip: {
backgroundColor: "rgba(15, 23, 42, 0.96)",
borderColor: "rgba(34, 211, 238, 0.2)",
borderWidth: 1,
callbacks: {
title: (items) => {
const rawX = items?.[0]?.parsed?.x;
return rawX != null ? formatMinuteAxisLabel(Number(rawX)) : "";
},
label: (ctx) => {
const label = String(ctx.dataset.label || "");
const raw = ctx.raw as
| { marker?: { summary?: string; markerType?: string; displayType?: string; isCurrent?: boolean; isPeakWindow?: boolean } }
| undefined;
if (
label === "TAF Timing" ||
label === "TAF 时段" ||
label === "Current TAF" ||
label === "当前 TAF" ||
label === "Peak-window TAF" ||
label === "峰值窗口 TAF"
) {
const marker = raw?.marker;
if (!marker) return label;
const markerType = String(marker.markerType || "");
const displayType = String(
marker.displayType || marker.markerType || "",
);
const summary = String(marker.summary || "");
const prefix =
marker.isCurrent && marker.isPeakWindow
? locale === "en-US"
? "Current / peak-window TAF"
: "当前 / 峰值窗口 TAF"
: marker.isCurrent
? locale === "en-US"
? "Current TAF"
: "当前 TAF"
: marker.isPeakWindow
? locale === "en-US"
? "Peak-window TAF"
: "峰值窗口 TAF"
: label;
return `${prefix}: ${
markerType ? summary.replace(markerType, displayType) : summary
}`;
}
const value = ctx.parsed.y;
if (value == null) return label;
return `${label}: ${value.toFixed(1)}${detail.temp_symbol || "°C"}`;
},
},
},
},
responsive: true,
scales: {
x: {
max: todayChartData.xMax,
min: todayChartData.xMin,
grid: { color: "rgba(255,255,255,0.04)" },
type: "linear",
ticks: {
callback: (value) => {
const num = Number(value);
if (!Number.isFinite(num)) return "";
const minutes = Math.round(num);
if (
minutes !== todayChartData.xMin &&
minutes !== todayChartData.xMax &&
minutes % 120 !== 0
) {
return "";
}
return formatMinuteAxisLabel(minutes);
},
color: "#64748b",
font: { family: "Inter", size: 10 },
maxRotation: 0,
},
},
y: {
grid: { color: "rgba(255,255,255,0.04)" },
max: todayChartData.max,
min: todayChartData.min,
ticks: {
callback: (value) => `${value}${detail.temp_symbol || "°C"}`,
color: "#64748b",
font: { family: "Inter", size: 10 },
},
},
},
},
type: "line",
} satisfies ChartConfiguration<"line">;
}
const labels = view.slice.map((point) => point.label);
const unit = detail.temp_symbol || "°C";
return {
data: {
datasets: [
{
backgroundColor: "rgba(34, 211, 238, 0.08)",
borderColor: "#22d3ee",
data: view.slice.map((point) => point.temp),
fill: false,
label:
locale === "en-US" ? "Open-Meteo Temperature" : "Open-Meteo 温度",
pointRadius: 2,
tension: 0.28,
},
{
backgroundColor: "transparent",
borderColor: "#a78bfa",
borderDash: [5, 4],
data: view.slice.map((point) => point.dewPoint),
fill: false,
label: locale === "en-US" ? "Dew Point" : "露点",
pointRadius: 0,
tension: 0.24,
},
],
labels,
},
options: {
interaction: { intersect: false, mode: "index" },
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
color: "#94a3b8",
font: { family: "Inter", size: 11 },
},
},
tooltip: {
backgroundColor: "rgba(15, 23, 42, 0.96)",
borderColor: "rgba(34, 211, 238, 0.2)",
borderWidth: 1,
callbacks: {
label: (ctx) =>
`${ctx.dataset.label}: ${ctx.parsed.y?.toFixed(1)}${unit}`,
},
},
},
responsive: true,
scales: {
x: {
grid: { color: "rgba(255,255,255,0.04)" },
ticks: {
color: "#64748b",
font: { family: "Inter", size: 10 },
maxRotation: 0,
},
},
y: {
grid: { color: "rgba(255,255,255,0.04)" },
ticks: {
callback: (value) => `${value}${unit}`,
color: "#64748b",
font: { family: "Inter", size: 10 },
},
},
},
},
type: "line",
} satisfies ChartConfiguration<"line">;
}, [detail, isToday, locale, todayChartData, view]);
return (
<>
{isToday && (
{todayChartData?.legendText || t("future.chartLegendEmpty")}
)}
>
);
}
export function FutureForecastModal() {
const store = useDashboardStore();
const { locale, t } = useI18n();
const detail = store.selectedDetail;
const dateStr = store.futureModalDate;
const isPro = store.proAccess.subscriptionActive;
const isProLoading = store.proAccess.loading;
const [showDeferredTodaySections, setShowDeferredTodaySections] = useState(false);
const [freshMarketScan, setFreshMarketScan] = useState(null);
if (!detail || !dateStr) return null;
useEffect(() => {
setShowDeferredTodaySections(false);
if (typeof window === "undefined") {
setShowDeferredTodaySections(true);
return;
}
let cancelled = false;
let timeoutId: ReturnType | null = null;
let idleId: number | null = null;
const reveal = () => {
if (!cancelled) {
setShowDeferredTodaySections(true);
}
};
if ("requestIdleCallback" in window) {
idleId = window.requestIdleCallback(reveal, { timeout: 600 });
} else {
timeoutId = setTimeout(reveal, 120);
}
return () => {
cancelled = true;
if (idleId != null && "cancelIdleCallback" in window) {
window.cancelIdleCallback(idleId);
}
if (timeoutId != null) {
clearTimeout(timeoutId);
}
};
}, [dateStr, detail]);
const isToday =
store.forecastModalMode === "today" ||
(store.forecastModalMode == null && dateStr === detail.local_date);
const detailDepth = detail.detail_depth || "full";
const isFullDetailReady = detailDepth === "full";
const isStructureSyncing = store.loadingState.futureDeep || !isFullDetailReady;
const isAnyLayerSyncing = isStructureSyncing;
const isTodayBlockingRefresh = isToday && isStructureSyncing;
const activeMarketScan = freshMarketScan || detail.market_scan || null;
useEffect(() => {
setFreshMarketScan(null);
if (!isToday || !isFullDetailReady || !isPro) return;
const cityName = String(detail.name || detail.display_name || "").trim();
if (!cityName || !dateStr) return;
let cancelled = false;
let intervalId: ReturnType | null = null;
const refreshMarketScan = () => {
dashboardClient
.getCityMarketScan(cityName, {
force: false,
marketSlug: detail.market_scan?.primary_market?.slug || null,
targetDate: dateStr,
})
.then((payload) => {
if (cancelled) return;
setFreshMarketScan(payload.market_scan || null);
})
.catch(() => {
if (!cancelled) {
setFreshMarketScan(null);
}
});
};
refreshMarketScan();
intervalId = setInterval(() => {
if (typeof document !== "undefined" && document.visibilityState === "hidden") {
return;
}
refreshMarketScan();
}, 3000);
return () => {
cancelled = true;
if (intervalId != null) {
clearInterval(intervalId);
}
};
}, [
dateStr,
detail.display_name,
detail.local_date,
detail.market_scan?.primary_market?.slug,
detail.name,
detail.updated_at,
isFullDetailReady,
isPro,
isToday,
]);
const view = getFutureModalView(detail, dateStr, locale);
const scorePosition = `${50 + view.front.score / 2}%`;
const barStyle = {
"--score-position": scorePosition,
} as CSSProperties & { "--score-position": string };
const weatherSummary = getWeatherSummary(detail, locale);
const paceView = useMemo(
() =>
isToday && showDeferredTodaySections
? getTodayPaceView(detail, locale)
: null,
[detail, isToday, locale, showDeferredTodaySections],
);
const probabilityView = useMemo(
() => getProbabilityView(detail, dateStr),
[dateStr, detail],
);
const modelView = useMemo(() => getModelView(detail, dateStr), [dateStr, detail]);
const probabilityEngineKey = String(probabilityView?.engine || "")
.trim()
.toLowerCase();
const probabilityCalibrationMode = String(
probabilityView?.calibrationMode || "",
)
.trim()
.toLowerCase();
const hasLgbmProbability = useMemo(
() =>
Object.keys(modelView?.models || {}).some((name) =>
String(name || "").toLowerCase().replace(/[\s_/-]/g, "").includes("lgbm"),
),
[modelView],
);
const hasEmosProbability =
probabilityEngineKey === "emos" || probabilityCalibrationMode.includes("emos");
const probabilityEngineLabel = hasLgbmProbability
? locale === "en-US"
? "LGBM"
: "LGBM"
: hasEmosProbability
? "EMOS"
: locale === "en-US"
? "Calibrated model"
: "校准模型";
const probabilityTitle = hasLgbmProbability
? locale === "en-US"
? "LGBM-Calibrated Probability"
: "LGBM 校准概率"
: hasEmosProbability
? locale === "en-US"
? "EMOS-Calibrated Probability"
: "EMOS 校准概率"
: locale === "en-US"
? "Calibrated Model Probability"
: "校准模型概率";
const topProbabilityBucket = useMemo(() => {
const buckets = Array.isArray(probabilityView?.probabilities)
? probabilityView.probabilities
: [];
return [...buckets]
.filter((bucket) => Number.isFinite(Number(bucket?.probability)))
.sort((a, b) => Number(b?.probability) - Number(a?.probability))[0];
}, [probabilityView]);
const modelSpreadView = useMemo(() => {
const values = Object.values(modelView?.models || {})
.map((value) => Number(value))
.filter((value) => Number.isFinite(value));
if (!values.length) return null;
const min = Math.min(...values);
const max = Math.max(...values);
const spread = max - min;
return {
count: values.length,
max,
min,
spread,
};
}, [modelView]);
const boundaryRiskView = useMemo(() => {
if (!showDeferredTodaySections) return null;
if (!isToday || !paceView) return null;
const selectedBucket = topProbabilityBucket || null;
const bounds = parseBucketBoundaries(selectedBucket);
if (!bounds) return null;
const projected =
paceView.paceAdjustedHigh ??
(detail.deb?.prediction != null ? Number(detail.deb.prediction) : null);
if (projected == null || !Number.isFinite(projected)) return null;
const distances = [bounds.lower, bounds.upper]
.filter((value): value is number => value != null && Number.isFinite(value))
.map((value) => ({
boundary: value,
gap: Math.abs(projected - value),
}))
.sort((a, b) => a.gap - b.gap);
if (!distances.length) return null;
const nearest = distances[0];
const tone =
nearest.gap <= 0.4 ? "amber" : nearest.gap <= 0.8 ? "blue" : "cyan";
const status =
nearest.gap <= 0.4
? locale === "en-US"
? "High boundary risk"
: "边界风险高"
: nearest.gap <= 0.8
? locale === "en-US"
? "Watch boundary"
: "边界需观察"
: locale === "en-US"
? "Boundary buffer"
: "边界缓冲";
const note =
locale === "en-US"
? `${projected.toFixed(1)}${detail.temp_symbol} is ${nearest.gap.toFixed(1)}${detail.temp_symbol} from the nearest boundary ${nearest.boundary.toFixed(1)}°C.`
: `${projected.toFixed(1)}${detail.temp_symbol} 距最近边界 ${nearest.boundary.toFixed(1)}°C 还有 ${nearest.gap.toFixed(1)}${detail.temp_symbol}。`;
return {
label: locale === "en-US" ? "Boundary risk" : "边界风险",
note,
status,
tone,
value: `${nearest.gap.toFixed(1)}${detail.temp_symbol}`,
};
}, [detail.deb?.prediction, detail.temp_symbol, isToday, locale, paceView, showDeferredTodaySections, topProbabilityBucket]);
const peakWindowStateView = useMemo(() => {
if (!showDeferredTodaySections) return null;
if (!isToday || !paceView) return null;
const firstHour = Number(detail.peak?.first_h);
const lastHour = Number(detail.peak?.last_h);
if (
!Number.isFinite(firstHour) ||
!Number.isFinite(lastHour) ||
firstHour < 0 ||
lastHour < firstHour
) {
return null;
}
const currentMinutes = parseClockMinutes(detail.local_time);
const startMinutes = firstHour * 60;
const endMinutes = (lastHour + 1) * 60;
let status = locale === "en-US" ? "Awaiting peak" : "未进入峰值";
let tone: "amber" | "blue" | "cyan" = "blue";
if (currentMinutes != null && currentMinutes >= endMinutes) {
status = locale === "en-US" ? "Past peak" : "已过峰值";
tone = "cyan";
} else if (currentMinutes != null && currentMinutes >= startMinutes) {
status = locale === "en-US" ? "Peak window live" : "峰值窗口进行中";
tone = "amber";
}
const note =
locale === "en-US"
? `Primary peak window ${paceView.peakWindowText}.`
: `核心峰值窗口 ${paceView.peakWindowText}。`;
return {
label: locale === "en-US" ? "Peak window" : "峰值窗口状态",
note,
status,
tone,
value: paceView.peakWindowText,
};
}, [detail.local_time, detail.peak?.first_h, detail.peak?.last_h, isToday, locale, paceView, showDeferredTodaySections]);
const networkLeadView = useMemo(() => {
if (!showDeferredTodaySections) return null;
if (!isToday) return null;
const delta = Number(detail.airport_vs_network_delta);
const leadSignal = detail.network_lead_signal;
if (!Number.isFinite(delta)) return null;
const leaderLabel =
String(leadSignal?.leader_station_label || "").trim() ||
String(leadSignal?.leader_station_code || "").trim();
const leaderSyncStatus = String(leadSignal?.leader_sync_status || "").trim().toLowerCase();
const leaderSyncDelta = Number(leadSignal?.leader_time_delta_vs_anchor_minutes);
const syncNote =
leaderSyncStatus === "near_realtime" || leaderSyncStatus === "lagged"
? Number.isFinite(leaderSyncDelta)
? locale === "en-US"
? ` Timing offset versus the airport anchor is about ${Math.round(leaderSyncDelta)} minutes.`
: ` 与机场锚点存在约 ${Math.round(leaderSyncDelta)} 分钟时间差。`
: locale === "en-US"
? " Nearby observations are not fully synchronized."
: " 周边观测并非完全同步。"
: leaderSyncStatus === "unknown"
? locale === "en-US"
? " Nearby station timing is not fully verified."
: " 周边站观测时间尚未完全校验。"
: "";
const absDelta = Math.abs(delta);
const status =
delta <= -0.4
? locale === "en-US"
? "Airport trailing"
: "机场落后"
: delta >= 0.4
? locale === "en-US"
? "Airport leading"
: "机场领先"
: locale === "en-US"
? "Tracking network"
: "与站网齐平";
const tone =
delta <= -0.4 ? "amber" : delta >= 0.4 ? "cyan" : "blue";
const note =
delta <= -0.4
? locale === "en-US"
? `Airport anchor is ${absDelta.toFixed(1)}${detail.temp_symbol} cooler than the nearby official network${leaderLabel ? `, led by ${leaderLabel}` : ""}.${syncNote}`
: `机场主站当前比周边官方站网低 ${absDelta.toFixed(1)}${detail.temp_symbol}${leaderLabel ? `,领先点位是 ${leaderLabel}` : ""}。${syncNote}`
: delta >= 0.4
? locale === "en-US"
? `Airport anchor is ${absDelta.toFixed(1)}${detail.temp_symbol} hotter than the nearby official network.${syncNote}`
: `机场主站当前比周边官方站网高 ${absDelta.toFixed(1)}${detail.temp_symbol}。${syncNote}`
: locale === "en-US"
? "Airport anchor and nearby official network are broadly aligned."
: "机场主站与周边官方站网当前大体齐平。";
return {
label: locale === "en-US" ? "Airport vs network" : "机场 vs 周边站",
note,
status,
tone,
value: `${delta > 0 ? "+" : ""}${delta.toFixed(1)}${detail.temp_symbol}`,
};
}, [detail.airport_vs_network_delta, detail.network_lead_signal, detail.temp_symbol, isToday, locale, showDeferredTodaySections]);
const isNoaaSettlement =
detail.current?.settlement_source === "noaa" ||
detail.current?.settlement_source_label === "NOAA";
const noaaStationCode = String(
detail.current?.station_code || detail.risk?.icao || "NOAA",
)
.trim()
.toUpperCase();
const noaaStationName =
String(detail.current?.station_name || "").trim() ||
String(detail.risk?.airport || "").trim() ||
noaaStationCode;
const hottestBucketLabel = formatBucketLabel(topProbabilityBucket);
const probabilitySummary = (() => {
if (!topProbabilityBucket) {
return locale === "en-US"
? "Probability mass is still too dispersed; avoid over-reading a single bracket."
: "当前概率还比较分散,不要只盯单一区间。";
}
const bucketLabel = formatBucketLabel(topProbabilityBucket);
const bucketProb = formatMarketPercent(topProbabilityBucket.probability);
if (!isToday) {
return locale === "en-US"
? `Target-day model probability reference puts the leading bucket at ${bucketLabel} (${bucketProb}). EMOS is reserved for intraday analysis after live anchor observations arrive.`
: `目标日模型概率参考显示领先温度桶为 ${bucketLabel}(${bucketProb})。EMOS 仅用于有实时锚点观测后的日内分析。`;
}
if (hasLgbmProbability) {
return locale === "en-US"
? `LGBM-calibrated read puts the leading bucket at ${bucketLabel} (${bucketProb}). Treat this as the base case, not the final settlement.`
: `LGBM 校准后领先温度桶为 ${bucketLabel}(${bucketProb})。可作为基准情形,但不要直接等同于最终结算。`;
}
if (hasEmosProbability) {
return locale === "en-US"
? `EMOS-calibrated probability puts the leading bucket at ${bucketLabel} (${bucketProb}). It is the primary calibrated probability layer, not the final settlement.`
: `EMOS 校准概率显示领先温度桶为 ${bucketLabel}(${bucketProb})。这是当前主概率层,但不要直接等同于最终结算。`;
}
return locale === "en-US"
? `Calibrated model probability puts the leading bucket at ${bucketLabel} (${bucketProb}). Treat this as the base case, not the final settlement.`
: `校准模型概率显示领先温度桶为 ${bucketLabel}(${bucketProb})。可作为基准情形,但不要直接等同于最终结算。`;
})();
const modelSummary = (() => {
if (!modelSpreadView) {
return locale === "en-US"
? "Model spread is unavailable right now."
: "当前拿不到可用的模型分歧。";
}
const modelEntries = Object.entries(modelView?.models || {}).filter(
([, value]) => value !== null && value !== undefined && Number.isFinite(Number(value)),
);
if (modelEntries.length === 1) {
const [singleModelName, singleModelValue] = modelEntries[0];
return locale === "en-US"
? `Only ${singleModelName} is available right now at ${Number(singleModelValue).toFixed(1)}${detail.temp_symbol}; multi-model spread is temporarily unavailable.`
: `当前只收到 ${singleModelName} ${Number(singleModelValue).toFixed(1)}${detail.temp_symbol},其他多模型暂未回传,所以这里先不判断模型分歧。`;
}
return locale === "en-US"
? `Model range runs from ${modelSpreadView.min.toFixed(1)}${detail.temp_symbol} to ${modelSpreadView.max.toFixed(1)}${detail.temp_symbol}; spread ${modelSpreadView.spread.toFixed(1)}${detail.temp_symbol}.`
: `当前模型区间在 ${modelSpreadView.min.toFixed(1)}${detail.temp_symbol} 到 ${modelSpreadView.max.toFixed(1)}${detail.temp_symbol},分歧 ${modelSpreadView.spread.toFixed(1)}${detail.temp_symbol}。`;
})();
const upperAirSignal = detail.vertical_profile_signal || {};
const tafSignal = detail.taf?.signal || {};
const upperAirCue = useMemo(() => {
if (!showDeferredTodaySections) return null;
if (!isToday || (!upperAirSignal.source && !tafSignal.available)) return null;
const setup = String(upperAirSignal.heating_setup || "neutral").toLowerCase();
const tafSuppression = String(
tafSignal.suppression_level || "low",
).toLowerCase();
const tafDisruption = String(
tafSignal.disruption_level || "low",
).toLowerCase();
const reasons: string[] = [];
let score = 0;
if (setup === "supportive") {
score += 2;
reasons.push(
locale === "en-US"
? "upper-air structure still supports daytime heating"
: "高空结构仍偏支持白天冲高",
);
} else if (setup === "suppressed") {
score -= 2;
reasons.push(
locale === "en-US"
? "upper-air structure still leans toward capping the peak"
: "高空结构更偏向压住峰值",
);
}
if (tafSuppression === "high") {
score -= 2;
reasons.push(
locale === "en-US"
? "TAF flags meaningful cloud/rain suppression near the peak window"
: "TAF 在峰值窗口提示云雨压温风险偏高",
);
} else if (tafSuppression === "medium") {
score -= 1;
reasons.push(
locale === "en-US"
? "TAF keeps some cloud/rain suppression risk on the table"
: "TAF 仍提示一定的云雨压温风险",
);
}
if (tafDisruption === "high") {
score -= 1;
reasons.push(
locale === "en-US"
? "TAF also suggests a noisier afternoon regime"
: "TAF 还提示午后扰动偏强",
);
} else if (tafDisruption === "medium") {
score -= 0.5;
reasons.push(
locale === "en-US"
? "TAF keeps some afternoon timing noise in play"
: "TAF 提示午后仍可能有时段性扰动",
);
}
if (score >= 1.5) {
return {
summary:
locale === "en-US"
? "The combined upper-air and TAF read still leans warmer. Do not fade lower buckets too early."
: "高空和 TAF 两层信号合并后仍偏暖侧,不宜过早做更低温区间。",
note:
locale === "en-US"
? `${reasons.slice(0, 2).join("; ")}.`
: `${reasons.slice(0, 2).join(";")}。`,
tone: "warm",
value: locale === "en-US" ? "Lean warmer" : "偏暖侧",
};
}
if (score <= -1.5) {
return {
summary:
locale === "en-US"
? "The combined upper-air and TAF read leans more defensive. Be more careful chasing higher buckets."
: "高空和 TAF 两层信号合并后更偏防守,追更高温区间要更谨慎。",
note:
locale === "en-US"
? `${reasons.slice(0, 2).join("; ")}.`
: `${reasons.slice(0, 2).join(";")}。`,
tone: "cold",
value: locale === "en-US" ? "Lean cautious" : "偏谨慎",
};
}
return {
summary:
locale === "en-US"
? "The combined upper-air and TAF read is mixed. Let surface structure decide before taking a side."
: "高空和 TAF 两层信号目前偏混合,先看近地面结构变化,不急着站边。",
note:
locale === "en-US"
? `${reasons.slice(0, 2).join("; ") || "No clean edge from the upper-air layer alone"}.`
: `${reasons.slice(0, 2).join(";") || "单看高空层还没有干净的交易边"}。`,
tone: "",
value: locale === "en-US" ? "Wait / confirm" : "先观察",
};
}, [
tafSignal.available,
tafSignal.disruption_level,
tafSignal.suppression_level,
isToday,
locale,
upperAirSignal.heating_setup,
upperAirSignal.source,
showDeferredTodaySections,
]);
const topObservedTemp =
detail.current?.max_so_far != null
? detail.current.max_so_far
: detail.current?.temp;
const currentTempText =
detail.current?.temp != null
? `${detail.current.temp}${detail.temp_symbol}`
: "--";
const daylightProgress = (() => {
const now = parseClockMinutes(detail.current?.obs_time);
const sunrise = parseClockMinutes(detail.forecast?.sunrise);
const sunset = parseClockMinutes(detail.forecast?.sunset);
if (now == null || sunrise == null || sunset == null || sunset <= sunrise) {
return null;
}
const percent = clamp(((now - sunrise) / (sunset - sunrise)) * 100, 0, 100);
const phase =
now < sunrise ? "夜间" : now > sunset ? "已日落" : "白昼进行中";
return {
phase,
percent,
};
})();
const displayedUpperAirSummary = showDeferredTodaySections
? upperAirCue?.summary || view.front.upperAirSummary
: "";
const displayedUpperAirMetrics = showDeferredTodaySections
? (view.front.upperAirMetrics || []).map((metric, index) =>
index === 0 &&
(metric.label === "Trade cue" || metric.label === "交易动作") &&
upperAirCue
? {
...metric,
note: upperAirCue.note,
tone: upperAirCue.tone,
value: upperAirCue.value,
}
: metric,
)
: [];
const localizedAiCommentaryLines = useMemo(() => {
if (!showDeferredTodaySections) return [] as string[];
const commentary = detail.dynamic_commentary || {};
const headline = String(
locale === "en-US" ? commentary.headline_en || "" : commentary.headline_zh || "",
).trim();
const bullets = (
locale === "en-US" ? commentary.bullets_en : commentary.bullets_zh
) as string[] | null | undefined;
const cleanedBullets = Array.isArray(bullets)
? bullets.map((item) => String(item || "").trim()).filter(Boolean)
: [];
return [headline, ...cleanedBullets].filter(Boolean).slice(0, 3);
}, [detail.dynamic_commentary, locale, showDeferredTodaySections]);
const todayTradeSummaryLines = useMemo(() => {
if (!showDeferredTodaySections) return [] as string[];
if (!isToday) return [] as string[];
if (localizedAiCommentaryLines.length > 0) {
return localizedAiCommentaryLines;
}
const lines: string[] = [];
if (paceView) {
const headline =
paceView.biasTone === "warm"
? locale === "en-US"
? `Pace is running hot by ${paceView.deltaText}; the day high still leans above the base curve.`
: `节奏偏热 ${paceView.deltaText},日高仍偏向落在基础曲线之上。`
: paceView.biasTone === "cold"
? locale === "en-US"
? `Pace is trailing by ${paceView.deltaText}; chasing higher buckets needs caution.`
: `节奏落后 ${paceView.deltaText},继续追更高温区间要更谨慎。`
: locale === "en-US"
? "Pace is still on curve; the next move depends on the peak-window push."
: "节奏目前贴着曲线走,下一步主要看峰值窗口还有没有上冲。";
lines.push(headline);
}
if (boundaryRiskView) {
lines.push(
locale === "en-US"
? `${boundaryRiskView.label}: ${boundaryRiskView.note}`
: `${boundaryRiskView.label}:${boundaryRiskView.note}`,
);
}
if (networkLeadView) {
lines.push(
locale === "en-US"
? `${networkLeadView.label}: ${networkLeadView.note}`
: `${networkLeadView.label}:${networkLeadView.note}`,
);
}
return lines.slice(0, 3);
}, [boundaryRiskView, isToday, locale, localizedAiCommentaryLines, networkLeadView, paceView, showDeferredTodaySections]);
const intradayMeteorology = detail.intraday_meteorology || {};
const meteorologySignals = Array.isArray(intradayMeteorology.signal_contributions)
? intradayMeteorology.signal_contributions
: [];
const invalidationRules = localizedList(
locale,
intradayMeteorology.invalidation_rules,
intradayMeteorology.invalidation_rules_en,
);
const confirmationRules = localizedList(
locale,
intradayMeteorology.confirmation_rules,
intradayMeteorology.confirmation_rules_en,
);
const meteorologyHeadline =
localizedText(
locale,
intradayMeteorology.headline,
intradayMeteorology.headline_en,
) ||
todayTradeSummaryLines[0] ||
(locale === "en-US"
? "Intraday meteorology layers are still syncing; use the next observation as the anchor."
: "关键日内气象层仍在同步,先以下一次观测作为判断锚点。");
const baseCaseBucket =
String(intradayMeteorology.base_case_bucket || "").trim() ||
formatBucketLabel(topProbabilityBucket);
const nextObservationTime =
String(intradayMeteorology.next_observation_time || "").trim() || "--";
const baseBucketNumber = parseLeadingNumber(baseCaseBucket);
const referenceObservedTemp =
topObservedTemp != null && Number.isFinite(Number(topObservedTemp))
? Number(topObservedTemp)
: detail.current?.temp != null
? Number(detail.current.temp)
: null;
const gapToBaseBucket =
baseBucketNumber != null && referenceObservedTemp != null
? Math.max(0, baseBucketNumber - referenceObservedTemp)
: null;
const pathStatus =
gapToBaseBucket == null
? locale === "en-US"
? "Awaiting anchor"
: "等待锚点"
: gapToBaseBucket <= 0.05
? locale === "en-US"
? "Base path touched"
: "基准路径已触达"
: gapToBaseBucket <= 1.0
? locale === "en-US"
? "Base path open"
: "基准路径开放"
: locale === "en-US"
? "Needs peak push"
: "需要峰值推动";
const peakWindowText =
String(intradayMeteorology.peak_window || "").trim() ||
paceView?.peakWindowText ||
"--";
const settlementSourceCode = String(
detail.current?.settlement_source || "",
).trim().toLowerCase();
const settlementStationCode = String(
detail.current?.station_code || detail.risk?.icao || "",
)
.trim()
.toUpperCase();
const settlementStationName =
String(detail.current?.station_name || detail.risk?.airport || "").trim() ||
settlementStationCode ||
(locale === "en-US" ? "Anchor station" : "锚点站");
const airportMetarAnchor =
settlementSourceCode === "metar" ||
settlementSourceCode === "wunderground" ||
Boolean(settlementStationCode && /^[A-Z]{4}$/.test(settlementStationCode));
const anchorSourceLabel = airportMetarAnchor
? settlementStationCode
? `${settlementStationCode} METAR`
: "METAR"
: detail.current?.settlement_source_label ||
detail.current?.settlement_source ||
(locale === "en-US" ? "Official observation" : "官方观测");
const anchorRuleText = airportMetarAnchor
? locale === "en-US"
? `Airport contract anchor: use the ${anchorSourceLabel} reports. Wunderground is only a history display page when present.`
: `机场合约锚点:以 ${anchorSourceLabel} 报文为准;若出现 Wunderground,它只是历史展示页面。`
: locale === "en-US"
? `Official anchor: use ${anchorSourceLabel} observations for this contract.`
: `官方锚点:该合约按 ${anchorSourceLabel} 观测口径判断。`;
const nextObservationLabel = airportMetarAnchor
? locale === "en-US"
? "Next METAR watch"
: "下一次 METAR 观察"
: locale === "en-US"
? "Next anchor watch"
: "下一次锚点观察";
const gapToBaseText =
gapToBaseBucket == null
? "--"
: `${gapToBaseBucket.toFixed(1)}${detail.temp_symbol || "°C"}`;
const syncStatusItems = [
{
key: "base",
state: isAnyLayerSyncing ? "syncing" : "ready",
label:
isAnyLayerSyncing
? locale === "en-US"
? "Refreshing base analysis"
: "正在刷新基础分析"
: locale === "en-US" ? "Base analysis ready" : "基础分析已加载",
note:
isAnyLayerSyncing
? locale === "en-US"
? "Latest anchor readings and forecast curve are being rebuilt."
: "正在重建最新锚点读数和预测曲线。"
: locale === "en-US"
? "Forecast curve, anchor state, and the core intraday view are available."
: "预测曲线、锚点状态和核心日内视图已经可用。",
},
{
key: "market",
state: isAnyLayerSyncing ? "syncing" : "ready",
label:
isAnyLayerSyncing
? locale === "en-US"
? "Refreshing probability layer"
: "正在刷新概率层"
: locale === "en-US"
? "Probability layer ready"
: "概率层已加载",
note:
isAnyLayerSyncing
? locale === "en-US"
? "Model spread and calibrated buckets are updating."
: "模型分歧和校准概率桶正在更新。"
: locale === "en-US"
? `Probability buckets are derived from the ${probabilityEngineLabel} layer.`
: `概率桶当前由 ${probabilityEngineLabel} 层推导。`,
},
] as const;
return (
{
if (event.target === event.currentTarget) {
store.closeFutureModal();
}
}}
>
{isProLoading ? (
) : !isPro ? (
) : (
{locale === "en-US" ? "Analysis workspace" : "分析工作台"}
•
{detail.display_name.toUpperCase()}
{isToday
? t("future.todayTitle", {
city: detail.display_name.toUpperCase(),
})
: t("future.dateTitle", {
city: detail.display_name.toUpperCase(),
date: dateStr,
})}
{
if (isToday) {
void store.openTodayModal(true);
return;
}
store.openFutureModal(dateStr, true);
}}
title={
!isPro
? locale === "en-US"
? "Pro subscription required"
: "需要 Pro 订阅"
: locale === "en-US"
? "Refresh Data"
: "刷新数据"
}
>
{isToday
? locale === "en-US"
? "Base signal first, then probability and model layers."
: "先看基础信号,再看概率层和模型层。"
: locale === "en-US"
? "Forward date view with phased model and structure sync."
: "未来日期视图,模型层与结构层分阶段补齐。"}
×
{isTodayBlockingRefresh && (
{locale === "en-US"
? "Refreshing latest intraday data"
: "正在刷新最新日内数据"}
{locale === "en-US"
? "Old cached readings are temporarily locked to prevent misjudgement. The analysis will unlock after the latest anchor observation, model layer, and probability layer are ready."
: "旧缓存读数已临时锁定,避免误判。最新锚点观测、模型层和概率层就绪后会自动解锁。"}
)}
{isToday && (
{locale === "en-US" ? "Professional meteorology read" : "专业气象判断"}
{anchorSourceLabel}
{meteorologyHeadline}
{anchorRuleText}
{locale === "en-US" ? "Confidence" : "置信度"} ·{" "}
{formatConfidenceLabel(intradayMeteorology.confidence, locale)}
{locale === "en-US" ? "Path state" : "路径状态"} · {pathStatus}
{nextObservationLabel} · {nextObservationTime}
{locale === "en-US" ? "Anchor" : "锚点"}
{settlementStationName}
{anchorSourceLabel}
{locale === "en-US" ? "Base" : "基准"}
{baseCaseBucket || "--"}
{locale === "en-US" ? "Upside" : "上修"}
{intradayMeteorology.upside_bucket || "--"}
{locale === "en-US" ? "Downside" : "下修"}
{intradayMeteorology.downside_bucket || "--"}
{locale === "en-US" ? "Gap to base" : "距基准还差"}
{gapToBaseText}
)}
{syncStatusItems.map((item) => (
))}
{isNoaaSettlement && (
{locale === "en-US"
? `${detail.display_name} now settles against NOAA ${noaaStationCode} (${noaaStationName}). The market uses the highest rounded whole-degree Celsius reading in the Temp column after the day is finalized.`
: `${detail.display_name} 当前按 NOAA ${noaaStationCode}(${noaaStationName})结算。市场最终采用该日 Temp 列完成质控后的最高整度摄氏值,不按小数温度结算。`}
)}
{isToday ? (
{locale === "en-US"
? "Anchor Status"
: "锚点状态"}
{locale === "en-US"
? "Settlement anchor and current clock"
: "结算锚点与当前时钟"}
{currentTempText}
{weatherSummary.weatherText}
@{detail.current?.obs_time || "--"}
{daylightProgress ? (
{locale === "en-US"
? "Solar Window"
: "昼夜进度"}
{locale === "en-US"
? `${daylightProgress.phase} ${Math.round(daylightProgress.percent)}%`
: `${daylightProgress.phase} ${Math.round(daylightProgress.percent)}%`}
{detail.forecast?.sunrise || "--"}
{detail.forecast?.sunset || "--"}
) : null}
{locale === "en-US" ? "High so far" : "日内已见高点"}
{topObservedTemp ?? "--"}
{detail.temp_symbol}
{locale === "en-US" ? "Anchor clock" : "锚点时钟"}
{detail.current?.obs_time || "--"}
{locale === "en-US" ? "Gap to base" : "距基准档"}
{gapToBaseBucket != null
? `${gapToBaseBucket.toFixed(1)}${detail.temp_symbol}`
: "--"}
{locale === "en-US" ? "Path state" : "路径状态"}
{pathStatus}
{locale === "en-US" ? "Sunrise" : "日出时间"}
{detail.forecast?.sunrise || "--"}
{locale === "en-US" ? "Sunset" : "日落时间"}
{detail.forecast?.sunset || "--"}
{showDeferredTodaySections && paceView ? (
{locale === "en-US" ? "Current Pace" : "当前节奏"}
{locale === "en-US"
? "Expected now vs airport anchor"
: "此刻应到 vs 机场锚点"}
{paceView.kicker}
{paceView.badge}
{paceView.deltaText}
{paceView.summary}
{locale === "en-US" ? "Expected now" : "预期此刻"}
{paceView.expectedNow.toFixed(1)}
{detail.temp_symbol}
{paceView.observedLabel}
{paceView.observedNow.toFixed(1)}
{detail.temp_symbol}
{paceView.paceAdjustedLabel}
{paceView.paceAdjustedHigh != null
? `${paceView.paceAdjustedHigh.toFixed(1)}${detail.temp_symbol}`
: "--"}
{locale === "en-US" ? "Peak window" : "峰值窗口"}
{paceView.peakWindowText}
{[boundaryRiskView, peakWindowStateView, networkLeadView]
.filter((item) => item != null)
.map((item) => (
{item.label}
{item.status}
{item.value}
{item.note}
))}
) : isToday ? (
{locale === "en-US" ? "Current Pace" : "当前节奏"}
{locale === "en-US"
? "Backfilling intraday pace context"
: "正在补齐日内节奏上下文"}
{locale === "en-US"
? "Expected-now pace, boundary risk, and airport-vs-network cues are loading in the background."
: "预期此刻节奏、边界风险和机场对比站网信号正在后台补齐。"}
) : null}
{locale === "en-US" ? "Primary view" : "主视图"}
{locale === "en-US"
? "Today's temperature path (anchor obs + models)"
: "今日气温路径(锚点观测 + 模型)"}
{locale === "en-US" ? "Base" : "基准"} · {baseCaseBucket || "--"}
{locale === "en-US" ? "Upside" : "上修"} · {intradayMeteorology.upside_bucket || "--"}
{locale === "en-US" ? "Invalidates at" : "失效观察"} · {nextObservationTime}
{locale === "en-US" ? "Evidence chain" : "气象证据链"}
{locale === "en-US" ? "Signal Contributions" : "信号贡献"}
{meteorologySignals.length > 0 ? (
meteorologySignals.map((signal, index) => (
{localizedText(locale, signal.label, signal.label_en) || "--"}
{formatSignalDirection(signal.direction, locale)} ·{" "}
{formatSignalStrength(signal.strength, locale)}
{localizedText(locale, signal.summary, signal.summary_en) || "--"}
))
) : (
{locale === "en-US"
? "Meteorology signals are still loading."
: "气象信号仍在加载。"}
)}
{locale === "en-US" ? "Failure modes" : "失效条件"}
{locale === "en-US" ? "What Downgrades the Read" : "什么会让判断降级"}
{(invalidationRules.length > 0
? invalidationRules
: [
locale === "en-US"
? "If observations stop tracking the expected curve, wait for the next refresh."
: "若实测不再贴近预期曲线,等待下一次刷新确认。",
]
).map((rule, index) => (
{rule}
))}
{locale === "en-US" ? "Confirmation" : "确认条件"}
{locale === "en-US" ? "What Confirms the Path" : "什么会确认主路径"}
{(confirmationRules.length > 0
? confirmationRules
: [
locale === "en-US"
? airportMetarAnchor
? "Keep watching the next anchor METAR report."
: "Keep watching the next official anchor observation."
: airportMetarAnchor
? "继续观察下一次锚点 METAR 报文。"
: "继续观察下一次官方锚点观测。",
]
).map((rule, index) => (
{rule}
))}
{modelSummary}
{locale === "en-US" ? "Probability read" : "概率判断"}
{probabilityTitle}
{probabilitySummary}
{locale === "en-US" ? "Model layer" : "模型层"}
{locale === "en-US" ? "Model Range & Spread" : "模型区间与分歧"}
{modelSummary}
) : (
<>
{t("future.targetForecast")}
{view.forecastEntry?.max_temp ?? "--"}
{detail.temp_symbol}
{t("future.deb")}
{view.deb ?? "--"}
{detail.temp_symbol}
{t("future.mu")}
{view.mu != null
? `${view.mu.toFixed(1)}${detail.temp_symbol}`
: "--"}
{t("future.score")}
{view.front.score > 0 ? "+" : ""}
{view.front.score}
{t("future.targetTempTrend")}
{t("future.probability")}
>
)}
)}
);
}