证据面板新增 AI 预测最高温 vs DEB 双锚对比卡片

在 AI 机场报文解读面板顶部加入双卡片布局:左侧展示 AI 预测最高温
(含置信区间和置信度),右侧展示 DEB 融合值,形成可对比的"双锚"
视图。同时更新加载态文案为"AI 正在基于最新报文预测今日最高温"。
This commit is contained in:
2569718930@qq.com
2026-05-06 15:48:27 +08:00
parent c296bbb789
commit f9d0ea31bc
5 changed files with 172 additions and 4 deletions
@@ -917,6 +917,89 @@
line-height: 1.6; 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) { .root :global(.scan-ai-weather-summary) {
color: #e6edf3; color: #e6edf3;
font-weight: 850; font-weight: 850;
@@ -5,15 +5,28 @@ import type {
AiCityForecastPayload, AiCityForecastPayload,
AiCityForecastState, AiCityForecastState,
} from "@/components/dashboard/scan-terminal/types"; } 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({ export function AiEvidencePanel({
aiBullets, aiBullets,
aiCityForecast, aiCityForecast,
aiForecast, aiForecast,
aiPredictedMax,
aiRangeLow,
aiRangeHigh,
aiConfidence,
aiReadCompleteText, aiReadCompleteText,
aiReadInProgressText, aiReadInProgressText,
aiRuleEvidenceMode, aiRuleEvidenceMode,
aiRuleEvidenceText, aiRuleEvidenceText,
debPrediction,
fallbackAiReason, fallbackAiReason,
isCompactCard, isCompactCard,
isEn, isEn,
@@ -21,14 +34,20 @@ export function AiEvidencePanel({
localModelSupportNote, localModelSupportNote,
localizedFinalJudgment, localizedFinalJudgment,
rawObservationText, rawObservationText,
tempSymbol,
}: { }: {
aiBullets: string[]; aiBullets: string[];
aiCityForecast: AiCityForecastPayload["city_forecast"] | null; aiCityForecast: AiCityForecastPayload["city_forecast"] | null;
aiForecast: AiCityForecastState; aiForecast: AiCityForecastState;
aiPredictedMax: number | null;
aiRangeLow: number | null;
aiRangeHigh: number | null;
aiConfidence: string | null;
aiReadCompleteText: string; aiReadCompleteText: string;
aiReadInProgressText: string; aiReadInProgressText: string;
aiRuleEvidenceMode: boolean; aiRuleEvidenceMode: boolean;
aiRuleEvidenceText: string; aiRuleEvidenceText: string;
debPrediction: number | null;
fallbackAiReason: string; fallbackAiReason: string;
isCompactCard: boolean; isCompactCard: boolean;
isEn: boolean; isEn: boolean;
@@ -36,6 +55,7 @@ export function AiEvidencePanel({
localModelSupportNote: string; localModelSupportNote: string;
localizedFinalJudgment: string; localizedFinalJudgment: string;
rawObservationText: string; rawObservationText: string;
tempSymbol: string;
}) { }) {
const [isOpen, setIsOpen] = useState(() => !isCompactCard); const [isOpen, setIsOpen] = useState(() => !isCompactCard);
@@ -43,6 +63,14 @@ export function AiEvidencePanel({
setIsOpen(!isCompactCard); setIsOpen(!isCompactCard);
}, [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 ( return (
<details <details
className="scan-ai-city-section scan-ai-city-ai-read" className="scan-ai-city-section scan-ai-city-ai-read"
@@ -60,6 +88,30 @@ export function AiEvidencePanel({
</summary> </summary>
{isOpen ? ( {isOpen ? (
<div className="scan-ai-city-section-body"> <div className="scan-ai-city-section-body">
{hasAiPrediction || hasDebPrediction ? (
<div className="scan-ai-prediction-dual">
{hasAiPrediction ? (
<div className="scan-ai-prediction-card ai">
<small>{isEn ? "AI predicted high" : "AI 预测最高温"}</small>
<strong>{formatTemperatureValue(aiPredictedMax!, tempSymbol, { digits: 1 })}</strong>
<span className={`scan-ai-confidence ${aiConfidenceMeta.tone}`}>
{aiConfidenceMeta.label}
</span>
{aiRangeText ? <em>{aiRangeText}</em> : null}
</div>
) : null}
{hasDebPrediction ? (
<div className="scan-ai-prediction-card deb">
<small>{isEn ? "DEB fusion" : "DEB 融合"}</small>
<strong>{formatTemperatureValue(debPrediction!, tempSymbol, { digits: 1 })}</strong>
<span className="scan-ai-confidence neutral">
{isEn ? "Reference" : "参考"}
</span>
</div>
) : null}
</div>
) : null}
{aiForecast.status === "loading" ? ( {aiForecast.status === "loading" ? (
<> <>
<p className="scan-ai-weather-summary">{aiReadInProgressText}</p> <p className="scan-ai-weather-summary">{aiReadInProgressText}</p>
@@ -358,6 +358,9 @@ export function AiPinnedCityCard({
? `Model support is unavailable, so this city must rely on DEB path and ${observationSourceEn}.` ? `Model support is unavailable, so this city must rely on DEB path and ${observationSourceEn}.`
: `暂无可用多模型支撑,需要主要参考 DEB 路径和${observationSourceZh}`; : `暂无可用多模型支撑,需要主要参考 DEB 路径和${observationSourceZh}`;
const aiPredictedMax = toFiniteDecisionNumber(aiCityForecast?.predicted_max); 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({ const decisionExpectedHighNumber = resolveExpectedHighCandidate({
aiPredictedMax, aiPredictedMax,
currentTemp: currentTempNumber, currentTemp: currentTempNumber,
@@ -549,13 +552,18 @@ export function AiPinnedCityCard({
<MobileDecisionCard <MobileDecisionCard
aiBullets={aiBullets} aiBullets={aiBullets}
aiCityForecast={aiCityForecast} aiCityForecast={aiCityForecast}
aiConfidence={aiConfidence}
aiForecast={aiForecast} aiForecast={aiForecast}
aiPredictedMax={aiPredictedMax}
aiRangeHigh={aiRangeHigh}
aiRangeLow={aiRangeLow}
aiReadCompleteText={aiReadCompleteText} aiReadCompleteText={aiReadCompleteText}
aiReadInProgressText={aiReadInProgressText} aiReadInProgressText={aiReadInProgressText}
aiRuleEvidenceMode={aiRuleEvidenceMode} aiRuleEvidenceMode={aiRuleEvidenceMode}
aiRuleEvidenceText={aiRuleEvidenceText} aiRuleEvidenceText={aiRuleEvidenceText}
currentTempText={currentTempText} currentTempText={currentTempText}
dataFreshnessRows={dataFreshnessRows} dataFreshnessRows={dataFreshnessRows}
debPrediction={debNumber}
decisionState={decisionState} decisionState={decisionState}
detail={detail} detail={detail}
displayName={displayName} displayName={displayName}
@@ -574,6 +582,7 @@ export function AiPinnedCityCard({
peakWindow={peakWindow} peakWindow={peakWindow}
rawObservationText={rawObservationText} rawObservationText={rawObservationText}
removing={removing} removing={removing}
tempSymbol={tempSymbol}
/> />
) : ( ) : (
<> <>
@@ -620,11 +629,16 @@ export function AiPinnedCityCard({
<AiEvidencePanel <AiEvidencePanel
aiBullets={aiBullets} aiBullets={aiBullets}
aiCityForecast={aiCityForecast} aiCityForecast={aiCityForecast}
aiConfidence={aiConfidence}
aiForecast={aiForecast} aiForecast={aiForecast}
aiPredictedMax={aiPredictedMax}
aiRangeHigh={aiRangeHigh}
aiRangeLow={aiRangeLow}
aiReadCompleteText={aiReadCompleteText} aiReadCompleteText={aiReadCompleteText}
aiReadInProgressText={aiReadInProgressText} aiReadInProgressText={aiReadInProgressText}
aiRuleEvidenceMode={aiRuleEvidenceMode} aiRuleEvidenceMode={aiRuleEvidenceMode}
aiRuleEvidenceText={aiRuleEvidenceText} aiRuleEvidenceText={aiRuleEvidenceText}
debPrediction={debNumber}
fallbackAiReason={fallbackAiReason} fallbackAiReason={fallbackAiReason}
isCompactCard={isCompactCard} isCompactCard={isCompactCard}
isEn={isEn} isEn={isEn}
@@ -632,6 +646,7 @@ export function AiPinnedCityCard({
localModelSupportNote={localModelSupportNote} localModelSupportNote={localModelSupportNote}
localizedFinalJudgment={localizedFinalJudgment} localizedFinalJudgment={localizedFinalJudgment}
rawObservationText={rawObservationText} rawObservationText={rawObservationText}
tempSymbol={tempSymbol}
/> />
</div> </div>
@@ -32,13 +32,18 @@ import type { CityDetail } from "@/lib/dashboard-types";
export function MobileDecisionCard({ export function MobileDecisionCard({
aiBullets, aiBullets,
aiCityForecast, aiCityForecast,
aiConfidence,
aiForecast, aiForecast,
aiPredictedMax,
aiRangeHigh,
aiRangeLow,
aiReadCompleteText, aiReadCompleteText,
aiReadInProgressText, aiReadInProgressText,
aiRuleEvidenceMode, aiRuleEvidenceMode,
aiRuleEvidenceText, aiRuleEvidenceText,
currentTempText, currentTempText,
dataFreshnessRows, dataFreshnessRows,
debPrediction,
decisionState, decisionState,
detail, detail,
displayName, displayName,
@@ -57,16 +62,22 @@ export function MobileDecisionCard({
peakWindow, peakWindow,
rawObservationText, rawObservationText,
removing, removing,
tempSymbol,
}: { }: {
aiBullets: string[]; aiBullets: string[];
aiCityForecast: AiCityForecastPayload["city_forecast"] | null; aiCityForecast: AiCityForecastPayload["city_forecast"] | null;
aiConfidence: string | null;
aiForecast: AiCityForecastState; aiForecast: AiCityForecastState;
aiPredictedMax: number | null;
aiRangeHigh: number | null;
aiRangeLow: number | null;
aiReadCompleteText: string; aiReadCompleteText: string;
aiReadInProgressText: string; aiReadInProgressText: string;
aiRuleEvidenceMode: boolean; aiRuleEvidenceMode: boolean;
aiRuleEvidenceText: string; aiRuleEvidenceText: string;
currentTempText: string; currentTempText: string;
dataFreshnessRows: DataFreshnessRow[]; dataFreshnessRows: DataFreshnessRow[];
debPrediction: number | null;
decisionState: CityDecisionState; decisionState: CityDecisionState;
detail: CityDetail | null; detail: CityDetail | null;
displayName: string; displayName: string;
@@ -85,6 +96,7 @@ export function MobileDecisionCard({
peakWindow: string; peakWindow: string;
rawObservationText: string; rawObservationText: string;
removing?: boolean; removing?: boolean;
tempSymbol: string;
}) { }) {
const copy = getMobileDecisionCopy(isEn); const copy = getMobileDecisionCopy(isEn);
const loadingCopy = getCityLoadingCopy({ isEn, isHkoObservation }); const loadingCopy = getCityLoadingCopy({ isEn, isHkoObservation });
@@ -170,11 +182,16 @@ export function MobileDecisionCard({
<AiEvidencePanel <AiEvidencePanel
aiBullets={aiBullets} aiBullets={aiBullets}
aiCityForecast={aiCityForecast} aiCityForecast={aiCityForecast}
aiConfidence={aiConfidence}
aiForecast={aiForecast} aiForecast={aiForecast}
aiPredictedMax={aiPredictedMax}
aiRangeHigh={aiRangeHigh}
aiRangeLow={aiRangeLow}
aiReadCompleteText={aiReadCompleteText} aiReadCompleteText={aiReadCompleteText}
aiReadInProgressText={aiReadInProgressText} aiReadInProgressText={aiReadInProgressText}
aiRuleEvidenceMode={aiRuleEvidenceMode} aiRuleEvidenceMode={aiRuleEvidenceMode}
aiRuleEvidenceText={aiRuleEvidenceText} aiRuleEvidenceText={aiRuleEvidenceText}
debPrediction={debPrediction}
fallbackAiReason={fallbackAiReason} fallbackAiReason={fallbackAiReason}
isCompactCard isCompactCard
isEn={isEn} isEn={isEn}
@@ -182,6 +199,7 @@ export function MobileDecisionCard({
localModelSupportNote={localModelSupportNote} localModelSupportNote={localModelSupportNote}
localizedFinalJudgment={localizedFinalJudgment} localizedFinalJudgment={localizedFinalJudgment}
rawObservationText={rawObservationText} rawObservationText={rawObservationText}
tempSymbol={tempSymbol}
/> />
<details <details
@@ -21,11 +21,11 @@ export function getAiReadCopy({
: "AI 机场报文解读已完成", : "AI 机场报文解读已完成",
inProgress: isEn inProgress: isEn
? isHkoObservation ? isHkoObservation
? "Fast read is ready; AI is adding HKO observation details..." ? "Fast read is ready; AI is predicting today's high from the HKO observation..."
: "Fast read is ready; AI is adding airport bulletin details..." : "Fast read is ready; AI is predicting today's high from the airport bulletin..."
: isHkoObservation : isHkoObservation
? "快速判断已完成,AI 正在补充香港天文台观测细节…" ? "快速判断已完成,AI 正在基于最新观测预测今日最高温…"
: "快速判断已完成,AI 正在补充机场报文细节…", : "快速判断已完成,AI 正在基于最新报文预测今日最高温…",
ruleEvidence: isEn ruleEvidence: isEn
? "AI read did not return completely; rule evidence is being used." ? "AI read did not return completely; rule evidence is being used."
: "AI 解读未完整返回,当前使用规则证据", : "AI 解读未完整返回,当前使用规则证据",