Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 38 additions & 24 deletions src/app/country-intel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { MILITARY_BASES } from '@/config';
import { mlWorker } from '@/services/ml-worker';
import { isHeadlineMemoryEnabled } from '@/services/ai-flow-settings';
import { t, getCurrentLanguage } from '@/services/i18n';
import { translateContentText } from '@/services/content-translation';
import { trackCountrySelected, trackCountryBriefOpened } from '@/services/analytics';
import { toApiUrl } from '@/services/runtime';
import type { StrategicPosturePanel } from '@/components/StrategicPosturePanel';
Expand Down Expand Up @@ -350,51 +351,64 @@ export class CountryIntelManager implements AppModule {
this.ctx.countryBriefPage?.updateBrief({ brief: briefText, country, code });
} else {
let fallbackBrief = '';
const currentLang = getCurrentLanguage();
const translateEnglishLine = async (line: string): Promise<string> => {
const translated = await translateContentText(line, currentLang, { sourceLang: 'en' });
return translated || line;
};
const sumModelId = BETA_MODE ? 'summarization-beta' : 'summarization';
if (briefHeadlines.length >= 2 && mlWorker.isAvailable && mlWorker.isModelLoaded(sumModelId)) {
try {
const lang = getCurrentLanguage();
const prompt = lang === 'fr'
const promptLang = currentLang === 'fr' ? 'fr' : 'en';
const prompt = promptLang === 'fr'
? `Résumez la situation actuelle en ${country} à partir de ces titres : ${briefHeadlines.slice(0, 8).join('. ')}`
: `Summarize the current situation in ${country} based on these headlines: ${briefHeadlines.slice(0, 8).join('. ')}`;

const [summary] = await mlWorker.summarize([prompt], BETA_MODE ? 'summarization-beta' : undefined);
if (summary && summary.length > 20) fallbackBrief = summary;
if (summary && summary.length > 20) {
fallbackBrief = currentLang !== promptLang
? await translateEnglishLine(summary)
: summary;
}
} catch { /* T5 failed */ }
}

if (fallbackBrief) {
this.ctx.countryBriefPage?.updateBrief({ brief: fallbackBrief, country, code, fallback: true });
} else {
const lines: string[] = [];
if (score) lines.push(t('countryBrief.fallback.instabilityIndex', { score: String(score.score), level: t(`countryBrief.levels.${score.level}`), trend: t(`countryBrief.trends.${score.trend}`) }));
if (signals.protests > 0) lines.push(t('countryBrief.fallback.protestsDetected', { count: String(signals.protests) }));
if (signals.militaryFlights > 0) lines.push(t('countryBrief.fallback.aircraftTracked', { count: String(signals.militaryFlights) }));
if (signals.militaryVessels > 0) lines.push(t('countryBrief.fallback.vesselsTracked', { count: String(signals.militaryVessels) }));
if (signals.activeStrikes > 0) lines.push(t('countryBrief.fallback.activeStrikes', { count: String(signals.activeStrikes) }));
if (signals.travelAdvisoryMaxLevel === 'do-not-travel') lines.push(`⚠️ Travel advisory: Do Not Travel (${signals.travelAdvisories} source${signals.travelAdvisories > 1 ? 's' : ''})`);
else if (signals.travelAdvisoryMaxLevel === 'reconsider') lines.push(`⚠️ Travel advisory: Reconsider Travel (${signals.travelAdvisories} source${signals.travelAdvisories > 1 ? 's' : ''})`);
if (signals.outages > 0) lines.push(t('countryBrief.fallback.internetOutages', { count: String(signals.outages) }));
if (signals.criticalNews > 0) lines.push(`🚨 Critical headlines in scope: ${signals.criticalNews}`);
if (signals.cyberThreats > 0) lines.push(`🛡️ Cyber threat indicators: ${signals.cyberThreats}`);
if (signals.aisDisruptions > 0) lines.push(`🚢 Maritime AIS disruptions: ${signals.aisDisruptions}`);
if (signals.satelliteFires > 0) lines.push(`🔥 Satellite fire detections: ${signals.satelliteFires}`);
if (signals.radiationAnomalies > 0) lines.push(`☢️ Radiation anomalies: ${signals.radiationAnomalies}`);
if (signals.temporalAnomalies > 0) lines.push(`⏱️ Temporal anomaly alerts: ${signals.temporalAnomalies}`);
if (signals.thermalEscalations > 0) lines.push(`🌡️ Thermal escalation clusters: ${signals.thermalEscalations}`);
if (signals.earthquakes > 0) lines.push(t('countryBrief.fallback.recentEarthquakes', { count: String(signals.earthquakes) }));
if (signals.orefHistory24h > 0) lines.push(`🚨 Sirens in past 24h: ${signals.orefHistory24h}`);
if (context.stockIndex) lines.push(t('countryBrief.fallback.stockIndex', { value: context.stockIndex }));
const linePromises: Promise<string>[] = [];
if (score) linePromises.push(Promise.resolve(t('countryBrief.fallback.instabilityIndex', { score: String(score.score), level: t(`countryBrief.levels.${score.level}`), trend: t(`countryBrief.trends.${score.trend}`) })));
if (signals.protests > 0) linePromises.push(Promise.resolve(t('countryBrief.fallback.protestsDetected', { count: String(signals.protests) })));
if (signals.militaryFlights > 0) linePromises.push(Promise.resolve(t('countryBrief.fallback.aircraftTracked', { count: String(signals.militaryFlights) })));
if (signals.militaryVessels > 0) linePromises.push(Promise.resolve(t('countryBrief.fallback.vesselsTracked', { count: String(signals.militaryVessels) })));
if (signals.activeStrikes > 0) linePromises.push(Promise.resolve(t('countryBrief.fallback.activeStrikes', { count: String(signals.activeStrikes) })));
if (signals.travelAdvisoryMaxLevel === 'do-not-travel') {
linePromises.push(translateEnglishLine(`⚠️ Travel advisory: Do Not Travel (${signals.travelAdvisories} source${signals.travelAdvisories > 1 ? 's' : ''})`));
} else if (signals.travelAdvisoryMaxLevel === 'reconsider') {
linePromises.push(translateEnglishLine(`⚠️ Travel advisory: Reconsider Travel (${signals.travelAdvisories} source${signals.travelAdvisories > 1 ? 's' : ''})`));
}
if (signals.outages > 0) linePromises.push(Promise.resolve(t('countryBrief.fallback.internetOutages', { count: String(signals.outages) })));
if (signals.criticalNews > 0) linePromises.push(translateEnglishLine(`🚨 Critical headlines in scope: ${signals.criticalNews}`));
if (signals.cyberThreats > 0) linePromises.push(translateEnglishLine(`🛡️ Cyber threat indicators: ${signals.cyberThreats}`));
if (signals.aisDisruptions > 0) linePromises.push(translateEnglishLine(`🚢 Maritime AIS disruptions: ${signals.aisDisruptions}`));
if (signals.satelliteFires > 0) linePromises.push(translateEnglishLine(`🔥 Satellite fire detections: ${signals.satelliteFires}`));
if (signals.radiationAnomalies > 0) linePromises.push(translateEnglishLine(`☢️ Radiation anomalies: ${signals.radiationAnomalies}`));
if (signals.temporalAnomalies > 0) linePromises.push(translateEnglishLine(`⏱️ Temporal anomaly alerts: ${signals.temporalAnomalies}`));
if (signals.thermalEscalations > 0) linePromises.push(translateEnglishLine(`🌡️ Thermal escalation clusters: ${signals.thermalEscalations}`));
if (signals.earthquakes > 0) linePromises.push(Promise.resolve(t('countryBrief.fallback.recentEarthquakes', { count: String(signals.earthquakes) })));
if (signals.orefHistory24h > 0) linePromises.push(translateEnglishLine(`🚨 Sirens in past 24h: ${signals.orefHistory24h}`));
if (context.stockIndex) linePromises.push(Promise.resolve(t('countryBrief.fallback.stockIndex', { value: context.stockIndex })));
const lines = (await Promise.all(linePromises)).filter((line) => line.length > 0);
if (lines.length > 0) {
this.ctx.countryBriefPage?.updateBrief({ brief: lines.join('\n'), country, code, fallback: true });
} else {
this.ctx.countryBriefPage?.updateBrief({ brief: '', country, code, error: 'No AI service available. Configure GROQ_API_KEY in Settings for full briefs.' });
this.ctx.countryBriefPage?.updateBrief({ brief: '', country, code, error: t('countryBrief.briefUnavailable') });
}
}
}
} catch (err) {
console.error('[CountryBrief] fetch error:', err);
this.ctx.countryBriefPage?.updateBrief({ brief: '', country, code, error: 'Failed to generate brief' });
this.ctx.countryBriefPage?.updateBrief({ brief: '', country, code, error: t('countryBrief.assessmentUnavailable') });
}
}

