diff --git a/frontend/components/dashboard/scan-terminal/AiPinnedCityCardSections.tsx b/frontend/components/dashboard/scan-terminal/AiPinnedCityCardSections.tsx new file mode 100644 index 00000000..f909d19c --- /dev/null +++ b/frontend/components/dashboard/scan-terminal/AiPinnedCityCardSections.tsx @@ -0,0 +1,380 @@ +"use client"; + +import clsx from "clsx"; +import { ChevronDown, RefreshCw, X } from "lucide-react"; +import type { MouseEvent } from "react"; +import type { + MarketDecisionView, + WeatherDecisionView, +} from "@/components/dashboard/scan-terminal/city-card-decision-utils"; +import type { + AiCityForecastPayload, + AiCityForecastState, +} from "@/components/dashboard/scan-terminal/types"; + +type StatusTone = "green" | "blue" | "amber" | "red" | "muted"; + +export type CityStatusTag = { + label: string; + tone: StatusTone; +}; + +export type DataFreshnessRow = { + label: string; + value: string; + tone: string; +}; + +export function CityCardHeader({ + aiStatusLabel, + aiStatusTone, + collapseId, + collapsed, + currentTempText, + dataFreshnessRows, + debText, + detailLocalTime, + displayName, + expectedHighText, + freshnessSeparator, + isEn, + isRefreshing, + modelRange, + onRefresh, + onRemove, + onToggleCollapsed, + peakWindow, + removing, + rowLocalTime, + statusTags, +}: { + aiStatusLabel: string; + aiStatusTone: StatusTone; + collapseId: string; + collapsed: boolean; + currentTempText: string; + dataFreshnessRows: DataFreshnessRow[]; + debText: string; + detailLocalTime?: string | null; + displayName: string; + expectedHighText: string; + freshnessSeparator: string; + isEn: boolean; + isRefreshing: boolean; + modelRange: string; + onRefresh: (event: MouseEvent) => void; + onRemove: (event: MouseEvent) => void; + onToggleCollapsed: () => void; + peakWindow: string; + removing?: boolean; + rowLocalTime?: string | null; + statusTags: CityStatusTag[]; +}) { + return ( +
+
+ + {isEn ? "Deep analysis" : "城市深度分析"} + +

{displayName}

+
+ + {isEn ? "Observed" : "当前温度"} + {currentTempText} + + + {isEn ? "Expected high" : "预测高点"} + {expectedHighText} + + + {isEn ? "Peak" : "峰值时间"} + {peakWindow} + +
+
+ {statusTags.map((tag) => ( + + {tag.label} + + ))} +
+
+ {detailLocalTime || rowLocalTime || "--"} + DEB {debText} + {isEn ? "Model" : "模型"} {modelRange} + {isEn ? "Peak" : "峰值"} {peakWindow} +
+
+ {isEn ? "Data freshness" : "数据新鲜度"} + {dataFreshnessRows.map((freshness) => ( + + {freshness.label}{freshnessSeparator} + {freshness.value} + + ))} + + AI{freshnessSeparator} + {aiStatusLabel} + +
+
+
+ {isEn ? "Expected high" : "预计最高温"} + {expectedHighText} +
+ + + +
+
+
+ ); +} + +export function WeatherDecisionBand({ + currentTempText, + decisionView, + decisionWhyText, + isEn, + longText, + marketDecisionView, + marketLineText, + paceDeltaText, + peakWindow, +}: { + currentTempText: string; + decisionView: WeatherDecisionView; + decisionWhyText: string; + isEn: boolean; + longText: string; + marketDecisionView: MarketDecisionView; + marketLineText: string; + paceDeltaText: string; + peakWindow: string; +}) { + return ( +
+
+ {decisionView.kicker} + {decisionView.action} +

{decisionWhyText}

+

{longText}

+
+ {decisionView.reasons.map((reason, index) => ( + {reason} + ))} +
+

{decisionView.risk}

+
+ {isEn ? "Market price" : "市场价格"} + {marketLineText} +
+
+
+ {isEn ? "Polymarket price layer" : "Polymarket 价格层"} + {marketDecisionView.title} +

{marketDecisionView.reason}

+
+
+ + {isEn ? "Bucket" : "温度桶"} {marketDecisionView.bucketLabel} + + + {isEn ? "YES buy" : "YES 买价"} {marketDecisionView.priceText} + + + {isEn ? "Model-market" : "模型-市场差"} {marketDecisionView.edgeText} + +
+ {marketDecisionView.marketUrl ? ( + + {isEn ? "Open market" : "打开市场"} + + ) : null} +
+
+
+ + {isEn ? "Expected high" : "预计高点"} + {decisionView.expectedHigh} + + + {isEn ? "Weather range" : "天气区间"} + {decisionView.targetRange} + + + {isEn ? "Confidence" : "信心"} + {decisionView.confidence} + + + {isEn ? "Observed" : "实测"} + {currentTempText} + + + {isEn ? "Path delta" : "路径偏差"} {paceDeltaText} + + + {isEn ? "Peak window" : "峰值窗口"} {peakWindow} + + + {isEn ? "Market implied" : "市场隐含"} {marketDecisionView.impliedText} + + + {isEn ? "Model prob" : "模型概率"} {marketDecisionView.modelText} + + + {isEn ? "Quote status" : "报价状态"}{" "} + {marketDecisionView.status === "ready" ? (isEn ? "Ready" : "已同步") : marketDecisionView.status === "loading" ? (isEn ? "Loading" : "同步中") : (isEn ? "Unavailable" : "不可用")} + +
+
+ ); +} + +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; +}) { + return ( +
+ + {isHkoObservation + ? isEn + ? "Evidence · AI HKO observation read" + : "证据 · AI 香港天文台观测解读" + : isEn + ? "Evidence · AI airport read" + : "证据 · AI 机场报文解读"} + +
+ {aiForecast.status === "loading" ? ( + <> +

{aiReadInProgressText}

+ {localizedFinalJudgment || aiForecast.streamText ? ( +

+ {localizedFinalJudgment || aiForecast.streamText} +

+ ) : null} +

+ {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 解读返回后会自动合并。"} +

+ + ) : aiForecast.status === "ready" && aiCityForecast ? ( + <> +

+ {aiRuleEvidenceMode ? aiRuleEvidenceText : aiReadCompleteText} +

+
    + {[localizedFinalJudgment, ...aiBullets] + .filter((line) => String(line || "").trim()) + .map((line, index) => ( +
  • {line}
  • + ))} +
+

{rawObservationText}

+ + ) : aiForecast.status === "ready" ? ( + <> +

{aiRuleEvidenceText}

+
    + {fallbackAiReason ?
  • {fallbackAiReason}
  • : null} +
  • {localModelSupportNote}
  • +
  • {rawObservationText}
  • +
+ + ) : aiForecast.status === "failed" ? ( + <> +

{aiRuleEvidenceText}

+
    + {aiForecast.error ?
  • {aiForecast.error}
  • : null} +
  • {localModelSupportNote}
  • +
  • {rawObservationText}
  • +
+ + ) : ( +

+ {isEn + ? isHkoObservation + ? "Waiting for AI to read the latest HKO observation." + : "Waiting for AI to read the latest airport bulletin." + : isHkoObservation + ? "等待 AI 解读最新香港天文台观测。" + : "等待 AI 解读最新机场报文。"} +

+ )} +
+
+ ); +} diff --git a/frontend/components/dashboard/scan-terminal/AiPinnedForecastView.tsx b/frontend/components/dashboard/scan-terminal/AiPinnedForecastView.tsx index 16c420a9..67b9e146 100644 --- a/frontend/components/dashboard/scan-terminal/AiPinnedForecastView.tsx +++ b/frontend/components/dashboard/scan-terminal/AiPinnedForecastView.tsx @@ -1,10 +1,14 @@ "use client"; import clsx from "clsx"; -import { ChevronDown, RefreshCw, X } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { ModelForecast } from "@/components/dashboard/PanelSections"; import { AiCityTemperatureChart } from "@/components/dashboard/scan-terminal/AiCityTemperatureChart"; +import { + AiEvidencePanel, + CityCardHeader, + WeatherDecisionBand, +} from "@/components/dashboard/scan-terminal/AiPinnedCityCardSections"; import { buildMarketDecisionView, buildWeatherDecisionView, @@ -394,6 +398,10 @@ function AiPinnedCityCard({ currentTempNumber != null ? formatTemperatureValue(currentTempNumber, tempSymbol, { digits: 1 }) : "--"; + const debText = + debNumber != null + ? formatTemperatureValue(debNumber, tempSymbol, { digits: 1 }) + : "--"; const marketDecisionView = buildMarketDecisionView({ expectedHigh: decisionExpectedHighNumber, isEn, @@ -634,289 +642,77 @@ function AiPinnedCityCard({ return (
-
-
- - {isEn ? "Deep analysis" : "城市深度分析"} - -

{displayName}

-
- - {isEn ? "Observed" : "当前温度"} - {currentTempText} - - - {isEn ? "Expected high" : "预测高点"} - {expectedHighText} - - - {isEn ? "Peak" : "峰值时间"} - {peakWindow} - -
-
- {statusTags.map((tag) => ( - - {tag.label} - - ))} -
-
- {detail?.local_time || row?.local_time || "--"} - - DEB{" "} - {debNumber != null - ? formatTemperatureValue(debNumber, tempSymbol, { digits: 1 }) - : "--"} - - {isEn ? "Model" : "模型"} {modelRange} - {isEn ? "Peak" : "峰值"} {peakWindow} -
-
- {isEn ? "Data freshness" : "数据新鲜度"} - {dataFreshnessRows.map((freshness) => ( - - {freshness.label}{freshnessSeparator} - {freshness.value} - - ))} - - AI{freshnessSeparator} - {aiStatusLabel} - -
-
-
- {isEn ? "Expected high" : "预计最高温"} - - {expectedHighText} - -
- - - -
-
-
+ { + event.preventDefault(); + event.stopPropagation(); + if (refreshingDetail) return; + setRefreshingDetail(true); + void onRefreshCityDetail(detailCityName) + .catch(() => {}) + .finally(() => { + refreshAiForecast(); + setRefreshingDetail(false); + }); + }} + onRemove={(event) => { + event.preventDefault(); + event.stopPropagation(); + onRemove(); + }} + onToggleCollapsed={onToggleCollapsed} + peakWindow={peakWindow} + removing={removing} + rowLocalTime={row?.local_time} + statusTags={statusTags} + /> {detail && !collapsed ? (
-
-
- {decisionView.kicker} - {decisionView.action} -

{decisionWhyText}

-

{localizedFinalJudgment || paceText}

-
- {decisionView.reasons.map((reason, index) => ( - {reason} - ))} -
-

{decisionView.risk}

-
- {isEn ? "Market price" : "市场价格"} - {marketLineText} -
-
-
- - {isEn ? "Polymarket price layer" : "Polymarket 价格层"} - - {marketDecisionView.title} -

{marketDecisionView.reason}

-
-
- - {isEn ? "Bucket" : "温度桶"} {marketDecisionView.bucketLabel} - - - {isEn ? "YES buy" : "YES 买价"} {marketDecisionView.priceText} - - - {isEn ? "Model-market" : "模型-市场差"} {marketDecisionView.edgeText} - -
- {marketDecisionView.marketUrl ? ( - - {isEn ? "Open market" : "打开市场"} - - ) : null} -
-
-
- - {isEn ? "Expected high" : "预计高点"} - {decisionView.expectedHigh} - - - {isEn ? "Weather range" : "天气区间"} - {decisionView.targetRange} - - - {isEn ? "Confidence" : "信心"} - {decisionView.confidence} - - - {isEn ? "Observed" : "实测"} - {currentTempText} - - - {isEn ? "Path delta" : "路径偏差"} {paceView?.deltaText || "--"} - - - {isEn ? "Peak window" : "峰值窗口"} {peakWindow} - - - {isEn ? "Market implied" : "市场隐含"} {marketDecisionView.impliedText} - - - {isEn ? "Model prob" : "模型概率"} {marketDecisionView.modelText} - - - {isEn ? "Quote status" : "报价状态"} {marketDecisionView.status === "ready" ? (isEn ? "Ready" : "已同步") : marketDecisionView.status === "loading" ? (isEn ? "Loading" : "同步中") : (isEn ? "Unavailable" : "不可用")} - -
-
+
-
- - {isHkoObservation - ? isEn - ? "Evidence · AI HKO observation read" - : "证据 · AI 香港天文台观测解读" - : isEn - ? "Evidence · AI airport read" - : "证据 · AI 机场报文解读"} - -
- {aiForecast.status === "loading" ? ( - <> -

- {aiReadInProgressText} -

- {localizedFinalJudgment || aiForecast.streamText ? ( -

- {localizedFinalJudgment || aiForecast.streamText} -

- ) : null} -

- {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 解读返回后会自动合并。"} -

- - ) : aiForecast.status === "ready" && aiCityForecast ? ( - <> -

- {aiRuleEvidenceMode ? aiRuleEvidenceText : aiReadCompleteText} -

-
    - {[ - localizedFinalJudgment, - ...aiBullets, - ].filter((line) => String(line || "").trim()).map((line, index) => ( -
  • {line}
  • - ))} -
-

- {rawObservationText} -

- - ) : aiForecast.status === "ready" ? ( - <> -

- {aiRuleEvidenceText} -

-
    - {fallbackAiReason ?
  • {fallbackAiReason}
  • : null} -
  • {localModelSupportNote}
  • -
  • {rawObservationText}
  • -
- - ) : aiForecast.status === "failed" ? ( - <> -

- {aiRuleEvidenceText} -

-
    - {aiForecast.error ?
  • {aiForecast.error}
  • : null} -
  • {localModelSupportNote}
  • -
  • {rawObservationText}
  • -
- - ) : ( -

- {isEn - ? isHkoObservation - ? "Waiting for AI to read the latest HKO observation." - : "Waiting for AI to read the latest airport bulletin." - : isHkoObservation - ? "等待 AI 解读最新香港天文台观测。" - : "等待 AI 解读最新机场报文。"} -

- )} -
-
+