/** * Download Reports Component * Allows users to select and download analyst reports */ "use client"; import { useState } from "react"; import { Download, FileDown, CheckIcon } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { cn } from "@/lib/utils"; interface AnalystInfo { key: string; label: string; reportKey: string; description: string; } interface DownloadReportsProps { ticker: string; analysisDate: string; taskId: string; analysts: AnalystInfo[]; reports: any; } export function DownloadReports({ ticker, analysisDate, taskId, analysts, reports, }: DownloadReportsProps) { const [selectedAnalysts, setSelectedAnalysts] = useState([]); const [isDownloading, setIsDownloading] = useState(false); // Helper to get nested value from reports object const getNestedValue = (obj: any, path: string) => { return path.split('.').reduce((current, key) => current?.[key], obj); }; // Filter analysts that have actual reports const availableAnalysts = analysts.filter(analyst => { const reportContent = getNestedValue(reports, analyst.reportKey); return reportContent && reportContent.trim().length > 0; }); // Handle select all const handleSelectAll = () => { if (selectedAnalysts.length === availableAnalysts.length) { setSelectedAnalysts([]); } else { setSelectedAnalysts(availableAnalysts.map(a => a.key)); } }; // Handle individual selection const handleToggleAnalyst = (analystKey: string) => { setSelectedAnalysts(prev => { if (prev.includes(analystKey)) { return prev.filter(key => key !== analystKey); } else { return [...prev, analystKey]; } }); }; // Handle download const handleDownload = async () => { if (selectedAnalysts.length === 0) return; setIsDownloading(true); try { const response = await fetch('/api/download/reports', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ ticker, analysis_date: analysisDate, task_id: taskId, analysts: selectedAnalysts, }), }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); const errorMessage = errorData.detail || `下載失敗 (${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}_${analysisDate}.pdf`; if (contentDisposition) { const filenameMatch = contentDisposition.match(/filename=(.+)/); if (filenameMatch) { filename = filenameMatch[1]; } } else if (selectedAnalysts.length > 1) { filename = `${ticker}_${analysisDate}.zip`; } // 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 || '下載失敗,請稍後再試'); } finally { setIsDownloading(false); } }; if (availableAnalysts.length === 0) { return null; } const isAllSelected = selectedAnalysts.length === availableAnalysts.length && availableAnalysts.length > 0; return ( 下載報告 選擇要下載的分析師報告(支援單一PDF或多個ZIP) {/* Select All */}
{/* Analyst List */}
{availableAnalysts.map(analyst => { const isSelected = selectedAnalysts.includes(analyst.key); return (
handleToggleAnalyst(analyst.key)} className={cn( "relative flex cursor-pointer flex-col gap-2 rounded-lg border-2 p-4 transition-all hover:bg-accent", isSelected ? "border-primary bg-primary/5 text-primary" : "border-muted-foreground/25 bg-card text-muted-foreground" )} >
{isSelected && }

{analyst.label}

{analyst.description}

); })}
{/* Download Button */}
已選擇 {selectedAnalysts.length} 個報告
); }