/** * Report Export Dialog Component * Full-featured report export with preview and multi-format support * Cassette futurism aesthetic */ import { useState, useEffect, useCallback, memo } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; import { FileText, FileJson, FileCode, Download, Loader2, Copy, Check, AlertTriangle, RefreshCw, Eye, Terminal, Shield, Bug, CheckCircle2, } from "lucide-react"; import { apiClient } from "@/shared/api/serverClient"; import { downloadAgentReport } from "@/shared/api/agentTasks"; import type { AgentTask, AgentFinding } from "@/shared/api/agentTasks"; // ============ Types ============ type ReportFormat = "markdown" | "json" | "html"; interface ReportExportDialogProps { open: boolean; onOpenChange: (open: boolean) => void; task: AgentTask | null; findings: AgentFinding[]; } interface ReportPreview { content: string; format: ReportFormat; loading: boolean; error: string | null; } // ============ Constants ============ const FORMAT_CONFIG: Record = { markdown: { label: "Markdown", icon: , extension: ".md", mime: "text/markdown", }, json: { label: "JSON", icon: , extension: ".json", mime: "application/json", }, html: { label: "HTML", icon: , extension: ".html", mime: "text/html", }, }; // ============ Helper Functions ============ function getSeverityColor(severity: string): string { const colors: Record = { critical: "text-rose-400", high: "text-orange-400", medium: "text-amber-400", low: "text-sky-400", info: "text-slate-400", }; return colors[severity.toLowerCase()] || colors.info; } function formatBytes(bytes: number): string { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; } // ============ Sub Components ============ // Report stats summary - uses task statistics for reliability const ReportStats = memo(function ReportStats({ task, }: { task: AgentTask; findings: AgentFinding[]; // Keep for API compatibility, but use task stats }) { // Use task's reliable statistics instead of computing from findings array // This ensures consistency even when findings array is empty or not loaded const totalFindings = task.findings_count || 0; const criticalAndHigh = (task.critical_count || 0) + (task.high_count || 0); const verified = task.verified_count || 0; return (
Score
{task.security_score?.toFixed(0) || "N/A"}
Findings
{totalFindings}
Critical
{criticalAndHigh}
Verified
{verified}
); }); // Markdown preview renderer const MarkdownPreview = memo(function MarkdownPreview({ content, }: { content: string; }) { // Simple markdown to styled elements renderer const renderMarkdown = (text: string) => { const lines = text.split("\n"); const elements: React.ReactNode[] = []; let inCodeBlock = false; let codeContent: string[] = []; let codeLanguage = ""; lines.forEach((line, index) => { // Code block handling if (line.startsWith("```")) { if (inCodeBlock) { elements.push(
{codeLanguage || "code"}
                {codeContent.join("\n")}
              
); codeContent = []; codeLanguage = ""; inCodeBlock = false; } else { inCodeBlock = true; codeLanguage = line.slice(3).trim(); } return; } if (inCodeBlock) { codeContent.push(line); return; } // Headers if (line.startsWith("# ")) { elements.push(

{line.slice(2)}

); return; } if (line.startsWith("## ")) { elements.push(

{line.slice(3)}

); return; } if (line.startsWith("### ")) { elements.push(

{line.slice(4)}

); return; } // Horizontal rule if (line.match(/^---+$/)) { elements.push(
); return; } // List items if (line.match(/^[-*]\s/)) { elements.push(
{line.slice(2)}
); return; } // Bold text handling let processedLine = line; if (line.includes("**")) { const parts = line.split(/\*\*(.+?)\*\*/g); const lineElements = parts.map((part, i) => { if (i % 2 === 1) { return {part}; } return part; }); elements.push(

{lineElements}

); return; } // Empty lines if (line.trim() === "") { elements.push(
); return; } // Regular paragraphs elements.push(

{processedLine}

); }); return elements; }; return (
{renderMarkdown(content)}
); }); // JSON preview with syntax highlighting const JsonPreview = memo(function JsonPreview({ content, }: { content: string; }) { const highlightJson = (json: string) => { try { const parsed = JSON.parse(json); const formatted = JSON.stringify(parsed, null, 2); return formatted .replace(/"([^"]+)":/g, '"$1":') .replace(/: "([^"]+)"/g, ': "$1"') .replace(/: (\d+)/g, ': $1') .replace(/: (true|false)/g, ': $1') .replace(/: (null)/g, ': $1'); } catch { return json; } }; return (
  );
});

// ============ Main Component ============

