Make dashboard decision cards feel product-ready
Users needed reassurance that unavailable quotes and long AI evidence are normal states, not broken systems. This adds a v1.5.5 upgrade announcement, softens market-unavailable copy, surfaces a one-line recommendation reason, and makes mobile cards prioritize observed temperature, expected high, peak timing, AI expansion, and a separate market line. Constraint: Keep existing dashboard data contracts and avoid backend schema changes. Rejected: Hide unavailable market rows entirely | users still need to know weather evidence remains usable without a quote. Confidence: high Scope-risk: moderate Reversibility: clean Tested: npm run build Not-tested: Browser visual QA on physical mobile devices.
This commit is contained in:
+5
-1
@@ -2,8 +2,12 @@
|
||||
|
||||
## 1.5.5 - 2026-04-27
|
||||
|
||||
- Dashboard 新增 v1.5.5 升级公告,提示所有会员已额外延长 7 天,并集中说明 DeepSeek 机场报文解读、日历行动视图、本地时间峰值窗口和 AI 证据护栏
|
||||
- 城市决策卡空状态与市场不可用文案产品化:将“未接入/缺失”改为“市场价格暂不可用,天气判断仍可参考”,避免用户误以为系统故障
|
||||
- 城市决策卡新增“为什么推荐/为什么不推荐”短句,优先解释实测突破、峰值窗口已过、METAR 过旧、市场暂不可用或模型一致等关键原因
|
||||
- 移动端城市决策卡前置当前温度、预测高点和峰值时间;长 AI 解读在手机端默认折叠,可展开查看,市场价格单独成行展示
|
||||
- 新增 Qingdao / 青岛城市,结算锚点接入 Wunderground 青岛胶东国际机场 `ZSQD` 历史页,并补齐别名、时区、预热、官方来源和前端地区归类
|
||||
- 城市决策卡顶部状态标签收口为 2-3 个高优先级信号,优先展示“实测突破 / 峰值窗口已过 / METAR 过旧 / AI 解读中 / 市场价格缺失 / 模型高度一致 / 需要等待下一报文”,让用户第一眼看到重点
|
||||
- 城市决策卡顶部状态标签收口为 2-3 个高优先级信号,优先展示“实测突破 / 峰值窗口已过 / METAR 过旧 / AI 解读中 / 市场价暂不可用 / 模型高度一致 / 需要等待下一报文”,让用户第一眼看到重点
|
||||
- 城市决策卡 AI 机场报文区明确拆分“快速判断已完成,AI 正在补充机场报文细节… / AI 机场报文解读已完成 / AI 解读未完整返回,当前使用规则证据”三种状态,减少 fallback 与流式返回造成的误解
|
||||
- 城市决策卡新增“数据新鲜度”区块,分别展示 METAR/官方观测、模型、市场价格和 AI 状态;过旧观测会标明“仅作背景参考”
|
||||
- 日历视图升级为行动视图,按“现在可看 / 1-3 小时内 / 今天稍后 / 已过峰值,等待确认”分组,并为每个城市显示一句核心原因
|
||||
|
||||
@@ -9384,6 +9384,68 @@
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.root :global(.scan-upgrade-announcement) {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
margin: -4px 0 6px;
|
||||
border: 1px solid rgba(77, 163, 255, 0.2);
|
||||
border-radius: 18px;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(77, 163, 255, 0.18), transparent 36%),
|
||||
linear-gradient(135deg, rgba(18, 33, 58, 0.96), rgba(15, 23, 42, 0.92));
|
||||
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.2);
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.root :global(.scan-upgrade-announcement-copy) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.root :global(.scan-upgrade-announcement-copy span) {
|
||||
color: #86efac;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.root :global(.scan-upgrade-announcement-copy strong) {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
color: #f3f8ff;
|
||||
font-size: 18px;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.root :global(.scan-upgrade-announcement-copy p) {
|
||||
margin: 6px 0 0;
|
||||
color: #b4c8e2;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.root :global(.scan-upgrade-announcement ul) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.root :global(.scan-upgrade-announcement li) {
|
||||
border: 1px solid rgba(77, 163, 255, 0.22);
|
||||
border-radius: 999px;
|
||||
background: rgba(77, 163, 255, 0.1);
|
||||
color: #d7e5f7;
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
padding: 7px 10px;
|
||||
}
|
||||
|
||||
.root :global(.scan-ghost-button),
|
||||
.root :global(.scan-cta-ghost),
|
||||
.root :global(.scan-theme-button),
|
||||
@@ -11314,6 +11376,10 @@
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-mobile-priority) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-pills) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -11598,6 +11664,17 @@
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-decision-why) {
|
||||
display: inline-flex;
|
||||
max-width: 100%;
|
||||
border: 1px solid rgba(77, 163, 255, 0.24);
|
||||
border-radius: 12px;
|
||||
background: rgba(77, 163, 255, 0.1);
|
||||
color: #d8e7f8 !important;
|
||||
font-weight: 900;
|
||||
padding: 9px 11px;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-decision-reasons) {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
@@ -11632,6 +11709,10 @@
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-market-mobile-line) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-market-decision) {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
@@ -11753,6 +11834,41 @@
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-ai-read) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-ai-read summary) {
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-ai-read summary::-webkit-details-marker) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-ai-read summary::after) {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
border-radius: 999px;
|
||||
background: rgba(77, 163, 255, 0.12);
|
||||
color: #9ecbff;
|
||||
content: "⌄";
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
transition: transform 0.18s ease;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-ai-read[open] summary::after) {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-section-body) {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-section p) {
|
||||
margin: 0 0 10px;
|
||||
color: #9fb2c7;
|
||||
@@ -14055,6 +14171,14 @@
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.root :global(.scan-upgrade-announcement) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.root :global(.scan-upgrade-announcement ul) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.root :global(.scan-table-header),
|
||||
.root :global(.scan-table-row) {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -14101,6 +14225,10 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-market-decision) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.root :global(.scan-opportunity-card-grid),
|
||||
.root :global(.scan-opportunity-summary) {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -14157,6 +14285,114 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.root :global(.scan-upgrade-announcement) {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.root :global(.scan-upgrade-announcement-copy strong) {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.root :global(.scan-upgrade-announcement ul) {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.root :global(.scan-upgrade-announcement li) {
|
||||
font-size: 11px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-hero) {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-hero h3) {
|
||||
margin-bottom: 10px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-mobile-priority) {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-mobile-priority span) {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
border: 1px solid rgba(77, 163, 255, 0.18);
|
||||
border-radius: 12px;
|
||||
background: rgba(77, 163, 255, 0.09);
|
||||
padding: 9px;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-mobile-priority small) {
|
||||
color: #9fb2c7;
|
||||
font-size: 10px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-mobile-priority b) {
|
||||
color: #f3f8ff;
|
||||
font-size: 13px;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-pills) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-freshness) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-decision-band) {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-decision-band strong) {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-decision-long),
|
||||
.root :global(.scan-ai-decision-reasons),
|
||||
.root :global(.scan-ai-decision-risk) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-market-mobile-line) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
border: 1px solid rgba(77, 163, 255, 0.18);
|
||||
border-radius: 12px;
|
||||
background: rgba(11, 18, 32, 0.48);
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-market-mobile-line span) {
|
||||
color: #9fb2c7;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-market-mobile-line b) {
|
||||
color: #e6edf3;
|
||||
font-size: 13px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-market-decision) {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.root :global(.scan-ai-city-ai-read:not([open])) {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.root :global(.scan-list-header) {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
@@ -14321,3 +14557,40 @@
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-status-tag.red) {
|
||||
color: #b91c1c !important;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-upgrade-announcement) {
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(37, 99, 235, 0.12), transparent 36%),
|
||||
#ffffff !important;
|
||||
border-color: rgba(37, 99, 235, 0.16) !important;
|
||||
box-shadow: 0 16px 36px rgba(61, 100, 145, 0.12) !important;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-upgrade-announcement-copy strong),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-mobile-priority b),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-market-mobile-line b) {
|
||||
color: #0f172a !important;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-upgrade-announcement-copy p),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-upgrade-announcement li),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-mobile-priority small),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-market-mobile-line span) {
|
||||
color: #334155 !important;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-upgrade-announcement-copy span) {
|
||||
color: #047857 !important;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-upgrade-announcement li),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-mobile-priority span),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-decision-why),
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-market-mobile-line) {
|
||||
background: #eef7ff !important;
|
||||
border-color: rgba(37, 99, 235, 0.16) !important;
|
||||
}
|
||||
|
||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-decision-why) {
|
||||
color: #1d4ed8 !important;
|
||||
}
|
||||
|
||||
@@ -701,6 +701,26 @@ function ScanTerminalScreen() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section className="scan-upgrade-announcement" aria-label={isEn ? "Upgrade announcement" : "升级公告"}>
|
||||
<div className="scan-upgrade-announcement-copy">
|
||||
<span>{isEn ? "v1.5.5 upgrade" : "v1.5.5 升级公告"}</span>
|
||||
<strong>
|
||||
{isEn ? "PolyWeather is now upgraded to v1.5.5" : "网站已升级到 v1.5.5"}
|
||||
</strong>
|
||||
<p>
|
||||
{isEn
|
||||
? "All members received an extra 7 days. The decision terminal now explains more evidence without making unavailable quotes look like a system failure."
|
||||
: "所有会员已额外延长 7 天。新版会把证据讲得更清楚,也不会让暂无报价看起来像系统故障。"}
|
||||
</p>
|
||||
</div>
|
||||
<ul>
|
||||
<li>{isEn ? "DeepSeek airport bulletin read" : "DeepSeek 机场报文解读"}</li>
|
||||
<li>{isEn ? "Action calendar view" : "日历行动视图"}</li>
|
||||
<li>{isEn ? "Local-time peak window" : "本地时间峰值窗口"}</li>
|
||||
<li>{isEn ? "AI evidence guardrails" : "AI 证据护栏"}</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<AiForecastKPIBar
|
||||
pinnedCount={aiPinnedCities.length}
|
||||
activeCityName={kpiCityName}
|
||||
|
||||
@@ -182,7 +182,7 @@ function buildMarketFreshnessValue({
|
||||
marketStatus: ReturnType<typeof useCityMarketScan>["marketStatus"];
|
||||
}) {
|
||||
if (marketStatus === "loading") return isEn ? "syncing" : "同步中";
|
||||
if (!marketScan?.available) return isEn ? "missing" : "缺失";
|
||||
if (!marketScan?.available) return isEn ? "temporarily unavailable" : "暂不可用";
|
||||
const quoteAgeMs = Number(
|
||||
marketScan.quote_age_ms ??
|
||||
marketScan.yes_token?.quote_age_ms ??
|
||||
@@ -307,6 +307,16 @@ function AiPinnedCityCard({
|
||||
enabled: Boolean(detail),
|
||||
});
|
||||
const isRefreshing = refreshingDetail || aiForecast.status === "loading";
|
||||
const [isCompactCard, setIsCompactCard] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined" || !window.matchMedia) return;
|
||||
const media = window.matchMedia("(max-width: 820px)");
|
||||
const syncCompactMode = () => setIsCompactCard(media.matches);
|
||||
syncCompactMode();
|
||||
media.addEventListener("change", syncCompactMode);
|
||||
return () => media.removeEventListener("change", syncCompactMode);
|
||||
}, []);
|
||||
|
||||
const aiCityForecast = aiForecast.payload?.city_forecast || null;
|
||||
const localizedFinalJudgmentRaw =
|
||||
@@ -376,6 +386,14 @@ function AiPinnedCityCard({
|
||||
peakWindow,
|
||||
tempSymbol,
|
||||
});
|
||||
const expectedHighText =
|
||||
decisionExpectedHighNumber != null
|
||||
? formatTemperatureValue(decisionExpectedHighNumber, tempSymbol, { digits: 1 })
|
||||
: "--";
|
||||
const currentTempText =
|
||||
currentTempNumber != null
|
||||
? formatTemperatureValue(currentTempNumber, tempSymbol, { digits: 1 })
|
||||
: "--";
|
||||
const marketDecisionView = buildMarketDecisionView({
|
||||
expectedHigh: decisionExpectedHighNumber,
|
||||
isEn,
|
||||
@@ -540,7 +558,7 @@ function AiPinnedCityCard({
|
||||
: null,
|
||||
marketDecisionView.status === "unavailable"
|
||||
? {
|
||||
label: isEn ? "Market missing" : "市场价格缺失",
|
||||
label: isEn ? "Market unavailable" : "市场价暂不可用",
|
||||
tone: marketStatusTone,
|
||||
}
|
||||
: null,
|
||||
@@ -574,6 +592,43 @@ function AiPinnedCityCard({
|
||||
(isEn ? aiForecast.payload?.reason_en : aiForecast.payload?.reason_zh) ||
|
||||
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 =
|
||||
marketDecisionView.status === "ready"
|
||||
? `${marketDecisionView.bucketLabel} · ${marketDecisionView.priceText}`
|
||||
: marketDecisionView.status === "loading"
|
||||
? isEn
|
||||
? "syncing"
|
||||
: "同步中"
|
||||
: isEn
|
||||
? "temporarily unavailable"
|
||||
: "暂不可用";
|
||||
|
||||
const collapseId = `ai-city-body-${normalizeCityKey(item.cityName) || item.addedAt}`;
|
||||
|
||||
@@ -585,6 +640,20 @@ function AiPinnedCityCard({
|
||||
{isEn ? "Deep analysis" : "城市深度分析"}
|
||||
</span>
|
||||
<h3>{displayName}</h3>
|
||||
<div className="scan-ai-city-mobile-priority" aria-label={isEn ? "Key city decision metrics" : "城市决策重点"}>
|
||||
<span>
|
||||
<small>{isEn ? "Observed" : "当前温度"}</small>
|
||||
<b>{currentTempText}</b>
|
||||
</span>
|
||||
<span>
|
||||
<small>{isEn ? "Expected high" : "预测高点"}</small>
|
||||
<b>{expectedHighText}</b>
|
||||
</span>
|
||||
<span>
|
||||
<small>{isEn ? "Peak" : "峰值时间"}</small>
|
||||
<b>{peakWindow}</b>
|
||||
</span>
|
||||
</div>
|
||||
<div className="scan-ai-city-status-tags">
|
||||
{statusTags.map((tag) => (
|
||||
<span
|
||||
@@ -623,9 +692,7 @@ function AiPinnedCityCard({
|
||||
<div className="scan-ai-city-hero-side">
|
||||
<span>{isEn ? "Expected high" : "预计最高温"}</span>
|
||||
<strong>
|
||||
{decisionExpectedHighNumber != null
|
||||
? formatTemperatureValue(decisionExpectedHighNumber, tempSymbol, { digits: 1 })
|
||||
: "--"}
|
||||
{expectedHighText}
|
||||
</strong>
|
||||
<div className="scan-ai-city-actions">
|
||||
<button
|
||||
@@ -687,13 +754,18 @@ function AiPinnedCityCard({
|
||||
<div className="scan-ai-decision-main">
|
||||
<span>{decisionView.kicker}</span>
|
||||
<strong>{decisionView.action}</strong>
|
||||
<p>{localizedFinalJudgment || paceText}</p>
|
||||
<p className="scan-ai-decision-why">{decisionWhyText}</p>
|
||||
<p className="scan-ai-decision-long">{localizedFinalJudgment || paceText}</p>
|
||||
<div className="scan-ai-decision-reasons">
|
||||
{decisionView.reasons.map((reason, index) => (
|
||||
<small key={`${reason}-${index}`}>{reason}</small>
|
||||
))}
|
||||
</div>
|
||||
<p className="scan-ai-decision-risk">{decisionView.risk}</p>
|
||||
<div className={clsx("scan-ai-market-mobile-line", marketDecisionView.tone)}>
|
||||
<span>{isEn ? "Market price" : "市场价格"}</span>
|
||||
<b>{marketLineText}</b>
|
||||
</div>
|
||||
<div className={clsx("scan-ai-market-decision", marketDecisionView.tone)}>
|
||||
<div>
|
||||
<span>
|
||||
@@ -740,11 +812,7 @@ function AiPinnedCityCard({
|
||||
</span>
|
||||
<span>
|
||||
{isEn ? "Observed" : "实测"}
|
||||
<b>
|
||||
{currentTempNumber != null
|
||||
? formatTemperatureValue(currentTempNumber, tempSymbol, { digits: 1 })
|
||||
: "--"}
|
||||
</b>
|
||||
<b>{currentTempText}</b>
|
||||
</span>
|
||||
<span>
|
||||
{isEn ? "Path delta" : "路径偏差"} <b>{paceView?.deltaText || "--"}</b>
|
||||
@@ -766,8 +834,8 @@ function AiPinnedCityCard({
|
||||
|
||||
<div className="scan-ai-city-analysis-grid">
|
||||
<AiCityTemperatureChart detail={detail} />
|
||||
<section className="scan-ai-city-section">
|
||||
<div className="scan-ai-city-section-title">
|
||||
<details className="scan-ai-city-section scan-ai-city-ai-read" open={!isCompactCard}>
|
||||
<summary className="scan-ai-city-section-title">
|
||||
{isHkoObservation
|
||||
? isEn
|
||||
? "Evidence · AI HKO observation read"
|
||||
@@ -775,78 +843,80 @@ function AiPinnedCityCard({
|
||||
: isEn
|
||||
? "Evidence · AI airport read"
|
||||
: "证据 · AI 机场报文解读"}
|
||||
</div>
|
||||
{aiForecast.status === "loading" ? (
|
||||
<>
|
||||
<p className="scan-ai-weather-summary">
|
||||
{aiReadInProgressText}
|
||||
</p>
|
||||
{localizedFinalJudgment || aiForecast.streamText ? (
|
||||
<p className="scan-ai-city-muted">
|
||||
{localizedFinalJudgment || aiForecast.streamText}
|
||||
</summary>
|
||||
<div className="scan-ai-city-section-body">
|
||||
{aiForecast.status === "loading" ? (
|
||||
<>
|
||||
<p className="scan-ai-weather-summary">
|
||||
{aiReadInProgressText}
|
||||
</p>
|
||||
) : null}
|
||||
<p className="scan-ai-city-muted">
|
||||
{localizedFinalJudgment || aiForecast.streamText ? (
|
||||
<p className="scan-ai-city-muted">
|
||||
{localizedFinalJudgment || aiForecast.streamText}
|
||||
</p>
|
||||
) : null}
|
||||
<p className="scan-ai-city-muted">
|
||||
{isEn
|
||||
? isHkoObservation
|
||||
? "Rule evidence is shown first; the full HKO AI read will merge automatically."
|
||||
: "Rule evidence is shown first; the full airport AI read will merge automatically."
|
||||
: isHkoObservation
|
||||
? "先展示规则证据,完整香港天文台 AI 解读返回后会自动合并。"
|
||||
: "先展示规则证据,完整机场 AI 解读返回后会自动合并。"}
|
||||
</p>
|
||||
</>
|
||||
) : aiForecast.status === "ready" && aiCityForecast ? (
|
||||
<>
|
||||
<p className="scan-ai-weather-summary">
|
||||
{aiRuleEvidenceMode ? aiRuleEvidenceText : aiReadCompleteText}
|
||||
</p>
|
||||
<ul className="scan-ai-weather-bullets">
|
||||
{[
|
||||
localizedFinalJudgment,
|
||||
...aiBullets,
|
||||
].filter((line) => String(line || "").trim()).map((line, index) => (
|
||||
<li key={`${line}-${index}`}>{line}</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="scan-ai-raw-metar">
|
||||
{rawObservationText}
|
||||
</p>
|
||||
</>
|
||||
) : aiForecast.status === "ready" ? (
|
||||
<>
|
||||
<p className="scan-ai-weather-summary">
|
||||
{aiRuleEvidenceText}
|
||||
</p>
|
||||
<ul className="scan-ai-weather-bullets">
|
||||
{fallbackAiReason ? <li>{fallbackAiReason}</li> : null}
|
||||
<li>{localModelSupportNote}</li>
|
||||
<li>{rawObservationText}</li>
|
||||
</ul>
|
||||
</>
|
||||
) : aiForecast.status === "failed" ? (
|
||||
<>
|
||||
<p className="scan-ai-weather-summary">
|
||||
{aiRuleEvidenceText}
|
||||
</p>
|
||||
<ul className="scan-ai-weather-bullets">
|
||||
{aiForecast.error ? <li>{aiForecast.error}</li> : null}
|
||||
<li>{localModelSupportNote}</li>
|
||||
<li>{rawObservationText}</li>
|
||||
</ul>
|
||||
</>
|
||||
) : (
|
||||
<p>
|
||||
{isEn
|
||||
? isHkoObservation
|
||||
? "Rule evidence is shown first; the full HKO AI read will merge automatically."
|
||||
: "Rule evidence is shown first; the full airport AI read will merge automatically."
|
||||
? "Waiting for AI to read the latest HKO observation."
|
||||
: "Waiting for AI to read the latest airport bulletin."
|
||||
: isHkoObservation
|
||||
? "先展示规则证据,完整香港天文台 AI 解读返回后会自动合并。"
|
||||
: "先展示规则证据,完整机场 AI 解读返回后会自动合并。"}
|
||||
? "等待 AI 解读最新香港天文台观测。"
|
||||
: "等待 AI 解读最新机场报文。"}
|
||||
</p>
|
||||
</>
|
||||
) : aiForecast.status === "ready" && aiCityForecast ? (
|
||||
<>
|
||||
<p className="scan-ai-weather-summary">
|
||||
{aiRuleEvidenceMode ? aiRuleEvidenceText : aiReadCompleteText}
|
||||
</p>
|
||||
<ul className="scan-ai-weather-bullets">
|
||||
{[
|
||||
localizedFinalJudgment,
|
||||
...aiBullets,
|
||||
].filter((line) => String(line || "").trim()).map((line, index) => (
|
||||
<li key={`${line}-${index}`}>{line}</li>
|
||||
))}
|
||||
</ul>
|
||||
<p className="scan-ai-raw-metar">
|
||||
{rawObservationText}
|
||||
</p>
|
||||
</>
|
||||
) : aiForecast.status === "ready" ? (
|
||||
<>
|
||||
<p className="scan-ai-weather-summary">
|
||||
{aiRuleEvidenceText}
|
||||
</p>
|
||||
<ul className="scan-ai-weather-bullets">
|
||||
{fallbackAiReason ? <li>{fallbackAiReason}</li> : null}
|
||||
<li>{localModelSupportNote}</li>
|
||||
<li>{rawObservationText}</li>
|
||||
</ul>
|
||||
</>
|
||||
) : aiForecast.status === "failed" ? (
|
||||
<>
|
||||
<p className="scan-ai-weather-summary">
|
||||
{aiRuleEvidenceText}
|
||||
</p>
|
||||
<ul className="scan-ai-weather-bullets">
|
||||
{aiForecast.error ? <li>{aiForecast.error}</li> : null}
|
||||
<li>{localModelSupportNote}</li>
|
||||
<li>{rawObservationText}</li>
|
||||
</ul>
|
||||
</>
|
||||
) : (
|
||||
<p>
|
||||
{isEn
|
||||
? isHkoObservation
|
||||
? "Waiting for AI to read the latest HKO observation."
|
||||
: "Waiting for AI to read the latest airport bulletin."
|
||||
: isHkoObservation
|
||||
? "等待 AI 解读最新香港天文台观测。"
|
||||
: "等待 AI 解读最新机场报文。"}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<section className="scan-ai-city-section models">
|
||||
|
||||
@@ -369,13 +369,11 @@ export function buildMarketDecisionView({
|
||||
impliedText: "--",
|
||||
modelText: "--",
|
||||
priceText: "--",
|
||||
reason:
|
||||
marketScan?.reason ||
|
||||
(isEn
|
||||
? "No active matched Polymarket temperature market is available for this city yet."
|
||||
: "该城市暂未匹配到可用的 Polymarket 温度市场。"),
|
||||
reason: isEn
|
||||
? "No tradable quote is available yet; weather evidence is still shown."
|
||||
: "暂无可交易价格,仅展示天气证据。天气判断仍可参考。",
|
||||
status: "unavailable",
|
||||
title: isEn ? "No market quote" : "暂无市场报价",
|
||||
title: isEn ? "Market price temporarily unavailable" : "市场价格暂不可用",
|
||||
tone: "watch",
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user