Use fresh city detail for scan list probabilities
This commit is contained in:
@@ -219,31 +219,118 @@ function getEmosPeak(row: ScanOpportunityRow, tempSymbol?: string | null) {
|
|||||||
return { label: peakLabel, probability: peakProbability };
|
return { label: peakLabel, probability: peakProbability };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeLookupKey(value?: string | null) {
|
||||||
|
return String(value || "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[\s_-]+/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
function getDetailForRow(
|
function getDetailForRow(
|
||||||
row: Pick<ScanOpportunityRow, "city" | "city_display_name" | "display_name">,
|
row: Pick<ScanOpportunityRow, "city" | "city_display_name" | "display_name">,
|
||||||
cityDetailsByName?: Record<string, CityDetail>,
|
cityDetailsByName?: Record<string, CityDetail>,
|
||||||
) {
|
) {
|
||||||
if (!cityDetailsByName) return null;
|
if (!cityDetailsByName) return null;
|
||||||
const rowKeys = [row.city, row.city_display_name, row.display_name]
|
const rowKeys = [row.city, row.city_display_name, row.display_name]
|
||||||
.map((value) => String(value || "").trim().toLowerCase())
|
.map(normalizeLookupKey)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
return (
|
return (
|
||||||
Object.entries(cityDetailsByName).find(([name, detail]) => {
|
Object.entries(cityDetailsByName).find(([name, detail]) => {
|
||||||
const detailKeys = [name, detail.name, detail.display_name]
|
const detailKeys = [name, detail.name, detail.display_name]
|
||||||
.map((value) => String(value || "").trim().toLowerCase())
|
.map(normalizeLookupKey)
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
return rowKeys.some((key) => detailKeys.includes(key));
|
return rowKeys.some((key) => detailKeys.includes(key));
|
||||||
})?.[1] || null
|
})?.[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(
|
function getDetailPeak(
|
||||||
detail: CityDetail | null,
|
detail: CityDetail | null,
|
||||||
targetDate?: string | null,
|
row?: ScanOpportunityRow | null,
|
||||||
tempSymbol?: string | null,
|
tempSymbol?: string | null,
|
||||||
) {
|
) {
|
||||||
if (!detail) return null;
|
if (!detail) return null;
|
||||||
const view = getProbabilityView(detail, targetDate);
|
const view = getProbabilityView(detail, getDetailViewDate(detail, row));
|
||||||
const buckets = Array.isArray(view.probabilitiesAll)
|
const buckets = Array.isArray(view.probabilitiesAll)
|
||||||
? view.probabilitiesAll
|
? view.probabilitiesAll
|
||||||
: [];
|
: [];
|
||||||
@@ -270,30 +357,12 @@ function getDetailBucketEventProbability(
|
|||||||
tempSymbol?: string | null,
|
tempSymbol?: string | null,
|
||||||
) {
|
) {
|
||||||
if (!detail) return 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)
|
const buckets = Array.isArray(view.probabilitiesAll)
|
||||||
? view.probabilitiesAll
|
? view.probabilitiesAll
|
||||||
: [];
|
: [];
|
||||||
if (!buckets.length) return null;
|
if (!buckets.length) return null;
|
||||||
const targetLabel = normalizeTemperatureLabel(row.target_label, tempSymbol);
|
const matched = buckets.find((bucket) => bucketMatchesRow(bucket, row, 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
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return normalizeProbability(matched?.probability);
|
return normalizeProbability(matched?.probability);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,9 +394,9 @@ function buildOpportunityGroups(
|
|||||||
row.city_display_name || row.display_name || row.city,
|
row.city_display_name || row.display_name || row.city,
|
||||||
locale,
|
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 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 modelView = detail ? getModelView(detail, date) : null;
|
||||||
const debPrediction = modelView?.deb ?? row.deb_prediction ?? null;
|
const debPrediction = modelView?.deb ?? row.deb_prediction ?? null;
|
||||||
const existing = groups.get(key);
|
const existing = groups.get(key);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
useDeferredValue,
|
useDeferredValue,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import styles from "./Dashboard.module.css";
|
import styles from "./Dashboard.module.css";
|
||||||
@@ -475,6 +476,7 @@ function ScanTerminalScreen() {
|
|||||||
const [userLocalTime, setUserLocalTime] = useState("--");
|
const [userLocalTime, setUserLocalTime] = useState("--");
|
||||||
const [themeMode, setThemeMode] = useState<ThemeMode>("dark");
|
const [themeMode, setThemeMode] = useState<ThemeMode>("dark");
|
||||||
const deferredRows = useDeferredValue(terminalData?.rows || []);
|
const deferredRows = useDeferredValue(terminalData?.rows || []);
|
||||||
|
const preloadedSnapshotRef = useRef<string | null>(null);
|
||||||
|
|
||||||
const prependAiLogs = (entries: ScanAiLogEntry[]) => {
|
const prependAiLogs = (entries: ScanAiLogEntry[]) => {
|
||||||
setAiLogs((current) => [...entries, ...current].slice(0, 8));
|
setAiLogs((current) => [...entries, ...current].slice(0, 8));
|
||||||
@@ -736,6 +738,37 @@ function ScanTerminalScreen() {
|
|||||||
void fetchTerminal(DEFAULT_FILTERS, false);
|
void fetchTerminal(DEFAULT_FILTERS, false);
|
||||||
}, [isPro]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!isPro) return;
|
if (!isPro) return;
|
||||||
const intervalId = window.setInterval(() => {
|
const intervalId = window.setInterval(() => {
|
||||||
|
|||||||
@@ -1271,7 +1271,7 @@ export function getProbabilityView(detail: CityDetail, targetDate?: string | nul
|
|||||||
engine: null,
|
engine: null,
|
||||||
mu: daily?.deb?.prediction ?? null,
|
mu: daily?.deb?.prediction ?? null,
|
||||||
probabilities: daily?.probabilities || [],
|
probabilities: daily?.probabilities || [],
|
||||||
probabilitiesAll: daily?.probabilities || [],
|
probabilitiesAll: daily?.probabilities_all || daily?.probabilities || [],
|
||||||
shadowProbabilities: [],
|
shadowProbabilities: [],
|
||||||
shadowProbabilitiesAll: [],
|
shadowProbabilitiesAll: [],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user