Expand Down
9 changes: 5 additions & 4 deletions src/app/data-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1459,9 +1459,10 @@ export class DataLoaderManager implements AppModule {
this.dailyBriefGeneration++;
const gen = this.dailyBriefGeneration;
this.ctx.inFlight.add('dailyMarketBrief');
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
const lang = getCurrentLanguage();
try {
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
const cached = await getCachedDailyMarketBrief(timezone);
const cached = await getCachedDailyMarketBrief(timezone, lang);

if (cached?.available) {
this.callPanel('daily-market-brief', 'renderBrief', cached, 'cached');
Expand All @@ -1487,6 +1488,7 @@ export class DataLoaderManager implements AppModule {
const brief = await buildDailyMarketBrief({
markets: this.ctx.latestMarkets,
newsByCategory: this.ctx.newsByCategory,
lang,
timezone,
regimeContext,
yieldCurveContext,
Expand All @@ -1510,8 +1512,7 @@ export class DataLoaderManager implements AppModule {
this.callPanel('daily-market-brief', 'renderBrief', brief, 'live');
} catch (error) {
console.warn('[DailyBrief] Failed to build daily market brief:', error);
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC';
const cached = await getCachedDailyMarketBrief(timezone).catch(() => null);
const cached = await getCachedDailyMarketBrief(timezone, lang).catch(() => null);
if (cached?.available) {
this.callPanel('daily-market-brief', 'renderBrief', cached, 'cached');
return;
Expand Down
65 changes: 63 additions & 2 deletions src/components/CountryBriefPage.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { escapeHtml, sanitizeUrl } from '@/utils/sanitize';
import { formatIntelBrief } from '@/utils/format-intel-brief';
import { t } from '@/services/i18n';
import { getCurrentLanguage, t } from '@/services/i18n';
import { getCSSColor } from '@/utils';
import type { CountryScore } from '@/services/country-instability';
import type { NewsItem } from '@/types';
Expand All @@ -15,8 +15,10 @@ import { exportCountryBriefJSON, exportCountryBriefCSV } from '@/utils/export';
import type { CountryBriefExport } from '@/utils/export';
import { ME_STRIKE_BOUNDS } from '@/services/country-geometry';
import { toFlagEmoji } from '@/utils/country-flag';
import { getCachedContentTranslation, shouldTranslateContent, translateContentText } from '@/services/content-translation';

type BriefAssetType = AssetType | 'port';
const COUNTRY_BRIEF_TRANSLATION_DELAY_MS = 80;

export class CountryBriefPage implements CountryBriefPanel {
private static BRIEF_BOUNDS: Record<string, { n: number; s: number; e: number; w: number }> = {
Expand Down Expand Up @@ -63,6 +65,8 @@ export class CountryBriefPage implements CountryBriefPanel {
private onShareStory?: (code: string, name: string) => void;
private onExportImage?: (code: string, name: string) => void;
private abortController: AbortController = new AbortController();
private newsTranslationTimer: ReturnType<typeof setTimeout> | null = null;
private newsTranslationRequestId = 0;

constructor() {
this.overlay = document.createElement('div');
Expand Down Expand Up @@ -281,6 +285,7 @@ export class CountryBriefPage implements CountryBriefPanel {
}

public showLoading(): void {
this.cancelPendingNewsTranslations();
this.currentCode = '__loading__';
this.overlay.innerHTML = `
<div class="country-brief-page">
Expand All @@ -306,6 +311,7 @@ export class CountryBriefPage implements CountryBriefPanel {
}

public showGeoError(onRetry: () => void): void {
this.cancelPendingNewsTranslations();
this.currentCode = '__error__';
this.overlay.textContent = '';

Expand Down Expand Up @@ -361,6 +367,7 @@ export class CountryBriefPage implements CountryBriefPanel {

public show(country: string, code: string, score: CountryScore | null, signals: CountryBriefSignals): void {
this.abortController.abort();
this.cancelPendingNewsTranslations();
this.abortController = new AbortController();
this.currentCode = code;
this.currentName = country;
Expand Down Expand Up @@ -556,6 +563,7 @@ export class CountryBriefPage implements CountryBriefPanel {
if (!section || !content || headlines.length === 0) return;

const items = headlines.slice(0, 8);
const currentLang = getCurrentLanguage();
this.currentHeadlineCount = items.length;
this.currentHeadlines = items;
section.style.display = '';
Expand All @@ -567,17 +575,28 @@ export class CountryBriefPage implements CountryBriefPanel {
: item.threat?.level === 'medium' ? getCSSColor('--threat-medium')
: getCSSColor('--threat-info');
const timeAgo = this.timeAgo(item.pubDate);
const cachedTitle = shouldTranslateContent(currentLang, item.lang)
? getCachedContentTranslation(item.title, currentLang)
: undefined;
const displayTitle = cachedTitle ?? item.title;
const wasTranslated = typeof cachedTitle === 'string' && cachedTitle !== item.title;
const cardBody = `
<span class="cb-news-threat" style="background:${threatColor}"></span>
<div class="cb-news-body">
<div class="cb-news-title">${escapeHtml(item.title)}</div>
<div class="cb-news-title"
data-original-title="${escapeHtml(item.title)}"
data-source-lang="${escapeHtml(item.lang || '')}"
data-translation-state="${wasTranslated ? 'translated' : 'original'}"
${wasTranslated ? `data-translated-title="${escapeHtml(displayTitle)}" title="${escapeHtml(item.title)}"` : ''}>${escapeHtml(displayTitle)}</div>
<div class="cb-news-meta">${escapeHtml(item.source)} · ${timeAgo}</div>
</div>`;
if (safeUrl) {
return `<a href="${safeUrl}" target="_blank" rel="noopener" class="cb-news-card" id="cb-news-${i + 1}">${cardBody}</a>`;
}
return `<div class="cb-news-card" id="cb-news-${i + 1}">${cardBody}</div>`;
}).join('');

this.scheduleAutoTranslateNewsTitles();
}


Expand Down Expand Up @@ -652,6 +671,47 @@ export class CountryBriefPage implements CountryBriefPanel {
return t('modals.countryBrief.timeAgo.d', { count: Math.floor(hours / 24) });
}

private cancelPendingNewsTranslations(): void {
this.newsTranslationRequestId += 1;
if (this.newsTranslationTimer) {
clearTimeout(this.newsTranslationTimer);
this.newsTranslationTimer = null;
}
}

private scheduleAutoTranslateNewsTitles(): void {
this.cancelPendingNewsTranslations();
const targetLang = getCurrentLanguage();
if (targetLang === 'en') return;

const requestId = this.newsTranslationRequestId;
this.newsTranslationTimer = setTimeout(() => {
void this.autoTranslateNewsTitles(targetLang, requestId);
}, COUNTRY_BRIEF_TRANSLATION_DELAY_MS);
}

private async autoTranslateNewsTitles(targetLang: string, requestId: number): Promise<void> {
if (requestId !== this.newsTranslationRequestId || !this.overlay.isConnected) return;

const titleEls = Array.from(this.overlay.querySelectorAll<HTMLElement>('.cb-news-title[data-original-title]'));
for (const titleEl of titleEls) {
if (requestId !== this.newsTranslationRequestId || !titleEl.isConnected) return;

const originalTitle = titleEl.dataset.originalTitle || titleEl.textContent || '';
const sourceLang = titleEl.dataset.sourceLang;
if (!originalTitle || !shouldTranslateContent(targetLang, sourceLang)) continue;

const translated = await translateContentText(originalTitle, targetLang, { sourceLang });
if (requestId !== this.newsTranslationRequestId || !titleEl.isConnected) return;
if (!translated || translated === originalTitle) continue;

titleEl.textContent = translated;
titleEl.dataset.translatedTitle = translated;
titleEl.dataset.translationState = 'translated';
titleEl.title = originalTitle;
}
}

private formatBrief(text: string, headlineCount = 0): string {
return formatIntelBrief(text, headlineCount > 0 ? { count: headlineCount, hrefPrefix: '#cb-news-' } : undefined);
}
Expand Down Expand Up @@ -749,6 +809,7 @@ export class CountryBriefPage implements CountryBriefPanel {

public hide(): void {
this.abortController.abort();
this.cancelPendingNewsTranslations();
this.overlay.classList.remove('active');
this.currentCode = null;
this.currentName = null;
Expand Down
Loading
Loading