Expose DEB quality guidance

This commit is contained in:
2569718930@qq.com
2026-06-07 23:57:47 +08:00
parent 4389d4a40d
commit bbe3871b7c
9 changed files with 264 additions and 3 deletions
@@ -1213,6 +1213,7 @@ export function LiveTemperatureThresholdChart({
observedHighRunway={observedHighRunway}
wundergroundDailyHigh={wundergroundDailyHigh}
debVal={debVal}
debQuality={chartHourly?.debQuality || null}
modelMin={modelMin}
modelMax={modelMax}
spread={spread}
@@ -38,6 +38,50 @@ function highLabel(label: string, isEn: boolean) {
return isEn ? (HIGH_LABEL_EN[label] || label) : label;
}
type DebQuality = {
quality_tier?: string | null;
recommendation?: string | null;
recent_hit_rate?: number | null;
recent_samples?: number | null;
};
function debQualityLabel(quality: DebQuality | null | undefined, isEn: boolean) {
const recommendation = quality?.recommendation;
if (recommendation === "primary") return isEn ? "Primary" : "主用";
if (recommendation === "supporting") return isEn ? "Support" : "辅助";
if (recommendation === "context_only") return isEn ? "Context" : "参考";
if (recommendation === "insufficient") return isEn ? "Thin" : "样本少";
return "";
}
function debQualityClass(quality: DebQuality | null | undefined) {
const tier = quality?.quality_tier;
if (tier === "high") return "border-emerald-200 bg-emerald-50 text-emerald-700";
if (tier === "medium") return "border-amber-200 bg-amber-50 text-amber-700";
if (tier === "low") return "border-rose-200 bg-rose-50 text-rose-700";
return "border-slate-200 bg-slate-50 text-slate-500";
}
function DebQualityBadge({ quality, isEn }: { quality?: DebQuality | null; isEn: boolean }) {
const label = debQualityLabel(quality, isEn);
if (!label) return null;
const hitRate = quality?.recent_hit_rate;
const samples = quality?.recent_samples;
const titleParts = [
isEn ? `DEB recommendation: ${label}` : `DEB 建议:${label}`,
hitRate == null ? null : `${hitRate.toFixed(0)}%`,
samples == null ? null : `n=${samples}`,
].filter(Boolean);
return (
<span
className={clsx("ml-1.5 inline-flex items-center rounded border px-1.5 py-0.5 text-[9px] font-black uppercase leading-none", debQualityClass(quality))}
title={titleParts.join(" · ")}
>
{label}
</span>
);
}
function buildStatsLabels({
isEn,
isShenzhen,
@@ -82,6 +126,7 @@ export function TemperatureStatsBars({
observedHighRunway,
wundergroundDailyHigh,
debVal,
debQuality,
modelMin,
modelMax,
spread,
@@ -104,6 +149,7 @@ export function TemperatureStatsBars({
observedHighRunway: number | null;
wundergroundDailyHigh: number | null;
debVal: number | null;
debQuality?: DebQuality | null;
modelMin: number | null;
modelMax: number | null;
spread: number | null;
@@ -139,6 +185,7 @@ export function TemperatureStatsBars({
<div className="flex items-center gap-4 text-[11px]">
<span className="font-semibold text-slate-500">
DEB: <strong className="text-orange-600 font-mono">{temp(debVal, tempSymbol)}</strong>
<DebQualityBadge quality={debQuality} isEn={isEn} />
</span>
{modelMin !== null && modelMax !== null && (
<>
@@ -187,6 +234,7 @@ export function TemperatureStatsBars({
<div className="flex flex-col">
<span className="text-[11px] font-semibold text-slate-500 uppercase tracking-wider">
DEB Max
<DebQualityBadge quality={debQuality} isEn={isEn} />
</span>
<span className="text-2xl font-bold font-mono text-orange-600 mt-1">
{temp(debVal, tempSymbol)}
@@ -237,6 +285,7 @@ export function TemperatureStatsBars({
</span>
<strong className="text-blue-600 font-bold">
{temp(debVal, tempSymbol)}
<DebQualityBadge quality={debQuality} isEn={isEn} />
</strong>
</div>
<div className="flex flex-col gap-0.5">
@@ -263,3 +312,4 @@ export function TemperatureStatsBars({
}
export const __buildTemperatureStatsLabelsForTest = buildStatsLabels;
export const __buildDebQualityLabelForTest = debQualityLabel;
@@ -1,4 +1,4 @@
import { __buildTemperatureStatsLabelsForTest } from "@/components/dashboard/scan-terminal/TemperatureStatsBars";
import { __buildDebQualityLabelForTest, __buildTemperatureStatsLabelsForTest } from "@/components/dashboard/scan-terminal/TemperatureStatsBars";
import { temp } from "@/components/dashboard/scan-terminal/utils";
function assert(condition: unknown, message: string) {
@@ -63,4 +63,12 @@ export function runTests() {
assert(temp(null, "°C") === "--", "empty temperature values should not render as 0.0°C while city detail is loading");
assert(temp(undefined, "°C") === "--", "undefined temperature values should not render as 0.0°C while city detail is loading");
assert(temp("", "°C") === "--", "blank temperature values should not render as 0.0°C while city detail is loading");
assert(
__buildDebQualityLabelForTest({ recommendation: "context_only" }, true) === "Context",
"low-confidence DEB should render as context-only guidance in English",
);
assert(
__buildDebQualityLabelForTest({ recommendation: "insufficient" }, false) === "样本少",
"thin-sample DEB should render a Chinese low-sample label",
);
}
@@ -6,6 +6,7 @@ import type {
ScanOpportunityRow,
ForecastDay,
DailyModelForecast,
DebForecast,
DebHourlyPath,
ProbabilityBucket,
} from "@/lib/dashboard-types";
@@ -940,6 +941,7 @@ function runwayPatchPointsFromRunwayObs(runwayObs: any) {
type HourlyForecast = {
forecastTodayHigh?: number | null;
debPrediction?: number | null;
debQuality?: Pick<DebForecast, "quality_tier" | "recommendation" | "recent_hit_rate" | "recent_samples" | "recent_hits" | "recent_mae"> | null;
debHourlyPath?: DebHourlyPath | null;
localDate?: string | null;
localTime?: string | null;
@@ -967,6 +969,7 @@ function seedHourlyForecastFromRow(row: ScanOpportunityRow | null): HourlyForeca
return {
forecastTodayHigh: null,
debPrediction: validNumber(row.deb_prediction),
debQuality: null,
debHourlyPath: null,
localDate: row.local_date || null,
localTime: row.local_time || null,
@@ -1029,6 +1032,14 @@ function parseHourlyForecastFromCityDetail(json: CityDetail | null): HourlyForec
return {
forecastTodayHigh: json.forecast?.today_high ?? null,
debPrediction: json.deb?.prediction ?? (json as any)?.overview?.deb_prediction ?? null,
debQuality: json.deb ? {
quality_tier: json.deb.quality_tier,
recommendation: json.deb.recommendation,
recent_hit_rate: json.deb.recent_hit_rate,
recent_samples: json.deb.recent_samples,
recent_hits: json.deb.recent_hits,
recent_mae: json.deb.recent_mae,
} : null,
debHourlyPath: json.deb?.hourly_path || null,
localDate: json.local_date || (json as any)?.overview?.local_date || null,
localTime: json.local_time || null,
@@ -1366,6 +1377,7 @@ function mergePatchIntoHourly(
...(prev || {
forecastTodayHigh: null,
debPrediction: null,
debQuality: null,
localDate: null,
localTime: null,
times: [],