export const ReportExportDialog = memo(function ReportExportDialog({
  open,
  onOpenChange,
  task,
  findings,
}: ReportExportDialogProps) {
  const [activeFormat, setActiveFormat] = useState("markdown");
  const [preview, setPreview] = useState({
    content: "",
    format: "markdown",
    loading: false,
    error: null,
  });
  const [copied, setCopied] = useState(false);
  const [downloading, setDownloading] = useState(false);

  // Fetch report content for preview
  const fetchPreview = useCallback(async (format: ReportFormat) => {
    if (!task) return;

    setPreview(prev => ({ ...prev, loading: true, error: null }));

    try {
      // For JSON, fetch from backend API to ensure data consistency
      // The backend properly queries findings from the database
      if (format === "json") {
        const response = await apiClient.get(`/agent-tasks/${task.id}/report`, {
          params: { format: "json" },
        });

        setPreview({
          content: JSON.stringify(response.data, null, 2),
          format: "json",
          loading: false,
          error: null,
        });
        return;
      }

      // For HTML, generate from markdown
      if (format === "html") {
        const mdResponse = await apiClient.get(`/agent-tasks/${task.id}/report`, {
          params: { format: "markdown" },
          responseType: "text",
        });

        const htmlContent = generateHtmlReport(mdResponse.data, task);
        setPreview({
          content: htmlContent,
          format: "html",
          loading: false,
          error: null,
        });
        return;
      }

      // For Markdown, fetch from server
      const response = await apiClient.get(`/agent-tasks/${task.id}/report`, {
        params: { format: "markdown" },
        responseType: "text",
      });

      setPreview({
        content: response.data,
        format: "markdown",
        loading: false,
        error: null,
      });
    } catch (err) {
      console.error("Failed to fetch report preview:", err);
      setPreview(prev => ({
        ...prev,
        loading: false,
        error: "Failed to load report preview",
      }));
    }
  }, [task, findings]);

  // Generate HTML report from markdown
  const generateHtmlReport = (markdown: string, task: AgentTask): string => {
    // Convert markdown to HTML with styling
    let html = markdown
      .replace(/^# (.+)$/gm, '

$1

') .replace(/^## (.+)$/gm, '

$1

') .replace(/^### (.+)$/gm, '

$1

') .replace(/\*\*(.+?)\*\*/g, '$1') .replace(/`([^`]+)`/g, '$1') .replace(/^- (.+)$/gm, '
  • $1
  • ') .replace(/^---$/gm, '
    ') .replace(/\n\n/g, '

    '); return ` Security Audit Report - ${task.name || task.id}

    DEEPAUDIT Security Report

    Generated ${new Date().toLocaleString()}

    ${html}

    `; }; // Load preview when format changes or dialog opens useEffect(() => { if (open && task) { fetchPreview(activeFormat); } }, [open, activeFormat, task, fetchPreview]); // Handle copy to clipboard const handleCopy = async () => { try { await navigator.clipboard.writeText(preview.content); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch (err) { console.error("Failed to copy:", err); } }; // Handle download const handleDownload = async () => { if (!task) return; setDownloading(true); try { if (activeFormat === "markdown" || activeFormat === "json") { await downloadAgentReport(task.id, activeFormat); } else { // HTML download const blob = new Blob([preview.content], { type: "text/html" }); const url = window.URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `audit-report-${task.id.slice(0, 8)}.html`; document.body.appendChild(link); link.click(); link.remove(); window.URL.revokeObjectURL(url); } } catch (err) { console.error("Download failed:", err); } finally { setDownloading(false); } }; if (!task) return null; return ( {/* Header */}
    Export Audit Report

    Task: {task.name || task.id.slice(0, 8)}

    {/* Stats Summary */}
    {/* Format Tabs & Content */} setActiveFormat(v as ReportFormat)} className="flex flex-col flex-1 overflow-hidden" > {/* Tab List */}
    {(Object.keys(FORMAT_CONFIG) as ReportFormat[]).map((format) => { const config = FORMAT_CONFIG[format]; return ( {config.icon} {config.label} ); })} {/* Actions */}
    {/* Preview Content */}
    {(Object.keys(FORMAT_CONFIG) as ReportFormat[]).map((format) => (
    {preview.loading ? (
    Loading preview...
    ) : preview.error ? (
    {preview.error}
    ) : (
    {/* Preview header */}
    Preview
    {formatBytes(preview.content.length)}
    {/* Preview content */}
    {format === "markdown" && } {format === "json" && } {format === "html" && (
    {preview.content.slice(0, 3000)} {preview.content.length > 3000 && ( ... (truncated for preview) )}
    )}
    )}
    ))}
    {/* Footer */}
    Export as {FORMAT_CONFIG[activeFormat].label} ({FORMAT_CONFIG[activeFormat].extension})
    ); }); export default ReportExportDialog;