Use fresh city detail for scan list probabilities

This commit is contained in:
2569718930@qq.com
2026-04-25 02:41:38 +08:00
parent 62a12257f0
commit 2b9e10384e
3 changed files with 129 additions and 27 deletions
@@ -219,31 +219,118 @@ function getEmosPeak(row: ScanOpportunityRow, tempSymbol?: string | null) {
return { label: peakLabel, probability: peakProbability };
}
function normalizeLookupKey(value?: string | null) {
return String(value || "")
.trim()
.toLowerCase()
.replace(/[\s_-]+/g, "");
}
function getDetailForRow(
row: Pick<ScanOpportunityRow, "city" | "city_display_name" | "display_name">,
cityDetailsByName?: Record<string, CityDetail>,
) {
if (!cityDetailsByName) return null;
const rowKeys = [row.city, row.city_display_name, row.display_name]
.map((value) => String(value || "").trim().toLowerCase())
.map(normalizeLookupKey)
.filter(Boolean);
return (
Object.entries(cityDetailsByName).find(([name, detail]) => {
const detailKeys = [name, detail.name, detail.display_name]
.map((value) => String(value || "").trim().toLowerCase())
.map(normalizeLookupKey)
.filter(Boolean);
return rowKeys.some((key) => detailKeys.includes(key));
})?.[1] || null
);
}
function getDetailViewDate(detail: CityDetail, row?: ScanOpportunityRow | null) {
if (!row) return detail.local_date;
const rawDate = row.selected_date || row.local_date || "";
const phase = String(row.window_phase || "").toLowerCase();
if ((phase === "tomorrow" || phase === "week_ahead") && rawDate) return rawDate;
if (!rawDate || rawDate === detail.local_date || row.local_date === detail.local_date) {
return detail.local_date;
}
return detail.local_date || rawDate;
}
function normalizeBucketLabel(value?: string | null, tempSymbol?: string | null) {
return normalizeTemperatureLabel(value, tempSymbol)
.toLowerCase()
.replace(/\s+/g, "")
.replace(/℃/g, "°c");
}
function extractNumbers(value?: string | null) {
return Array.from(String(value || "").matchAll(/-?\d+(?:\.\d+)?/g)).map((match) =>
Number(match[0]),
);
}
function getBucketText(bucket: { label?: string | null; bucket?: string | null; range?: string | null }) {
return [bucket.label, bucket.bucket, bucket.range]
.map((value) => String(value || "").trim())
.filter(Boolean);
}
function bucketMatchesRow(
bucket: {
label?: string | null;
bucket?: string | null;
range?: string | null;
value?: number | string | null;
},
row: ScanOpportunityRow,
tempSymbol?: string | null,
) {
const targetLabel = normalizeBucketLabel(row.target_label, tempSymbol);
const bucketLabels = getBucketText(bucket).map((label) =>
normalizeBucketLabel(label, tempSymbol),
);
if (targetLabel && bucketLabels.some((label) => label === targetLabel)) {
return true;
}
const rawTargetLabel = String(row.target_label || "");
const targetNumbers = extractNumbers(rawTargetLabel);
const targetValue =
row.target_value ?? row.target_threshold ?? row.target_lower ?? row.target_upper ?? targetNumbers[0] ?? null;
if (targetValue == null || !Number.isFinite(Number(targetValue))) return false;
const bucketNumbers = [
...(bucket.value != null && Number.isFinite(Number(bucket.value))
? [Number(bucket.value)]
: []),
...getBucketText(bucket).flatMap(extractNumbers),
];
const matchesNumber = bucketNumbers.some(
(value) => Math.abs(Number(value) - Number(targetValue)) < 0.05,
);
if (!matchesNumber) return false;
const targetIsUpper =
/(\+|以上|or\s*above|above|greater|>=|≥)/i.test(rawTargetLabel) ||
(row.target_lower != null && row.target_upper == null);
const targetIsLower =
/(<=|≤|below|or\s*below|以下)/i.test(rawTargetLabel) ||
(row.target_upper != null && row.target_lower == null);
const bucketRaw = getBucketText(bucket).join(" ");
const bucketIsUpper = /(\+|以上|or\s*above|above|greater|>=|≥|inf|∞)/i.test(bucketRaw);
const bucketIsLower = /(<=|≤|below|or\s*below|以下|-inf|-∞)/i.test(bucketRaw);
if (targetIsUpper || bucketIsUpper) return targetIsUpper === bucketIsUpper;
if (targetIsLower || bucketIsLower) return targetIsLower === bucketIsLower;
return true;
}
function getDetailPeak(
detail: CityDetail | null,
targetDate?: string | null,
row?: ScanOpportunityRow | null,
tempSymbol?: string | null,
) {
if (!detail) return null;
const view = getProbabilityView(detail, targetDate);
const view = getProbabilityView(detail, getDetailViewDate(detail, row));
const buckets = Array.isArray(view.probabilitiesAll)
? view.probabilitiesAll
: [];
@@ -270,30 +357,12 @@ function getDetailBucketEventProbability(
tempSymbol?: string | null,
) {
if (!detail) return null;
const view = getProbabilityView(detail, row.selected_date || row.local_date);
const view = getProbabilityView(detail, getDetailViewDate(detail, row));
const buckets = Array.isArray(view.probabilitiesAll)
? view.probabilitiesAll
: [];
if (!buckets.length) return null;
const targetLabel = normalizeTemperatureLabel(row.target_label, tempSymbol);
const targetValue =
row.target_value ?? row.target_threshold ?? row.target_lower ?? row.target_upper ?? null;
const matched =
buckets.find((bucket) => {
const label = normalizeTemperatureLabel(
bucket.label || bucket.bucket || bucket.range,
tempSymbol,
);
return Boolean(targetLabel && label && label === targetLabel);
}) ||
buckets.find((bucket) => {
const value = bucket.value;
return (
value != null &&
targetValue != null &&
Math.abs(Number(value) - Number(targetValue)) < 0.01
);
});
const matched = buckets.find((bucket) => bucketMatchesRow(bucket, row, tempSymbol));
return normalizeProbability(matched?.probability);
}
@@ -325,9 +394,9 @@ function buildOpportunityGroups(
row.city_display_name || row.display_name || row.city,
locale,
);
const date = row.selected_date || row.local_date || "";
const date = detail ? getDetailViewDate(detail, row) : row.selected_date || row.local_date || "";
const key = `${row.city || cityName}|${date}`;
const peak = getDetailPeak(detail, date, tempSymbol) || getEmosPeak(row, tempSymbol);
const peak = getDetailPeak(detail, row, tempSymbol) || getEmosPeak(row, tempSymbol);
const modelView = detail ? getModelView(detail, date) : null;
const debPrediction = modelView?.deb ?? row.deb_prediction ?? null;
const existing = groups.get(key);
@@ -16,6 +16,7 @@ import {
useDeferredValue,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import styles from "./Dashboard.module.css";
@@ -475,6 +476,7 @@ function ScanTerminalScreen() {
const [userLocalTime, setUserLocalTime] = useState("--");
const [themeMode, setThemeMode] = useState<ThemeMode>("dark");
const deferredRows = useDeferredValue(terminalData?.rows || []);
const preloadedSnapshotRef = useRef<string | null>(null);
const prependAiLogs = (entries: ScanAiLogEntry[]) => {
setAiLogs((current) => [...entries, ...current].slice(0, 8));
@@ -736,6 +738,37 @@ function ScanTerminalScreen() {
void fetchTerminal(DEFAULT_FILTERS, false);
}, [isPro]);
useEffect(() => {
if (!isPro || !terminalData?.rows.length) return;
const snapshotKey =
terminalData.snapshot_id ||
terminalData.generated_at ||
terminalData.rows.map((row) => row.id).join("|");
if (!snapshotKey || preloadedSnapshotRef.current === snapshotKey) return;
preloadedSnapshotRef.current = snapshotKey;
const seen = new Set<string>();
const cities = sortRowsByUserTime(terminalData.rows)
.map((row) => String(row.city || row.city_display_name || row.display_name || "").trim())
.filter((city) => {
const key = normalizeCityKey(city);
if (!key || seen.has(key)) return false;
seen.add(key);
return true;
})
.slice(0, 12);
cities.forEach((cityName) => {
void store.ensureCityDetail(cityName, true, "market").catch(() => {});
});
}, [
isPro,
store.ensureCityDetail,
terminalData?.rows,
terminalData?.generated_at,
terminalData?.snapshot_id,
]);
useEffect(() => {
if (!isPro) return;
const intervalId = window.setInterval(() => {
+1 -1
View File
@@ -1271,7 +1271,7 @@ export function getProbabilityView(detail: CityDetail, targetDate?: string | nul
engine: null,
mu: daily?.deb?.prediction ?? null,
probabilities: daily?.probabilities || [],
probabilitiesAll: daily?.probabilities || [],
probabilitiesAll: daily?.probabilities_all || daily?.probabilities || [],
shadowProbabilities: [],
shadowProbabilitiesAll: [],
};