证据面板新增 AI 预测最高温 vs DEB 双锚对比卡片
在 AI 机场报文解读面板顶部加入双卡片布局:左侧展示 AI 预测最高温 (含置信区间和置信度),右侧展示 DEB 融合值,形成可对比的"双锚" 视图。同时更新加载态文案为"AI 正在基于最新报文预测今日最高温"。
This commit is contained in:
@@ -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 解读未完整返回,当前使用规则证据",
|
||||||
|
|||||||
Reference in New Issue
Block a user