Clarify staged AI bulletin reads for city cards

Users saw fast-rule evidence, partial streams, DeepSeek completion, and fallback states as one blended AI status. The city evidence panel now names the current stage directly so a loading stream reads as fast judgment complete, a successful response reads as AI bulletin read complete, and incomplete responses explain that rule evidence is being used.

Constraint: Keep the existing fast evidence path visible while DeepSeek streams in.
Rejected: Label fallback as an AI failure | that incorrectly implies the card is broken even when rule evidence is valid.
Confidence: high
Scope-risk: narrow
Reversibility: clean
Tested: npm run build
Not-tested: Manual browser timing of partial stream transitions.
This commit is contained in:
2569718930@qq.com
2026-04-28 07:21:59 +08:00
parent 30b1f5e256
commit 1f364dfa4a
2 changed files with 56 additions and 45 deletions
+1
View File
@@ -4,6 +4,7 @@
- 新增 Qingdao / 青岛城市,结算锚点接入 Wunderground 青岛胶东国际机场 `ZSQD` 历史页,并补齐别名、时区、预热、官方来源和前端地区归类
- 城市决策卡顶部状态标签收口为 2-3 个高优先级信号,优先展示“实测突破 / 峰值窗口已过 / METAR 过旧 / AI 解读中 / 市场价格缺失 / 模型高度一致 / 需要等待下一报文”,让用户第一眼看到重点
- 城市决策卡 AI 机场报文区明确拆分“快速判断已完成,AI 正在补充机场报文细节… / AI 机场报文解读已完成 / AI 解读未完整返回,当前使用规则证据”三种状态,减少 fallback 与流式返回造成的误解
- 城市决策卡新增 AI 机场报文解读缓存说明:页面内存缓存保留 loading / 流式片段 / 最终结果,`localStorage` 保存最终成功 payload,后端 AI 缓存不再因 `local_time` 变化失效
- 城市决策卡兜底文案明确标记“快速证据模式”,避免在 DeepSeek 未完整返回时误写成“AI 机场报文解读正常”
- 城市决策卡流式 AI 解读改为只请求 METAR/官方观测核心解读与判断依据,最高温中枢、模型一致性和风险清单由后端规则补齐,减少等待时间
@@ -368,23 +368,46 @@ function AiPinnedCityCard({
isEn,
isHkoObservation,
});
const aiRuleEvidenceMode = Boolean(
aiForecast.status === "failed" ||
(aiForecast.status === "ready" && !aiCityForecast) ||
aiForecast.payload?.degraded ||
aiMeta?.fallback,
);
const aiReadInProgressText = isEn
? isHkoObservation
? "Fast read is ready; AI is adding HKO observation details..."
: "Fast read is ready; AI is adding airport bulletin details..."
: isHkoObservation
? "快速判断已完成,AI 正在补充香港天文台观测细节…"
: "快速判断已完成,AI 正在补充机场报文细节…";
const aiReadCompleteText = isEn
? isHkoObservation
? "AI HKO observation read is complete."
: "AI airport bulletin read is complete."
: isHkoObservation
? "AI 香港天文台观测解读已完成"
: "AI 机场报文解读已完成";
const aiRuleEvidenceText = isEn
? "AI read did not return completely; rule evidence is being used."
: "AI 解读未完整返回,当前使用规则证据";
const aiStatusLabel =
aiForecast.status === "loading"
? isEn
? "AI reading"
: "AI 解读中"
? "Fast read ready"
: "快速判断已完成"
: aiForecast.status === "ready" && aiCityForecast
? aiForecast.payload?.degraded || aiMeta?.fallback
? aiRuleEvidenceMode
? isEn
? "Rule fallback"
: "规则兜底"
? "Rule evidence"
: "规则证据模式"
: isEn
? "AI ready"
: "AI 已完成"
: aiForecast.status === "failed"
? "AI read complete"
: "AI 解读已完成"
: aiRuleEvidenceMode
? isEn
? "AI failed"
: "AI 失败"
? "Rule evidence"
: "规则证据模式"
: isEn
? "AI pending"
: "AI 待返回";
@@ -392,11 +415,11 @@ function AiPinnedCityCard({
aiForecast.status === "loading"
? "blue"
: aiForecast.status === "ready" && aiCityForecast
? aiForecast.payload?.degraded || aiMeta?.fallback
? aiRuleEvidenceMode
? "amber"
: "green"
: aiForecast.status === "failed"
? "red"
: aiRuleEvidenceMode
? "amber"
: "muted";
const marketFreshnessLabel =
marketDecisionView.status === "ready"
@@ -449,7 +472,7 @@ function AiPinnedCityCard({
: null,
aiForecast.status === "loading"
? {
label: isEn ? "AI reading" : "AI 解读中",
label: isEn ? "Fast read ready" : "快速判断已完成",
tone: aiStatusTone,
}
: null,
@@ -685,17 +708,14 @@ function AiPinnedCityCard({
</div>
{aiForecast.status === "loading" ? (
<>
<p className={aiForecast.streamText ? "scan-ai-weather-summary" : undefined}>
{localizedFinalJudgment ||
aiForecast.streamText ||
(isEn
? isHkoObservation
? "Fast read is ready; AI is adding HKO observation details..."
: "Fast read is ready; AI is adding airport bulletin details..."
: isHkoObservation
? "快速判断已完成,AI 正在补充香港天文台观测细节…"
: "快速判断已完成,AI 正在补充机场报文细节…")}
<p className="scan-ai-weather-summary">
{aiReadInProgressText}
</p>
{localizedFinalJudgment || aiForecast.streamText ? (
<p className="scan-ai-city-muted">
{localizedFinalJudgment || aiForecast.streamText}
</p>
) : null}
<p className="scan-ai-city-muted">
{isEn
? isHkoObservation
@@ -709,11 +729,13 @@ function AiPinnedCityCard({
) : aiForecast.status === "ready" && aiCityForecast ? (
<>
<p className="scan-ai-weather-summary">
{localizedFinalJudgment ||
(isEn ? "AI read returned without a final sentence." : "AI 已返回,但缺少最终判断。")}
{aiRuleEvidenceMode ? aiRuleEvidenceText : aiReadCompleteText}
</p>
<ul className="scan-ai-weather-bullets">
{aiBullets.map((line, index) => (
{[
localizedFinalJudgment,
...aiBullets,
].filter((line) => String(line || "").trim()).map((line, index) => (
<li key={`${line}-${index}`}>{line}</li>
))}
</ul>
@@ -723,34 +745,22 @@ function AiPinnedCityCard({
</>
) : aiForecast.status === "ready" ? (
<>
<p>
{aiForecast.payload?.status === "timeout"
? isEn
? "DeepSeek enhancement timed out. You can retry; city data and the right briefing were not refreshed."
: "DeepSeek 增强本次超时,可稍后重试;城市数据和右侧简报不会被刷新。"
: fallbackAiReason ||
(isEn
? "AI read is unavailable for this city right now."
: "该城市暂时没有可用的 AI 解读。")}
<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>
{isEn
? isHkoObservation
? "AI read failed. Model support and the HKO observation remain as fallback context."
: "AI read failed. Model support and the raw METAR remain as fallback context."
: isHkoObservation
? "AI 解读失败。下方保留多模型支撑和香港天文台观测作为兜底上下文。"
: "AI 解读失败。下方保留多模型支撑和原始 METAR 作为兜底上下文。"}
{aiForecast.error ? ` ${aiForecast.error}` : ""}
<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>