From f9d0ea31bcaa79b7a4143900cfe9c9cf352e3b6d Mon Sep 17 00:00:00 2001 From: "2569718930@qq.com" <2569718930@qq.com> Date: Wed, 6 May 2026 15:48:27 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AF=81=E6=8D=AE=E9=9D=A2=E6=9D=BF=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=20AI=20=E9=A2=84=E6=B5=8B=E6=9C=80=E9=AB=98=E6=B8=A9?= =?UTF-8?q?=20vs=20DEB=20=E5=8F=8C=E9=94=9A=E5=AF=B9=E6=AF=94=E5=8D=A1?= =?UTF-8?q?=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 AI 机场报文解读面板顶部加入双卡片布局:左侧展示 AI 预测最高温 (含置信区间和置信度),右侧展示 DEB 融合值,形成可对比的"双锚" 视图。同时更新加载态文案为"AI 正在基于最新报文预测今日最高温"。 --- .../dashboard/ScanTerminalCard.module.css | 83 +++++++++++++++++++ .../scan-terminal/AiEvidencePanel.tsx | 52 ++++++++++++ .../scan-terminal/AiPinnedCityCard.tsx | 15 ++++ .../scan-terminal/MobileDecisionCard.tsx | 18 ++++ .../dashboard/scan-terminal/decision-copy.ts | 8 +- 5 files changed, 172 insertions(+), 4 deletions(-) diff --git a/frontend/components/dashboard/ScanTerminalCard.module.css b/frontend/components/dashboard/ScanTerminalCard.module.css index b6b905c5..51c40274 100644 --- a/frontend/components/dashboard/ScanTerminalCard.module.css +++ b/frontend/components/dashboard/ScanTerminalCard.module.css @@ -917,6 +917,89 @@ line-height: 1.6; } +.root :global(.scan-ai-prediction-dual) { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 10px; + margin-bottom: 14px; +} + +.root :global(.scan-ai-prediction-card) { + display: grid; + gap: 4px; + padding: 12px 14px; + border: 1px solid rgba(77, 163, 255, 0.16); + border-radius: 14px; + background: rgba(13, 27, 48, 0.62); +} + +.root :global(.scan-ai-prediction-card.ai) { + border-color: rgba(34, 211, 238, 0.32); + background: linear-gradient(180deg, rgba(8, 36, 52, 0.72), rgba(10, 24, 42, 0.72)); +} + +.root :global(.scan-ai-prediction-card.deb) { + border-color: rgba(77, 163, 255, 0.18); +} + +.root :global(.scan-ai-prediction-card small) { + color: #7d92b2; + font-size: 10px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.root :global(.scan-ai-prediction-card strong) { + color: #eef7ff; + font-size: 28px; + font-weight: 900; + line-height: 1.1; +} + +.root :global(.scan-ai-prediction-card.ai strong) { + color: #4ef2d6; +} + +.root :global(.scan-ai-prediction-card em) { + display: block; + color: #8fa4c3; + font-size: 11px; + font-weight: 800; + font-style: normal; +} + +.root :global(.scan-ai-confidence) { + display: inline-block; + width: fit-content; + padding: 2px 8px; + border-radius: 999px; + font-size: 10px; + font-weight: 800; + background: rgba(77, 163, 255, 0.12); + color: #9ecbff; +} + +.root :global(.scan-ai-confidence.high) { + background: rgba(34, 197, 94, 0.14); + color: #86efac; +} + +.root :global(.scan-ai-confidence.medium) { + background: rgba(245, 158, 11, 0.14); + color: #facc15; +} + +.root :global(.scan-ai-confidence.low) { + background: rgba(148, 163, 184, 0.12); + color: #94a3b8; +} + +.root :global(.scan-ai-confidence.neutral) { + background: rgba(77, 163, 255, 0.08); + color: #7d92b2; +} + .root :global(.scan-ai-weather-summary) { color: #e6edf3; font-weight: 850; diff --git a/frontend/components/dashboard/scan-terminal/AiEvidencePanel.tsx b/frontend/components/dashboard/scan-terminal/AiEvidencePanel.tsx index 76aab13b..f06331d0 100644 --- a/frontend/components/dashboard/scan-terminal/AiEvidencePanel.tsx +++ b/frontend/components/dashboard/scan-terminal/AiEvidencePanel.tsx @@ -5,15 +5,28 @@ import type { AiCityForecastPayload, AiCityForecastState, } from "@/components/dashboard/scan-terminal/types"; +import { formatTemperatureValue } from "@/lib/temperature-utils"; + +function confidenceBadge(confidence: string | null, isEn: boolean) { + const c = String(confidence || "").toLowerCase(); + if (c === "high") return { label: isEn ? "High" : "高", tone: "high" }; + if (c === "medium") return { label: isEn ? "Medium" : "中", tone: "medium" }; + return { label: isEn ? "Low" : "低", tone: "low" }; +} export function AiEvidencePanel({ aiBullets, aiCityForecast, aiForecast, + aiPredictedMax, + aiRangeLow, + aiRangeHigh, + aiConfidence, aiReadCompleteText, aiReadInProgressText, aiRuleEvidenceMode, aiRuleEvidenceText, + debPrediction, fallbackAiReason, isCompactCard, isEn, @@ -21,14 +34,20 @@ export function AiEvidencePanel({ localModelSupportNote, localizedFinalJudgment, rawObservationText, + tempSymbol, }: { aiBullets: string[]; aiCityForecast: AiCityForecastPayload["city_forecast"] | null; aiForecast: AiCityForecastState; + aiPredictedMax: number | null; + aiRangeLow: number | null; + aiRangeHigh: number | null; + aiConfidence: string | null; aiReadCompleteText: string; aiReadInProgressText: string; aiRuleEvidenceMode: boolean; aiRuleEvidenceText: string; + debPrediction: number | null; fallbackAiReason: string; isCompactCard: boolean; isEn: boolean; @@ -36,6 +55,7 @@ export function AiEvidencePanel({ localModelSupportNote: string; localizedFinalJudgment: string; rawObservationText: string; + tempSymbol: string; }) { const [isOpen, setIsOpen] = useState(() => !isCompactCard); @@ -43,6 +63,14 @@ export function AiEvidencePanel({ setIsOpen(!isCompactCard); }, [isCompactCard]); + const aiConfidenceMeta = confidenceBadge(aiConfidence, isEn); + const hasAiPrediction = aiPredictedMax != null; + const hasDebPrediction = debPrediction != null; + const aiRangeText = + aiRangeLow != null && aiRangeHigh != null + ? `${formatTemperatureValue(aiRangeLow, tempSymbol, { digits: 0 })} ~ ${formatTemperatureValue(aiRangeHigh, tempSymbol, { digits: 0 })}` + : null; + return (
{isOpen ? (
+ {hasAiPrediction || hasDebPrediction ? ( +
+ {hasAiPrediction ? ( +
+ {isEn ? "AI predicted high" : "AI 预测最高温"} + {formatTemperatureValue(aiPredictedMax!, tempSymbol, { digits: 1 })} + + {aiConfidenceMeta.label} + + {aiRangeText ? {aiRangeText} : null} +
+ ) : null} + {hasDebPrediction ? ( +
+ {isEn ? "DEB fusion" : "DEB 融合"} + {formatTemperatureValue(debPrediction!, tempSymbol, { digits: 1 })} + + {isEn ? "Reference" : "参考"} + +
+ ) : null} +
+ ) : null} + {aiForecast.status === "loading" ? ( <>

{aiReadInProgressText}

diff --git a/frontend/components/dashboard/scan-terminal/AiPinnedCityCard.tsx b/frontend/components/dashboard/scan-terminal/AiPinnedCityCard.tsx index 43d117d9..7b96a3ce 100644 --- a/frontend/components/dashboard/scan-terminal/AiPinnedCityCard.tsx +++ b/frontend/components/dashboard/scan-terminal/AiPinnedCityCard.tsx @@ -358,6 +358,9 @@ export function AiPinnedCityCard({ ? `Model support is unavailable, so this city must rely on DEB path and ${observationSourceEn}.` : `暂无可用多模型支撑,需要主要参考 DEB 路径和${observationSourceZh}。`; const aiPredictedMax = toFiniteDecisionNumber(aiCityForecast?.predicted_max); + const aiRangeLow = toFiniteDecisionNumber(aiCityForecast?.range_low); + const aiRangeHigh = toFiniteDecisionNumber(aiCityForecast?.range_high); + const aiConfidence = String(aiCityForecast?.confidence || "").trim() || null; const decisionExpectedHighNumber = resolveExpectedHighCandidate({ aiPredictedMax, currentTemp: currentTempNumber, @@ -549,13 +552,18 @@ export function AiPinnedCityCard({ ) : ( <> @@ -620,11 +629,16 @@ export function AiPinnedCityCard({
diff --git a/frontend/components/dashboard/scan-terminal/MobileDecisionCard.tsx b/frontend/components/dashboard/scan-terminal/MobileDecisionCard.tsx index 617da6f2..80b087c0 100644 --- a/frontend/components/dashboard/scan-terminal/MobileDecisionCard.tsx +++ b/frontend/components/dashboard/scan-terminal/MobileDecisionCard.tsx @@ -32,13 +32,18 @@ import type { CityDetail } from "@/lib/dashboard-types"; export function MobileDecisionCard({ aiBullets, aiCityForecast, + aiConfidence, aiForecast, + aiPredictedMax, + aiRangeHigh, + aiRangeLow, aiReadCompleteText, aiReadInProgressText, aiRuleEvidenceMode, aiRuleEvidenceText, currentTempText, dataFreshnessRows, + debPrediction, decisionState, detail, displayName, @@ -57,16 +62,22 @@ export function MobileDecisionCard({ peakWindow, rawObservationText, removing, + tempSymbol, }: { aiBullets: string[]; aiCityForecast: AiCityForecastPayload["city_forecast"] | null; + aiConfidence: string | null; aiForecast: AiCityForecastState; + aiPredictedMax: number | null; + aiRangeHigh: number | null; + aiRangeLow: number | null; aiReadCompleteText: string; aiReadInProgressText: string; aiRuleEvidenceMode: boolean; aiRuleEvidenceText: string; currentTempText: string; dataFreshnessRows: DataFreshnessRow[]; + debPrediction: number | null; decisionState: CityDecisionState; detail: CityDetail | null; displayName: string; @@ -85,6 +96,7 @@ export function MobileDecisionCard({ peakWindow: string; rawObservationText: string; removing?: boolean; + tempSymbol: string; }) { const copy = getMobileDecisionCopy(isEn); const loadingCopy = getCityLoadingCopy({ isEn, isHkoObservation }); @@ -170,11 +182,16 @@ export function MobileDecisionCard({