import { Panel } from './Panel'; import { t } from '@/services/i18n'; import { hasPremiumAccess } from '@/services/panel-gating'; import { FrameworkSelector } from './FrameworkSelector'; import type { DailyMarketBrief } from '@/services/daily-market-brief'; import { describeFreshness } from '@/services/persistent-cache'; import { escapeHtml, unsafeRawHtml } from '@/utils/sanitize'; import { getChangeClass } from '@/utils'; import { createWatchlistButton } from './watchlist-modal'; type BriefSource = 'live' | 'cached'; function formatGeneratedTime(isoTimestamp: string, timezone: string): string { try { return new Intl.DateTimeFormat('en-US', { timeZone: timezone, hour: 'numeric', minute: '2-digit', month: 'short', day: 'numeric', }).format(new Date(isoTimestamp)); } catch { return isoTimestamp; } } function stanceLabel(stance: DailyMarketBrief['items'][number]['stance']): string { if (stance === 'bullish') return 'Bullish'; if (stance === 'defensive') return 'Defensive'; return 'Neutral'; } function formatPrice(price: number | null): string { if (typeof price !== 'number' || !Number.isFinite(price)) return 'N/A'; return price.toLocaleString(undefined, { maximumFractionDigits: 2 }); } function formatChange(change: number | null): string { if (typeof change !== 'number' || !Number.isFinite(change)) return 'Flat'; const sign = change > 0 ? '+' : ''; return `${sign}${change.toFixed(2)}%`; } export class DailyMarketBriefPanel extends Panel { private fwSelector: FrameworkSelector; constructor() { super({ id: 'daily-market-brief', title: 'Daily Market Brief', infoTooltip: t('components.dailyMarketBrief.infoTooltip'), premium: 'locked' }); this.fwSelector = new FrameworkSelector({ panelId: 'daily-market-brief', isPremium: hasPremiumAccess(), panel: this, note: 'Applies to client-generated analysis only' }); this.header.appendChild(this.fwSelector.el); this.header.appendChild(createWatchlistButton('Edit Watchlist')); } public override destroy(): void { this.fwSelector.destroy(); super.destroy(); } public renderBrief(brief: DailyMarketBrief, source: BriefSource = 'live'): void { const freshness = describeFreshness(new Date(brief.generatedAt).getTime()); this.setDataBadge(source, freshness); this.resetRetryBackoff(); const html = `
${escapeHtml(brief.title)}
${escapeHtml(formatGeneratedTime(brief.generatedAt, brief.timezone))}
${escapeHtml(brief.summary)}
Action Plan
${escapeHtml(brief.actionPlan)}
Risk Watch
${escapeHtml(brief.riskWatch)}
${brief.items.map((item) => `
${escapeHtml(item.name)}
${escapeHtml(item.display)}
${escapeHtml(formatPrice(item.price))}
${escapeHtml(formatChange(item.change))}
${escapeHtml(stanceLabel(item.stance))}
${item.relatedHeadline ? `
Linked headline
` : ''}
${escapeHtml(item.note)}
`).join('')}
`; this.setSafeContent(unsafeRawHtml(html, 'legacy Panel.setContent() migration')); } public showUnavailable(message = 'The daily brief needs live market data before it can be generated.'): void { this.showError(message); } }