/** * Download Reports Component * Simple unified PDF download button with i18n support */ "use client"; import { useState } from "react"; import { Download, FileText } from "lucide-react"; import { Button } from "@/components/ui/button"; import { useLanguage } from "@/contexts/LanguageContext"; interface AnalystInfo { key: string; label: string; reportKey: string; description: string; } interface DownloadReportsProps { ticker: string; analysisDate: string; taskId?: string | null; analysts: AnalystInfo[]; reports: any; priceData?: any[]; priceStats?: any; /** Compact mode - just the button, no card wrapper */ compact?: boolean; /** Language for report generation */ language?: string; } export function DownloadReports({ ticker, analysisDate, taskId: _taskId, // Kept for API compatibility, but direct mode is now preferred analysts, reports, priceData, priceStats, compact = false, language, }: DownloadReportsProps) { const [isDownloading, setIsDownloading] = useState(false); const { t, locale } = useLanguage(); // Helper to get nested value from reports object const getNestedValue = (obj: any, path: string) => { return path.split('.').reduce((current, key) => current?.[key], obj); }; // Helper to detect report language based on trader's final decision keywords // This ensures PDF language matches the report content language, not UI language const detectReportLanguage = (data: any): 'zh-TW' | 'en' | null => { try { // Get trader's investment plan (final decision) const traderPlan = data?.trader_investment_plan; if (!traderPlan || typeof traderPlan !== 'string') { return null; } // Chinese decision keywords: 買入, 賣出, 持有 const chineseKeywords = ['買入', '賣出', '持有']; // English decision keywords: buy, sell, hold (case insensitive) const englishKeywords = ['buy', 'sell', 'hold']; const lowerPlan = traderPlan.toLowerCase(); // Check for Chinese keywords first for (const keyword of chineseKeywords) { if (traderPlan.includes(keyword)) { return 'zh-TW'; } } // Check for English keywords for (const keyword of englishKeywords) { if (lowerPlan.includes(keyword)) { return 'en'; } } // Fallback: check for any Chinese characters as secondary detection const chineseRegex = /[\u4e00-\u9fa5]/; if (chineseRegex.test(traderPlan)) { return 'zh-TW'; } return 'en'; } catch (e) { return null; } }; // Get all available analyst keys (those with actual reports) const availableAnalystKeys = analysts .filter(analyst => { const reportContent = getNestedValue(reports, analyst.reportKey); return reportContent && reportContent.trim().length > 0; }) .map(a => a.key); // Handle download - always download all available reports as combined PDF const handleDownload = async () => { if (availableAnalystKeys.length === 0) return; setIsDownloading(true); try { // Build request body with all available analysts // Always use direct mode when reports data is available (task may be cleaned up from Redis) // Detect report language based on trader's final decision keywords const contentLang = detectReportLanguage(reports); // Use detected language if available, otherwise fallback to prop or current locale // This ensures that if reports are in Chinese, the PDF headers will also be in Chinese // even if the UI is in English const finalLanguage = contentLang || language || locale; const requestBody: any = { ticker, analysis_date: analysisDate, analysts: availableAnalystKeys, // Always include all available analysts language: finalLanguage, // Pass language for PDF generation // Direct mode: send report data directly (more reliable than task-based) reports: reports, price_data: priceData, price_stats: priceStats, }; const response = await fetch('/api/download/reports', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(requestBody), }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); const errorMessage = errorData.detail || `${t.download.failed} (${response.status})`; throw new Error(errorMessage); } // Get the blob const blob = await response.blob(); // Get filename from Content-Disposition header if available const contentDisposition = response.headers.get('Content-Disposition'); let filename = `${ticker}_Combined_Report_${analysisDate}.pdf`; if (contentDisposition) { const filenameMatch = contentDisposition.match(/filename=(.+)/); if (filenameMatch) { filename = filenameMatch[1]; } } // Create download link const url = window.URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); // Cleanup document.body.removeChild(link); window.URL.revokeObjectURL(url); } catch (error: any) { console.error('Download error:', error); alert(error.message || t.download.failed); } finally { setIsDownloading(false); } }; if (availableAnalystKeys.length === 0) { return null; } // Compact mode - just the button if (compact) { return ( ); } // Full mode - with description return (