feat: implement AI city forecast streaming service and UI components for scan terminal
This commit is contained in:
@@ -425,8 +425,8 @@ function AiPinnedCityCard({
|
||||
<p className={aiForecast.streamText ? "scan-ai-weather-summary" : undefined}>
|
||||
{aiForecast.streamText ||
|
||||
(isEn
|
||||
? "Deepseek V4 pro is reading the latest airport bulletin..."
|
||||
: "Deepseek V4 pro 正在解读最新机场报文...")}
|
||||
? "DeepSeek is reading the latest airport bulletin..."
|
||||
: "DeepSeek 正在解读最新机场报文...")}
|
||||
</p>
|
||||
{!aiForecast.streamText ? (
|
||||
<p className="scan-ai-city-muted">
|
||||
@@ -460,8 +460,8 @@ function AiPinnedCityCard({
|
||||
<p>
|
||||
{aiForecast.payload?.status === "timeout"
|
||||
? isEn
|
||||
? "Deepseek V4-Pro timed out. You can retry; city data and the right briefing were not refreshed."
|
||||
: "Deepseek V4-Pro 本次解读超时,可稍后重试;城市数据和右侧简报不会被刷新。"
|
||||
? "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."
|
||||
|
||||
@@ -9,6 +9,8 @@ export type AiCityForecastPayload = {
|
||||
reason_zh?: string | null;
|
||||
reason_en?: string | null;
|
||||
raw_reason?: string | null;
|
||||
degraded?: boolean | null;
|
||||
cached?: boolean | null;
|
||||
model?: string | null;
|
||||
provider?: string | null;
|
||||
city_forecast?: {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
fetchBackendApi,
|
||||
} from "@/lib/backend-api";
|
||||
import type { CityDetail, MarketScan } from "@/lib/dashboard-types";
|
||||
import { extractStreamingAirportRead } from "./ai-city-stream";
|
||||
import { normalizeCityKey } from "./decision-utils";
|
||||
|
||||
const AI_CITY_FORECAST_CACHE_PREFIX = "polyWeather_aiCityForecast_v2";
|
||||
@@ -106,6 +107,7 @@ function parseAiCityStreamBlock(block: string): AiCityStreamEvent | null {
|
||||
|
||||
async function readAiCityForecastStream(
|
||||
response: Response,
|
||||
locale: string,
|
||||
onProgress?: (progress: AiCityStreamProgress) => void,
|
||||
) {
|
||||
if (!response.body) {
|
||||
@@ -115,6 +117,7 @@ async function readAiCityForecastStream(
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
let accumulatedRaw = "";
|
||||
let finalPayload: AiCityForecastPayload | null = null;
|
||||
|
||||
const consumeBlock = (block: string) => {
|
||||
@@ -130,10 +133,26 @@ async function readAiCityForecastStream(
|
||||
return;
|
||||
}
|
||||
if (event === "delta") {
|
||||
const content = String(data.content || "");
|
||||
const rawLength = Number(data.raw_length);
|
||||
onProgress?.({
|
||||
if (content) {
|
||||
accumulatedRaw += content;
|
||||
}
|
||||
const streamingAirportRead = extractStreamingAirportRead(
|
||||
accumulatedRaw,
|
||||
locale,
|
||||
);
|
||||
const progress: AiCityStreamProgress = {
|
||||
raw_length: Number.isFinite(rawLength) ? rawLength : null,
|
||||
});
|
||||
};
|
||||
if (streamingAirportRead) {
|
||||
if (locale === "en-US") {
|
||||
progress.metar_read_en = streamingAirportRead;
|
||||
} else {
|
||||
progress.metar_read_zh = streamingAirportRead;
|
||||
}
|
||||
}
|
||||
onProgress?.(progress);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -214,7 +233,7 @@ function requestAiCityForecast({
|
||||
: `HTTP ${response.status}`,
|
||||
);
|
||||
}
|
||||
return readAiCityForecastStream(response, onProgress);
|
||||
return readAiCityForecastStream(response, locale, onProgress);
|
||||
})
|
||||
.finally(() => {
|
||||
pendingAiCityForecastRequests.delete(requestKey);
|
||||
@@ -235,8 +254,8 @@ function getAiCityStreamProgressText(progress: AiCityStreamProgress, isEn: boole
|
||||
const rawLength = Number(progress.raw_length);
|
||||
if (Number.isFinite(rawLength) && rawLength > 0) {
|
||||
return isEn
|
||||
? `DeepSeek V4-Pro is streaming the airport bulletin read... ${Math.round(rawLength)} chars received.`
|
||||
: `DeepSeek V4-Pro 正在流式解读机场报文... 已收到 ${Math.round(rawLength)} 字符。`;
|
||||
? `DeepSeek is streaming the airport-bulletin enhancement... ${Math.round(rawLength)} chars received.`
|
||||
: `DeepSeek 正在流式增强机场报文解读... 已收到 ${Math.round(rawLength)} 字符。`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -364,7 +383,7 @@ export function useAiCityForecast({
|
||||
: initialFallback.city_forecast?.metar_read_zh) ||
|
||||
(isEn
|
||||
? "Reading the latest airport bulletin with model/METAR fallback ready..."
|
||||
: "已先用最新 METAR 给出兜底解读,正在等待 DeepSeek V4-Pro 补充…"),
|
||||
: "已先用最新 METAR 给出兜底解读,正在等待 DeepSeek 补充…"),
|
||||
});
|
||||
void requestAiCityForecast({
|
||||
city: detailCityName,
|
||||
@@ -397,7 +416,9 @@ export function useAiCityForecast({
|
||||
isEn,
|
||||
report,
|
||||
});
|
||||
writeCachedPayload(cacheKey, usablePayload);
|
||||
if (usablePayload.status === "ready" && !usablePayload.degraded) {
|
||||
writeCachedPayload(cacheKey, usablePayload);
|
||||
}
|
||||
setAiForecast({ payload: usablePayload, status: "ready" });
|
||||
}
|
||||
})
|
||||
@@ -409,7 +430,6 @@ export function useAiCityForecast({
|
||||
isEn,
|
||||
report,
|
||||
});
|
||||
writeCachedPayload(cacheKey, fallbackPayload);
|
||||
setAiForecast({ payload: fallbackPayload, status: "ready" });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user