Model city decision states before rendering cards
The city card had accumulated independent checks for AI status, market availability, stale observations, breakouts, peak timing, model consensus, and next-bulletin waits. This introduces a CityDecisionState builder so UI components consume one coherent recommendation, urgency, evidence quality, AI status, market status, badges, and primary reason. Constraint: Keep the current card copy and badge priority behavior while moving decision-state ownership out of the component. Rejected: Refactor calendar and opportunity lanes in the same commit | the shared model should land behind the city card first, then other views can migrate with smaller visual-risk diffs. Confidence: high Scope-risk: moderate Reversibility: clean Tested: npm run build Not-tested: Browser visual regression and dedicated unit tests for every decision-state combination.
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
|||||||
buildWeatherDecisionView,
|
buildWeatherDecisionView,
|
||||||
resolveExpectedHighCandidate,
|
resolveExpectedHighCandidate,
|
||||||
} from "@/components/dashboard/scan-terminal/city-card-decision-utils";
|
} from "@/components/dashboard/scan-terminal/city-card-decision-utils";
|
||||||
|
import { buildCityDecisionState } from "@/components/dashboard/scan-terminal/city-decision-state";
|
||||||
import { getPeakWindowLabel, normalizeCityKey } from "@/components/dashboard/scan-terminal/decision-utils";
|
import { getPeakWindowLabel, normalizeCityKey } from "@/components/dashboard/scan-terminal/decision-utils";
|
||||||
import { LoadingSignal } from "@/components/dashboard/scan-terminal/LoadingSignal";
|
import { LoadingSignal } from "@/components/dashboard/scan-terminal/LoadingSignal";
|
||||||
import type { AiPinnedCity } from "@/components/dashboard/scan-terminal/types";
|
import type { AiPinnedCity } from "@/components/dashboard/scan-terminal/types";
|
||||||
@@ -95,13 +96,6 @@ function isHkoObservationCity(detail?: CityDetail | null) {
|
|||||||
return source === "hko";
|
return source === "hko";
|
||||||
}
|
}
|
||||||
|
|
||||||
type StatusTagTone = "green" | "blue" | "amber" | "red" | "muted";
|
|
||||||
|
|
||||||
type StatusTag = {
|
|
||||||
label: string;
|
|
||||||
tone: StatusTagTone;
|
|
||||||
};
|
|
||||||
|
|
||||||
function formatFreshnessAge(value: unknown, isEn: boolean) {
|
function formatFreshnessAge(value: unknown, isEn: boolean) {
|
||||||
const minutes = Number(value);
|
const minutes = Number(value);
|
||||||
if (!Number.isFinite(minutes) || minutes < 0) return "";
|
if (!Number.isFinite(minutes) || minutes < 0) return "";
|
||||||
@@ -195,15 +189,6 @@ function buildMarketFreshnessValue({
|
|||||||
return isEn ? "synced" : "已同步";
|
return isEn ? "synced" : "已同步";
|
||||||
}
|
}
|
||||||
|
|
||||||
function uniqueStatusTags(tags: Array<StatusTag | null | undefined>) {
|
|
||||||
const seen = new Set<string>();
|
|
||||||
return tags.filter((tag): tag is StatusTag => {
|
|
||||||
if (!tag?.label || seen.has(tag.label)) return false;
|
|
||||||
seen.add(tag.label);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AiPinnedCityCard({
|
export function AiPinnedCityCard({
|
||||||
item,
|
item,
|
||||||
detail,
|
detail,
|
||||||
@@ -460,42 +445,21 @@ export function AiPinnedCityCard({
|
|||||||
const aiRuleEvidenceText = isEn
|
const aiRuleEvidenceText = 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 解读未完整返回,当前使用规则证据";
|
||||||
const aiStatusLabel =
|
const decisionState = buildCityDecisionState({
|
||||||
aiForecast.status === "loading"
|
aiCityForecast,
|
||||||
? isEn
|
aiForecast,
|
||||||
? "Fast read ready"
|
aiRuleEvidenceMode,
|
||||||
: "快速判断已完成"
|
isEn,
|
||||||
: aiForecast.status === "ready" && aiCityForecast
|
isHkoObservation,
|
||||||
? aiRuleEvidenceMode
|
marketDecisionView,
|
||||||
? isEn
|
modelHighlyConsistent,
|
||||||
? "Rule evidence"
|
needsNextBulletin,
|
||||||
: "规则证据模式"
|
observationStale,
|
||||||
: isEn
|
observedHighBreak,
|
||||||
? "AI read complete"
|
observedLowBreak,
|
||||||
: "AI 解读已完成"
|
observedLowLag,
|
||||||
: aiRuleEvidenceMode
|
peakHasPassed,
|
||||||
? isEn
|
});
|
||||||
? "Rule evidence"
|
|
||||||
: "规则证据模式"
|
|
||||||
: isEn
|
|
||||||
? "AI pending"
|
|
||||||
: "AI 待返回";
|
|
||||||
const aiStatusTone: StatusTagTone =
|
|
||||||
aiForecast.status === "loading"
|
|
||||||
? "blue"
|
|
||||||
: aiForecast.status === "ready" && aiCityForecast
|
|
||||||
? aiRuleEvidenceMode
|
|
||||||
? "amber"
|
|
||||||
: "green"
|
|
||||||
: aiRuleEvidenceMode
|
|
||||||
? "amber"
|
|
||||||
: "muted";
|
|
||||||
const marketStatusTone: StatusTagTone =
|
|
||||||
marketDecisionView.status === "ready"
|
|
||||||
? "green"
|
|
||||||
: marketDecisionView.status === "loading"
|
|
||||||
? "blue"
|
|
||||||
: "muted";
|
|
||||||
const dataFreshnessRows = [
|
const dataFreshnessRows = [
|
||||||
{
|
{
|
||||||
label: isHkoObservation ? (isEn ? "HKO" : "天文台") : "METAR",
|
label: isHkoObservation ? (isEn ? "HKO" : "天文台") : "METAR",
|
||||||
@@ -524,62 +488,6 @@ export function AiPinnedCityCard({
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
const freshnessSeparator = isEn ? ": " : ":";
|
const freshnessSeparator = isEn ? ": " : ":";
|
||||||
const statusTags = uniqueStatusTags([
|
|
||||||
observedHighBreak
|
|
||||||
? {
|
|
||||||
label: isEn ? "Observed breakout" : "实测突破",
|
|
||||||
tone: "red",
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
peakHasPassed
|
|
||||||
? {
|
|
||||||
label: isEn ? "Peak window passed" : "峰值窗口已过",
|
|
||||||
tone: "muted",
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
observationStale
|
|
||||||
? {
|
|
||||||
label: isEn
|
|
||||||
? isHkoObservation
|
|
||||||
? "HKO stale"
|
|
||||||
: "METAR stale"
|
|
||||||
: isHkoObservation
|
|
||||||
? "观测过旧"
|
|
||||||
: "METAR 过旧",
|
|
||||||
tone: "amber",
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
observedLowBreak
|
|
||||||
? {
|
|
||||||
label: isEn ? "Peak revised down" : "峰值下修",
|
|
||||||
tone: "blue",
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
aiForecast.status === "loading"
|
|
||||||
? {
|
|
||||||
label: isEn ? "Fast read ready" : "快速判断已完成",
|
|
||||||
tone: aiStatusTone,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
marketDecisionView.status === "unavailable"
|
|
||||||
? {
|
|
||||||
label: isEn ? "Market unavailable" : "市场价暂不可用",
|
|
||||||
tone: marketStatusTone,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
modelHighlyConsistent
|
|
||||||
? {
|
|
||||||
label: isEn ? "Models agree" : "模型高度一致",
|
|
||||||
tone: "green",
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
observedLowLag || needsNextBulletin
|
|
||||||
? {
|
|
||||||
label: isEn ? "Wait next report" : "需要等待下一报文",
|
|
||||||
tone: "amber",
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
]).slice(0, 3);
|
|
||||||
const localizedRisksRaw =
|
const localizedRisksRaw =
|
||||||
(isEn ? aiCityForecast?.risks_en : aiCityForecast?.risks_zh) || [];
|
(isEn ? aiCityForecast?.risks_en : aiCityForecast?.risks_zh) || [];
|
||||||
const localizedRisks = Array.isArray(localizedRisksRaw)
|
const localizedRisks = Array.isArray(localizedRisksRaw)
|
||||||
@@ -597,33 +505,6 @@ export function AiPinnedCityCard({
|
|||||||
(isEn ? aiForecast.payload?.reason_en : aiForecast.payload?.reason_zh) ||
|
(isEn ? aiForecast.payload?.reason_en : aiForecast.payload?.reason_zh) ||
|
||||||
aiForecast.payload?.reason ||
|
aiForecast.payload?.reason ||
|
||||||
"";
|
"";
|
||||||
const decisionWhyText = observedHighBreak
|
|
||||||
? isEn
|
|
||||||
? "Worth watching now: observation has broken above the model range."
|
|
||||||
: "当前值得关注:实测已突破模型上沿。"
|
|
||||||
: peakHasPassed
|
|
||||||
? isEn
|
|
||||||
? "Avoid chasing now: peak window has passed; wait to confirm no new high."
|
|
||||||
: "当前不宜追高:峰值窗口已过,等待确认是否还有新高。"
|
|
||||||
: observationStale
|
|
||||||
? isEn
|
|
||||||
? "Use as background only: observation is stale and needs the next report."
|
|
||||||
: "当前仅作背景:观测已过旧,需要下一报文确认。"
|
|
||||||
: marketDecisionView.status === "unavailable"
|
|
||||||
? isEn
|
|
||||||
? "Weather evidence remains usable, but no tradable quote is available yet."
|
|
||||||
: "当前可参考天气:暂无可交易价格。"
|
|
||||||
: modelHighlyConsistent
|
|
||||||
? isEn
|
|
||||||
? "Worth watching now: models agree; wait for observation confirmation."
|
|
||||||
: "当前值得关注:模型高度一致,等待实测确认。"
|
|
||||||
: needsNextBulletin
|
|
||||||
? isEn
|
|
||||||
? "Wait for confirmation: the next bulletin should decide direction."
|
|
||||||
: "当前建议等待:下一报文更适合决定方向。"
|
|
||||||
: isEn
|
|
||||||
? "Watch the peak window and compare observations against the expected high."
|
|
||||||
: "当前重点:盯住峰值窗口,把实测与预计高点对照。";
|
|
||||||
const marketLineText =
|
const marketLineText =
|
||||||
marketDecisionView.status === "ready"
|
marketDecisionView.status === "ready"
|
||||||
? `${marketDecisionView.bucketLabel} · ${marketDecisionView.priceText}`
|
? `${marketDecisionView.bucketLabel} · ${marketDecisionView.priceText}`
|
||||||
@@ -638,10 +519,17 @@ export function AiPinnedCityCard({
|
|||||||
const collapseId = `ai-city-body-${normalizeCityKey(item.cityName) || item.addedAt}`;
|
const collapseId = `ai-city-body-${normalizeCityKey(item.cityName) || item.addedAt}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className={clsx("scan-ai-city-card", collapsed && "collapsed", removing && "removing")}>
|
<article
|
||||||
|
className={clsx("scan-ai-city-card", collapsed && "collapsed", removing && "removing")}
|
||||||
|
data-ai-status={decisionState.aiStatus}
|
||||||
|
data-evidence-quality={decisionState.evidenceQuality}
|
||||||
|
data-market-status={decisionState.marketStatus}
|
||||||
|
data-recommendation={decisionState.recommendation}
|
||||||
|
data-urgency={decisionState.urgency}
|
||||||
|
>
|
||||||
<CityCardHeader
|
<CityCardHeader
|
||||||
aiStatusLabel={aiStatusLabel}
|
aiStatusLabel={decisionState.aiStatusLabel}
|
||||||
aiStatusTone={aiStatusTone}
|
aiStatusTone={decisionState.aiStatusTone}
|
||||||
collapseId={collapseId}
|
collapseId={collapseId}
|
||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
currentTempText={currentTempText}
|
currentTempText={currentTempText}
|
||||||
@@ -675,7 +563,7 @@ export function AiPinnedCityCard({
|
|||||||
peakWindow={peakWindow}
|
peakWindow={peakWindow}
|
||||||
removing={removing}
|
removing={removing}
|
||||||
rowLocalTime={row?.local_time}
|
rowLocalTime={row?.local_time}
|
||||||
statusTags={statusTags}
|
statusTags={decisionState.badges}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{detail && !collapsed ? (
|
{detail && !collapsed ? (
|
||||||
@@ -683,7 +571,7 @@ export function AiPinnedCityCard({
|
|||||||
<WeatherDecisionBand
|
<WeatherDecisionBand
|
||||||
currentTempText={currentTempText}
|
currentTempText={currentTempText}
|
||||||
decisionView={decisionView}
|
decisionView={decisionView}
|
||||||
decisionWhyText={decisionWhyText}
|
decisionWhyText={decisionState.primaryReason}
|
||||||
isEn={isEn}
|
isEn={isEn}
|
||||||
longText={localizedFinalJudgment || paceText}
|
longText={localizedFinalJudgment || paceText}
|
||||||
marketDecisionView={marketDecisionView}
|
marketDecisionView={marketDecisionView}
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
import type { MarketDecisionView } from "@/components/dashboard/scan-terminal/city-card-decision-utils";
|
||||||
|
import type {
|
||||||
|
AiCityForecastPayload,
|
||||||
|
AiCityForecastState,
|
||||||
|
} from "@/components/dashboard/scan-terminal/types";
|
||||||
|
|
||||||
|
export type CityDecisionUrgency = "now" | "soon" | "later" | "past";
|
||||||
|
export type CityDecisionRecommendation = "watch" | "wait" | "avoid" | "background";
|
||||||
|
export type CityDecisionEvidenceQuality = "fresh" | "mixed" | "stale";
|
||||||
|
export type CityDecisionAiStatus =
|
||||||
|
| "fast-ready"
|
||||||
|
| "deepseek-loading"
|
||||||
|
| "complete"
|
||||||
|
| "fallback";
|
||||||
|
export type CityDecisionMarketStatus = "ready" | "loading" | "unavailable";
|
||||||
|
export type StatusBadgeTone = "green" | "blue" | "amber" | "red" | "muted";
|
||||||
|
|
||||||
|
export type StatusBadge = {
|
||||||
|
label: string;
|
||||||
|
tone: StatusBadgeTone;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CityDecisionState = {
|
||||||
|
urgency: CityDecisionUrgency;
|
||||||
|
recommendation: CityDecisionRecommendation;
|
||||||
|
evidenceQuality: CityDecisionEvidenceQuality;
|
||||||
|
aiStatus: CityDecisionAiStatus;
|
||||||
|
aiStatusLabel: string;
|
||||||
|
aiStatusTone: StatusBadgeTone;
|
||||||
|
marketStatus: CityDecisionMarketStatus;
|
||||||
|
marketStatusTone: StatusBadgeTone;
|
||||||
|
badges: StatusBadge[];
|
||||||
|
primaryReason: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function uniqueStatusBadges(badges: Array<StatusBadge | null | undefined>) {
|
||||||
|
const seen = new Set<string>();
|
||||||
|
return badges.filter((badge): badge is StatusBadge => {
|
||||||
|
if (!badge?.label || seen.has(badge.label)) return false;
|
||||||
|
seen.add(badge.label);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAiStatus({
|
||||||
|
aiCityForecast,
|
||||||
|
aiForecast,
|
||||||
|
aiRuleEvidenceMode,
|
||||||
|
}: {
|
||||||
|
aiCityForecast: AiCityForecastPayload["city_forecast"] | null;
|
||||||
|
aiForecast: AiCityForecastState;
|
||||||
|
aiRuleEvidenceMode: boolean;
|
||||||
|
}): CityDecisionAiStatus {
|
||||||
|
if (aiRuleEvidenceMode || aiForecast.status === "failed") return "fallback";
|
||||||
|
if (aiForecast.status === "loading") return "deepseek-loading";
|
||||||
|
if (aiForecast.status === "ready" && aiCityForecast) return "complete";
|
||||||
|
return "fast-ready";
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveAiStatusView(aiStatus: CityDecisionAiStatus, isEn: boolean) {
|
||||||
|
if (aiStatus === "deepseek-loading") {
|
||||||
|
return {
|
||||||
|
label: isEn ? "Fast read ready" : "快速判断已完成",
|
||||||
|
tone: "blue" as const,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (aiStatus === "complete") {
|
||||||
|
return {
|
||||||
|
label: isEn ? "AI read complete" : "AI 解读已完成",
|
||||||
|
tone: "green" as const,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (aiStatus === "fallback") {
|
||||||
|
return {
|
||||||
|
label: isEn ? "Rule evidence" : "规则证据模式",
|
||||||
|
tone: "amber" as const,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
label: isEn ? "AI pending" : "AI 待返回",
|
||||||
|
tone: "muted" as const,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveMarketTone(status: CityDecisionMarketStatus): StatusBadgeTone {
|
||||||
|
if (status === "ready") return "green";
|
||||||
|
if (status === "loading") return "blue";
|
||||||
|
return "muted";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildCityDecisionState({
|
||||||
|
aiCityForecast,
|
||||||
|
aiForecast,
|
||||||
|
aiRuleEvidenceMode,
|
||||||
|
isEn,
|
||||||
|
isHkoObservation,
|
||||||
|
marketDecisionView,
|
||||||
|
modelHighlyConsistent,
|
||||||
|
needsNextBulletin,
|
||||||
|
observationStale,
|
||||||
|
observedHighBreak,
|
||||||
|
observedLowBreak,
|
||||||
|
observedLowLag,
|
||||||
|
peakHasPassed,
|
||||||
|
}: {
|
||||||
|
aiCityForecast: AiCityForecastPayload["city_forecast"] | null;
|
||||||
|
aiForecast: AiCityForecastState;
|
||||||
|
aiRuleEvidenceMode: boolean;
|
||||||
|
isEn: boolean;
|
||||||
|
isHkoObservation: boolean;
|
||||||
|
marketDecisionView: MarketDecisionView;
|
||||||
|
modelHighlyConsistent: boolean;
|
||||||
|
needsNextBulletin: boolean;
|
||||||
|
observationStale: boolean;
|
||||||
|
observedHighBreak: boolean;
|
||||||
|
observedLowBreak: boolean;
|
||||||
|
observedLowLag: boolean;
|
||||||
|
peakHasPassed: boolean;
|
||||||
|
}): CityDecisionState {
|
||||||
|
const aiStatus = resolveAiStatus({ aiCityForecast, aiForecast, aiRuleEvidenceMode });
|
||||||
|
const aiStatusView = resolveAiStatusView(aiStatus, isEn);
|
||||||
|
const marketStatus = marketDecisionView.status;
|
||||||
|
const marketStatusTone = resolveMarketTone(marketStatus);
|
||||||
|
const evidenceQuality: CityDecisionEvidenceQuality = observationStale
|
||||||
|
? "stale"
|
||||||
|
: aiStatus === "fallback" || marketStatus === "unavailable"
|
||||||
|
? "mixed"
|
||||||
|
: "fresh";
|
||||||
|
const urgency: CityDecisionUrgency = peakHasPassed
|
||||||
|
? "past"
|
||||||
|
: observedHighBreak
|
||||||
|
? "now"
|
||||||
|
: needsNextBulletin
|
||||||
|
? "soon"
|
||||||
|
: "later";
|
||||||
|
const recommendation: CityDecisionRecommendation = peakHasPassed
|
||||||
|
? "avoid"
|
||||||
|
: observationStale
|
||||||
|
? "background"
|
||||||
|
: needsNextBulletin
|
||||||
|
? "wait"
|
||||||
|
: "watch";
|
||||||
|
const primaryReason = observedHighBreak
|
||||||
|
? isEn
|
||||||
|
? "Worth watching now: observation has broken above the model range."
|
||||||
|
: "当前值得关注:实测已突破模型上沿。"
|
||||||
|
: peakHasPassed
|
||||||
|
? isEn
|
||||||
|
? "Avoid chasing now: peak window has passed; wait to confirm no new high."
|
||||||
|
: "当前不宜追高:峰值窗口已过,等待确认是否还有新高。"
|
||||||
|
: observationStale
|
||||||
|
? isEn
|
||||||
|
? "Use as background only: observation is stale and needs the next report."
|
||||||
|
: "当前仅作背景:观测已过旧,需要下一报文确认。"
|
||||||
|
: marketStatus === "unavailable"
|
||||||
|
? isEn
|
||||||
|
? "Weather evidence remains usable, but no tradable quote is available yet."
|
||||||
|
: "当前可参考天气:暂无可交易价格。"
|
||||||
|
: modelHighlyConsistent
|
||||||
|
? isEn
|
||||||
|
? "Worth watching now: models agree; wait for observation confirmation."
|
||||||
|
: "当前值得关注:模型高度一致,等待实测确认。"
|
||||||
|
: needsNextBulletin
|
||||||
|
? isEn
|
||||||
|
? "Wait for confirmation: the next bulletin should decide direction."
|
||||||
|
: "当前建议等待:下一报文更适合决定方向。"
|
||||||
|
: isEn
|
||||||
|
? "Watch the peak window and compare observations against the expected high."
|
||||||
|
: "当前重点:盯住峰值窗口,把实测与预计高点对照。";
|
||||||
|
|
||||||
|
const badges = uniqueStatusBadges([
|
||||||
|
observedHighBreak
|
||||||
|
? {
|
||||||
|
label: isEn ? "Observed breakout" : "实测突破",
|
||||||
|
tone: "red",
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
peakHasPassed
|
||||||
|
? {
|
||||||
|
label: isEn ? "Peak window passed" : "峰值窗口已过",
|
||||||
|
tone: "muted",
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
observationStale
|
||||||
|
? {
|
||||||
|
label: isEn
|
||||||
|
? isHkoObservation
|
||||||
|
? "HKO stale"
|
||||||
|
: "METAR stale"
|
||||||
|
: isHkoObservation
|
||||||
|
? "观测过旧"
|
||||||
|
: "METAR 过旧",
|
||||||
|
tone: "amber",
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
observedLowBreak
|
||||||
|
? {
|
||||||
|
label: isEn ? "Peak revised down" : "峰值下修",
|
||||||
|
tone: "blue",
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
aiStatus === "deepseek-loading"
|
||||||
|
? {
|
||||||
|
label: isEn ? "Fast read ready" : "快速判断已完成",
|
||||||
|
tone: aiStatusView.tone,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
marketStatus === "unavailable"
|
||||||
|
? {
|
||||||
|
label: isEn ? "Market unavailable" : "市场价暂不可用",
|
||||||
|
tone: marketStatusTone,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
modelHighlyConsistent
|
||||||
|
? {
|
||||||
|
label: isEn ? "Models agree" : "模型高度一致",
|
||||||
|
tone: "green",
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
observedLowLag || needsNextBulletin
|
||||||
|
? {
|
||||||
|
label: isEn ? "Wait next report" : "需要等待下一报文",
|
||||||
|
tone: "amber",
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
]).slice(0, 3);
|
||||||
|
|
||||||
|
return {
|
||||||
|
urgency,
|
||||||
|
recommendation,
|
||||||
|
evidenceQuality,
|
||||||
|
aiStatus,
|
||||||
|
aiStatusLabel: aiStatusView.label,
|
||||||
|
aiStatusTone: aiStatusView.tone,
|
||||||
|
marketStatus,
|
||||||
|
marketStatusTone,
|
||||||
|
badges,
|
||||||
|
primaryReason,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user