Files
PolyWeather/frontend/components/dashboard/scan-terminal/AiEvidencePanel.tsx
T
2569718930@qq.com 2e5bdfd1b6 Defer heavy city card rendering until users need it
City decision cards rendered Chart.js canvases and AI evidence bodies before the user could see or expand those sections. This keeps the card shell visible while delaying chart data/canvas work until the section nears the viewport and skipping the AI evidence body while its details panel is collapsed.

Constraint: Preserve existing decision-card layout and evidence copy.

Rejected: Add list virtualization in the same pass | card-level render costs should be reduced before changing list mechanics.

Confidence: high

Scope-risk: narrow

Reversibility: clean

Tested: npm run build

Not-tested: Runtime scroll benchmark on a large production city set.
2026-04-28 11:10:12 +08:00

129 lines
4.4 KiB
TypeScript

"use client";
import { useEffect, useState } from "react";
import type {
AiCityForecastPayload,
AiCityForecastState,
} from "@/components/dashboard/scan-terminal/types";
export function AiEvidencePanel({
aiBullets,
aiCityForecast,
aiForecast,
aiReadCompleteText,
aiReadInProgressText,
aiRuleEvidenceMode,
aiRuleEvidenceText,
fallbackAiReason,
isCompactCard,
isEn,
isHkoObservation,
localModelSupportNote,
localizedFinalJudgment,
rawObservationText,
}: {
aiBullets: string[];
aiCityForecast: AiCityForecastPayload["city_forecast"] | null;
aiForecast: AiCityForecastState;
aiReadCompleteText: string;
aiReadInProgressText: string;
aiRuleEvidenceMode: boolean;
aiRuleEvidenceText: string;
fallbackAiReason: string;
isCompactCard: boolean;
isEn: boolean;
isHkoObservation: boolean;
localModelSupportNote: string;
localizedFinalJudgment: string;
rawObservationText: string;
}) {
const [isOpen, setIsOpen] = useState(() => !isCompactCard);
useEffect(() => {
setIsOpen(!isCompactCard);
}, [isCompactCard]);
return (
<details
className="scan-ai-city-section scan-ai-city-ai-read"
open={isOpen}
onToggle={(event) => setIsOpen(event.currentTarget.open)}
>
<summary className="scan-ai-city-section-title">
{isHkoObservation
? isEn
? "Evidence · AI HKO observation read"
: "证据 · AI 香港天文台观测解读"
: isEn
? "Evidence · AI airport read"
: "证据 · AI 机场报文解读"}
</summary>
{isOpen ? (
<div className="scan-ai-city-section-body">
{aiForecast.status === "loading" ? (
<>
<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
? "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
? "Waiting for AI to read the latest HKO observation."
: "Waiting for AI to read the latest airport bulletin."
: isHkoObservation
? "等待 AI 解读最新香港天文台观测。"
: "等待 AI 解读最新机场报文。"}
</p>
)}
</div>
) : null}
</details>
);
}