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
|
## 1.5.5 - 2026-04-27
|
||||||
|
|
||||||
|
- Dashboard 新增 v1.5.5 升级公告,提示所有会员已额外延长 7 天,并集中说明 DeepSeek 机场报文解读、日历行动视图、本地时间峰值窗口和 AI 证据护栏
|
||||||
|
- 城市决策卡空状态与市场不可用文案产品化:将“未接入/缺失”改为“市场价格暂不可用,天气判断仍可参考”,避免用户误以为系统故障
|
||||||
|
- 城市决策卡新增“为什么推荐/为什么不推荐”短句,优先解释实测突破、峰值窗口已过、METAR 过旧、市场暂不可用或模型一致等关键原因
|
||||||
|
- 移动端城市决策卡前置当前温度、预测高点和峰值时间;长 AI 解读在手机端默认折叠,可展开查看,市场价格单独成行展示
|
||||||
- 新增 Qingdao / 青岛城市,结算锚点接入 Wunderground 青岛胶东国际机场 `ZSQD` 历史页,并补齐别名、时区、预热、官方来源和前端地区归类
|
- 新增 Qingdao / 青岛城市,结算锚点接入 Wunderground 青岛胶东国际机场 `ZSQD` 历史页,并补齐别名、时区、预热、官方来源和前端地区归类
|
||||||
- 城市决策卡顶部状态标签收口为 2-3 个高优先级信号,优先展示“实测突破 / 峰值窗口已过 / METAR 过旧 / AI 解读中 / 市场价格缺失 / 模型高度一致 / 需要等待下一报文”,让用户第一眼看到重点
|
- 城市决策卡顶部状态标签收口为 2-3 个高优先级信号,优先展示“实测突破 / 峰值窗口已过 / METAR 过旧 / AI 解读中 / 市场价暂不可用 / 模型高度一致 / 需要等待下一报文”,让用户第一眼看到重点
|
||||||
- 城市决策卡 AI 机场报文区明确拆分“快速判断已完成,AI 正在补充机场报文细节… / AI 机场报文解读已完成 / AI 解读未完整返回,当前使用规则证据”三种状态,减少 fallback 与流式返回造成的误解
|
- 城市决策卡 AI 机场报文区明确拆分“快速判断已完成,AI 正在补充机场报文细节… / AI 机场报文解读已完成 / AI 解读未完整返回,当前使用规则证据”三种状态,减少 fallback 与流式返回造成的误解
|
||||||
- 城市决策卡新增“数据新鲜度”区块,分别展示 METAR/官方观测、模型、市场价格和 AI 状态;过旧观测会标明“仅作背景参考”
|
- 城市决策卡新增“数据新鲜度”区块,分别展示 METAR/官方观测、模型、市场价格和 AI 状态;过旧观测会标明“仅作背景参考”
|
||||||
- 日历视图升级为行动视图,按“现在可看 / 1-3 小时内 / 今天稍后 / 已过峰值,等待确认”分组,并为每个城市显示一句核心原因
|
- 日历视图升级为行动视图,按“现在可看 / 1-3 小时内 / 今天稍后 / 已过峰值,等待确认”分组,并为每个城市显示一句核心原因
|
||||||
|
|||||||
@@ -9384,6 +9384,68 @@
|
|||||||
font-variant-numeric: tabular-nums;
|
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-ghost-button),
|
||||||
.root :global(.scan-cta-ghost),
|
.root :global(.scan-cta-ghost),
|
||||||
.root :global(.scan-theme-button),
|
.root :global(.scan-theme-button),
|
||||||
@@ -11314,6 +11376,10 @@
|
|||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-ai-city-mobile-priority) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.root :global(.scan-ai-city-pills) {
|
.root :global(.scan-ai-city-pills) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -11598,6 +11664,17 @@
|
|||||||
line-height: 1.55;
|
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) {
|
.root :global(.scan-ai-decision-reasons) {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -11632,6 +11709,10 @@
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-ai-market-mobile-line) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.root :global(.scan-ai-market-decision) {
|
.root :global(.scan-ai-market-decision) {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||||
@@ -11753,6 +11834,41 @@
|
|||||||
margin-bottom: 12px;
|
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) {
|
.root :global(.scan-ai-city-section p) {
|
||||||
margin: 0 0 10px;
|
margin: 0 0 10px;
|
||||||
color: #9fb2c7;
|
color: #9fb2c7;
|
||||||
@@ -14055,6 +14171,14 @@
|
|||||||
align-items: flex-start;
|
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-header),
|
||||||
.root :global(.scan-table-row) {
|
.root :global(.scan-table-row) {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -14101,6 +14225,10 @@
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.root :global(.scan-ai-market-decision) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.root :global(.scan-opportunity-card-grid),
|
.root :global(.scan-opportunity-card-grid),
|
||||||
.root :global(.scan-opportunity-summary) {
|
.root :global(.scan-opportunity-summary) {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
@@ -14157,6 +14285,114 @@
|
|||||||
flex-wrap: wrap;
|
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) {
|
.root :global(.scan-list-header) {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@@ -14321,3 +14557,40 @@
|
|||||||
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-status-tag.red) {
|
.root:global(.light) :global(.scan-terminal.light .scan-ai-city-status-tag.red) {
|
||||||
color: #b91c1c !important;
|
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>
|
||||||
</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
|
<AiForecastKPIBar
|
||||||
pinnedCount={aiPinnedCities.length}
|
pinnedCount={aiPinnedCities.length}
|
||||||
activeCityName={kpiCityName}
|
activeCityName={kpiCityName}
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ function buildMarketFreshnessValue({
|
|||||||
marketStatus: ReturnType<typeof useCityMarketScan>["marketStatus"];
|
marketStatus: ReturnType<typeof useCityMarketScan>["marketStatus"];
|
||||||
}) {
|
}) {
|
||||||
if (marketStatus === "loading") return isEn ? "syncing" : "同步中";
|
if (marketStatus === "loading") return isEn ? "syncing" : "同步中";
|
||||||
if (!marketScan?.available) return isEn ? "missing" : "缺失";
|
if (!marketScan?.available) return isEn ? "temporarily unavailable" : "暂不可用";
|
||||||
const quoteAgeMs = Number(
|
const quoteAgeMs = Number(
|
||||||
marketScan.quote_age_ms ??
|
marketScan.quote_age_ms ??
|
||||||
marketScan.yes_token?.quote_age_ms ??
|
marketScan.yes_token?.quote_age_ms ??
|
||||||
@@ -307,6 +307,16 @@ function AiPinnedCityCard({
|
|||||||
enabled: Boolean(detail),
|
enabled: Boolean(detail),
|
||||||
});
|
});
|
||||||
const isRefreshing = refreshingDetail || aiForecast.status === "loading";
|
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 aiCityForecast = aiForecast.payload?.city_forecast || null;
|
||||||
const localizedFinalJudgmentRaw =
|
const localizedFinalJudgmentRaw =
|
||||||
@@ -376,6 +386,14 @@ function AiPinnedCityCard({
|
|||||||
peakWindow,
|
peakWindow,
|
||||||
tempSymbol,
|
tempSymbol,
|
||||||
});
|
});
|
||||||
|
const expectedHighText =
|
||||||
|
decisionExpectedHighNumber != null
|
||||||
|
? formatTemperatureValue(decisionExpectedHighNumber, tempSymbol, { digits: 1 })
|
||||||
|
: "--";
|
||||||
|
const currentTempText =
|
||||||
|
currentTempNumber != null
|
||||||
|
? formatTemperatureValue(currentTempNumber, tempSymbol, { digits: 1 })
|
||||||
|
: "--";
|
||||||
const marketDecisionView = buildMarketDecisionView({
|
const marketDecisionView = buildMarketDecisionView({
|
||||||
expectedHigh: decisionExpectedHighNumber,
|
expectedHigh: decisionExpectedHighNumber,
|
||||||
isEn,
|
isEn,
|
||||||
@@ -540,7 +558,7 @@ function AiPinnedCityCard({
|
|||||||
: null,
|
: null,
|
||||||
marketDecisionView.status === "unavailable"
|
marketDecisionView.status === "unavailable"
|
||||||
? {
|
? {
|
||||||
label: isEn ? "Market missing" : "市场价格缺失",
|
label: isEn ? "Market unavailable" : "市场价暂不可用",
|
||||||
tone: marketStatusTone,
|
tone: marketStatusTone,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
@@ -574,6 +592,43 @@ 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 =
|
||||||
|
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}`;
|
const collapseId = `ai-city-body-${normalizeCityKey(item.cityName) || item.addedAt}`;
|
||||||
|
|
||||||
@@ -585,6 +640,20 @@ function AiPinnedCityCard({
|
|||||||
{isEn ? "Deep analysis" : "城市深度分析"}
|
{isEn ? "Deep analysis" : "城市深度分析"}
|
||||||
</span>
|
</span>
|
||||||
<h3>{displayName}</h3>
|
<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">
|
<div className="scan-ai-city-status-tags">
|
||||||
{statusTags.map((tag) => (
|
{statusTags.map((tag) => (
|
||||||
<span
|
<span
|
||||||
@@ -623,9 +692,7 @@ function AiPinnedCityCard({
|
|||||||
<div className="scan-ai-city-hero-side">
|
<div className="scan-ai-city-hero-side">
|
||||||
<span>{isEn ? "Expected high" : "预计最高温"}</span>
|
<span>{isEn ? "Expected high" : "预计最高温"}</span>
|
||||||
<strong>
|
<strong>
|
||||||
{decisionExpectedHighNumber != null
|
{expectedHighText}
|
||||||
? formatTemperatureValue(decisionExpectedHighNumber, tempSymbol, { digits: 1 })
|
|
||||||
: "--"}
|
|
||||||
</strong>
|
</strong>
|
||||||
<div className="scan-ai-city-actions">
|
<div className="scan-ai-city-actions">
|
||||||
<button
|
<button
|
||||||
@@ -687,13 +754,18 @@ function AiPinnedCityCard({
|
|||||||
<div className="scan-ai-decision-main">
|
<div className="scan-ai-decision-main">
|
||||||
<span>{decisionView.kicker}</span>
|
<span>{decisionView.kicker}</span>
|
||||||
<strong>{decisionView.action}</strong>
|
<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">
|
<div className="scan-ai-decision-reasons">
|
||||||
{decisionView.reasons.map((reason, index) => (
|
{decisionView.reasons.map((reason, index) => (
|
||||||
<small key={`${reason}-${index}`}>{reason}</small>
|
<small key={`${reason}-${index}`}>{reason}</small>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="scan-ai-decision-risk">{decisionView.risk}</p>
|
<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 className={clsx("scan-ai-market-decision", marketDecisionView.tone)}>
|
||||||
<div>
|
<div>
|
||||||
<span>
|
<span>
|
||||||
@@ -740,11 +812,7 @@ function AiPinnedCityCard({
|
|||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{isEn ? "Observed" : "实测"}
|
{isEn ? "Observed" : "实测"}
|
||||||
<b>
|
<b>{currentTempText}</b>
|
||||||
{currentTempNumber != null
|
|
||||||
? formatTemperatureValue(currentTempNumber, tempSymbol, { digits: 1 })
|
|
||||||
: "--"}
|
|
||||||
</b>
|
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{isEn ? "Path delta" : "路径偏差"} <b>{paceView?.deltaText || "--"}</b>
|
{isEn ? "Path delta" : "路径偏差"} <b>{paceView?.deltaText || "--"}</b>
|
||||||
@@ -766,8 +834,8 @@ function AiPinnedCityCard({
|
|||||||
|
|
||||||
<div className="scan-ai-city-analysis-grid">
|
<div className="scan-ai-city-analysis-grid">
|
||||||
<AiCityTemperatureChart detail={detail} />
|
<AiCityTemperatureChart detail={detail} />
|
||||||
<section className="scan-ai-city-section">
|
<details className="scan-ai-city-section scan-ai-city-ai-read" open={!isCompactCard}>
|
||||||
<div className="scan-ai-city-section-title">
|
<summary className="scan-ai-city-section-title">
|
||||||
{isHkoObservation
|
{isHkoObservation
|
||||||
? isEn
|
? isEn
|
||||||
? "Evidence · AI HKO observation read"
|
? "Evidence · AI HKO observation read"
|
||||||
@@ -775,78 +843,80 @@ function AiPinnedCityCard({
|
|||||||
: isEn
|
: isEn
|
||||||
? "Evidence · AI airport read"
|
? "Evidence · AI airport read"
|
||||||
: "证据 · AI 机场报文解读"}
|
: "证据 · AI 机场报文解读"}
|
||||||
</div>
|
</summary>
|
||||||
{aiForecast.status === "loading" ? (
|
<div className="scan-ai-city-section-body">
|
||||||
<>
|
{aiForecast.status === "loading" ? (
|
||||||
<p className="scan-ai-weather-summary">
|
<>
|
||||||
{aiReadInProgressText}
|
<p className="scan-ai-weather-summary">
|
||||||
</p>
|
{aiReadInProgressText}
|
||||||
{localizedFinalJudgment || aiForecast.streamText ? (
|
|
||||||
<p className="scan-ai-city-muted">
|
|
||||||
{localizedFinalJudgment || aiForecast.streamText}
|
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
{localizedFinalJudgment || aiForecast.streamText ? (
|
||||||
<p className="scan-ai-city-muted">
|
<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
|
{isEn
|
||||||
? isHkoObservation
|
? isHkoObservation
|
||||||
? "Rule evidence is shown first; the full HKO AI read will merge automatically."
|
? "Waiting for AI to read the latest HKO observation."
|
||||||
: "Rule evidence is shown first; the full airport AI read will merge automatically."
|
: "Waiting for AI to read the latest airport bulletin."
|
||||||
: isHkoObservation
|
: isHkoObservation
|
||||||
? "先展示规则证据,完整香港天文台 AI 解读返回后会自动合并。"
|
? "等待 AI 解读最新香港天文台观测。"
|
||||||
: "先展示规则证据,完整机场 AI 解读返回后会自动合并。"}
|
: "等待 AI 解读最新机场报文。"}
|
||||||
</p>
|
</p>
|
||||||
</>
|
)}
|
||||||
) : aiForecast.status === "ready" && aiCityForecast ? (
|
</div>
|
||||||
<>
|
</details>
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<section className="scan-ai-city-section models">
|
<section className="scan-ai-city-section models">
|
||||||
|
|||||||
@@ -369,13 +369,11 @@ export function buildMarketDecisionView({
|
|||||||
impliedText: "--",
|
impliedText: "--",
|
||||||
modelText: "--",
|
modelText: "--",
|
||||||
priceText: "--",
|
priceText: "--",
|
||||||
reason:
|
reason: isEn
|
||||||
marketScan?.reason ||
|
? "No tradable quote is available yet; weather evidence is still shown."
|
||||||
(isEn
|
: "暂无可交易价格,仅展示天气证据。天气判断仍可参考。",
|
||||||
? "No active matched Polymarket temperature market is available for this city yet."
|
|
||||||
: "该城市暂未匹配到可用的 Polymarket 温度市场。"),
|
|
||||||
status: "unavailable",
|
status: "unavailable",
|
||||||
title: isEn ? "No market quote" : "暂无市场报价",
|
title: isEn ? "Market price temporarily unavailable" : "市场价格暂不可用",
|
||||||
tone: "watch",
|
tone: "watch",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user