| |
| |
| |
| |
|
|
| const VERDICT_LABEL = { |
| authentic: 'AUTHENTIC', |
| ai_generated: 'AI-GENERATED', |
| suspect: 'SUSPECT / INCONCLUSIVE', |
| }; |
|
|
| const VERDICT_COLOR = { |
| authentic: '#00c67a', |
| ai_generated: '#ff4757', |
| suspect: '#f59e0b', |
| }; |
|
|
| const SEV_COLOR = { |
| critical: '#ff4757', |
| high: '#f59e0b', |
| medium: '#7c3aed', |
| low: '#8896ab', |
| }; |
|
|
| function escapeHtml(str) { |
| return String(str ?? 'β') |
| .replace(/&/g, '&') |
| .replace(/</g, '<') |
| .replace(/>/g, '>') |
| .replace(/"/g, '"'); |
| } |
|
|
| function modelBreakdownRows(models) { |
| return models.map(m => ` |
| <tr> |
| <td>${escapeHtml(m.model)}</td> |
| <td class="mono">${m.score.toFixed(1)}%</td> |
| <td> |
| <div class="bar-track"> |
| <div class="bar-fill" style="width:${m.score}%"></div> |
| </div> |
| </td> |
| <td class="mono">${(m.weight * 100).toFixed(0)}%</td> |
| </tr> |
| `).join(''); |
| } |
|
|
| function artifactRows(artifacts) { |
| return artifacts.map(a => ` |
| <tr> |
| <td><span class="sev-badge" style="background:${SEV_COLOR[a.severity]}20;color:${SEV_COLOR[a.severity]};border:1px solid ${SEV_COLOR[a.severity]}40">${a.severity.toUpperCase()}</span></td> |
| <td><strong>${escapeHtml(a.type)}</strong></td> |
| <td>${escapeHtml(a.detail)}</td> |
| </tr> |
| `).join(''); |
| } |
|
|
| function timelineSection(result) { |
| if (result.type !== 'video' || !result.timeline) return ''; |
|
|
| const segments = result.timeline.flaggedSegments.map(s => ` |
| <tr> |
| <td><span class="sev-badge" style="background:${SEV_COLOR[s.severity]}20;color:${SEV_COLOR[s.severity]};border:1px solid ${SEV_COLOR[s.severity]}40">${s.severity.toUpperCase()}</span></td> |
| <td>${escapeHtml(s.reason)}</td> |
| <td class="mono">${s.start.toFixed(1)}s β ${s.end.toFixed(1)}s</td> |
| <td class="mono">${s.frames.length} frames</td> |
| </tr> |
| `).join(''); |
|
|
| return ` |
| <section> |
| <h2>4. Temporal Analysis</h2> |
| <p>Total flagged segments: <strong>${result.timeline.flaggedSegments.length}</strong> | |
| Clean segments: <strong>${result.timeline.cleanSegments.length}</strong></p> |
| <table> |
| <thead> |
| <tr><th>Severity</th><th>Anomaly</th><th>Timestamp</th><th>Frames</th></tr> |
| </thead> |
| <tbody>${segments}</tbody> |
| </table> |
| </section> |
| `; |
| } |
|
|
| function metadataRows(meta) { |
| return Object.entries(meta).map(([key, val]) => { |
| const label = key.replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase()); |
| return `<tr><td>${escapeHtml(label)}</td><td class="mono">${escapeHtml(val)}</td></tr>`; |
| }).join(''); |
| } |
|
|
| function gradcamTable(regions) { |
| return regions.map(r => ` |
| <tr> |
| <td>${escapeHtml(r.label)}</td> |
| <td class="mono">${Math.round(r.intensity * 100)}%</td> |
| <td> |
| <span class="sev-badge" style="background:${r.intensity > 0.8 ? '#ff475720' : r.intensity > 0.5 ? '#f59e0b20' : '#00c67a20'}; |
| color:${r.intensity > 0.8 ? '#ff4757' : r.intensity > 0.5 ? '#f59e0b' : '#00c67a'}; |
| border:1px solid ${r.intensity > 0.8 ? '#ff475740' : r.intensity > 0.5 ? '#f59e0b40' : '#00c67a40'}"> |
| ${r.intensity > 0.8 ? 'High' : r.intensity > 0.5 ? 'Medium' : 'Low'} |
| </span> |
| </td> |
| <td class="mono">x:${r.x}% y:${r.y}% w:${r.w}% h:${r.h}%</td> |
| </tr> |
| `).join(''); |
| } |
|
|
| export function generateReport(result, previewUrl) { |
| const verdictColor = VERDICT_COLOR[result.verdict]; |
| const verdictLabel = VERDICT_LABEL[result.verdict]; |
| const now = new Date().toUTCString(); |
|
|
| const html = `<!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| <title>UAIDE Forensic Report β ${escapeHtml(result.filename)}</title> |
| <style> |
| :root { |
| --accent: #0066ff; |
| --verdict: ${verdictColor}; |
| --text: #141820; |
| --muted: #6b7280; |
| --border: #e2e8f0; |
| --bg: #f8f9fc; |
| --surface: #ffffff; |
| } |
| |
| * { box-sizing: border-box; margin: 0; padding: 0; } |
| |
| body { |
| font-family: 'Segoe UI', Inter, system-ui, sans-serif; |
| background: var(--bg); |
| color: var(--text); |
| font-size: 13.5px; |
| line-height: 1.65; |
| padding: 0; |
| } |
| |
| .mono { font-family: 'Cascadia Code', 'Fira Code', 'Courier New', monospace; font-size: 12.5px; } |
| |
| /* Cover */ |
| .cover { |
| background: linear-gradient(135deg, #0a0f1e 0%, #0d1930 60%, #091428 100%); |
| color: white; |
| padding: 56px 64px; |
| display: flex; |
| flex-direction: column; |
| gap: 0; |
| page-break-after: always; |
| min-height: 340px; |
| } |
| |
| .cover-logo { |
| display: flex; |
| align-items: center; |
| gap: 14px; |
| margin-bottom: 36px; |
| } |
| |
| .logo-mark { |
| width: 44px; |
| height: 44px; |
| background: var(--accent); |
| border-radius: 10px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-weight: 900; |
| font-size: 17px; |
| letter-spacing: -0.5px; |
| color: white; |
| flex-shrink: 0; |
| } |
| |
| .logo-text { |
| display: flex; |
| flex-direction: column; |
| gap: 2px; |
| } |
| |
| .logo-name { |
| font-size: 17px; |
| font-weight: 800; |
| letter-spacing: -0.3px; |
| color: white; |
| } |
| |
| .logo-sub { |
| font-size: 10px; |
| font-weight: 500; |
| color: rgba(255,255,255,0.45); |
| letter-spacing: 1.2px; |
| text-transform: uppercase; |
| } |
| |
| .cover h1 { |
| font-size: 28px; |
| font-weight: 800; |
| letter-spacing: -0.8px; |
| margin-bottom: 10px; |
| color: white; |
| line-height: 1.2; |
| } |
| |
| .cover-sub { |
| font-size: 14px; |
| color: rgba(255,255,255,0.55); |
| margin-bottom: 32px; |
| } |
| |
| .verdict-banner { |
| display: inline-flex; |
| align-items: center; |
| gap: 10px; |
| background: ${verdictColor}22; |
| border: 1.5px solid ${verdictColor}55; |
| border-radius: 10px; |
| padding: 12px 20px; |
| margin-bottom: 32px; |
| } |
| |
| .verdict-dot { |
| width: 10px; |
| height: 10px; |
| border-radius: 50%; |
| background: ${verdictColor}; |
| flex-shrink: 0; |
| } |
| |
| .verdict-text { |
| font-size: 22px; |
| font-weight: 800; |
| letter-spacing: -0.3px; |
| color: ${verdictColor}; |
| } |
| |
| .verdict-score { |
| font-size: 13px; |
| color: rgba(255,255,255,0.6); |
| font-family: 'Fira Code', monospace; |
| } |
| |
| .cover-meta { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 24px; |
| margin-top: auto; |
| padding-top: 32px; |
| border-top: 1px solid rgba(255,255,255,0.1); |
| } |
| |
| .cover-meta-item { |
| display: flex; |
| flex-direction: column; |
| gap: 3px; |
| } |
| |
| .cover-meta-label { |
| font-size: 10px; |
| color: rgba(255,255,255,0.35); |
| text-transform: uppercase; |
| letter-spacing: 0.9px; |
| font-weight: 600; |
| } |
| |
| .cover-meta-value { |
| font-size: 13px; |
| color: rgba(255,255,255,0.85); |
| font-family: 'Fira Code', monospace; |
| font-weight: 500; |
| } |
| |
| /* Body */ |
| .body { |
| padding: 48px 64px; |
| max-width: 960px; |
| margin: 0 auto; |
| } |
| |
| section { |
| margin-bottom: 44px; |
| page-break-inside: avoid; |
| } |
| |
| h2 { |
| font-size: 17px; |
| font-weight: 800; |
| color: var(--text); |
| letter-spacing: -0.4px; |
| margin-bottom: 14px; |
| padding-bottom: 10px; |
| border-bottom: 2px solid var(--border); |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| } |
| |
| h2::before { |
| content: ''; |
| width: 4px; |
| height: 18px; |
| background: var(--accent); |
| border-radius: 2px; |
| flex-shrink: 0; |
| } |
| |
| h3 { |
| font-size: 14px; |
| font-weight: 700; |
| color: var(--text); |
| margin: 18px 0 10px; |
| letter-spacing: -0.2px; |
| } |
| |
| p { margin-bottom: 8px; } |
| |
| /* Summary grid */ |
| .summary-grid { |
| display: grid; |
| grid-template-columns: repeat(3, 1fr); |
| gap: 14px; |
| margin-bottom: 16px; |
| } |
| |
| .summary-card { |
| background: var(--surface); |
| border: 1px solid var(--border); |
| border-radius: 10px; |
| padding: 14px 16px; |
| } |
| |
| .summary-card-label { |
| font-size: 10px; |
| font-weight: 700; |
| color: var(--muted); |
| text-transform: uppercase; |
| letter-spacing: 0.7px; |
| margin-bottom: 5px; |
| } |
| |
| .summary-card-value { |
| font-size: 22px; |
| font-weight: 800; |
| color: var(--text); |
| letter-spacing: -0.5px; |
| } |
| |
| .summary-card-value.verdict-val { |
| font-size: 16px; |
| color: var(--verdict); |
| } |
| |
| /* Score arc */ |
| .score-section { |
| display: flex; |
| align-items: center; |
| gap: 28px; |
| background: var(--surface); |
| border: 1px solid var(--border); |
| border-radius: 12px; |
| padding: 20px 24px; |
| margin-bottom: 16px; |
| } |
| |
| .score-label-group { flex: 1; } |
| .score-main { font-size: 48px; font-weight: 900; color: var(--verdict); letter-spacing: -2px; line-height: 1; } |
| .score-unit { font-size: 18px; font-weight: 600; color: var(--muted); } |
| .score-desc { font-size: 13px; color: var(--muted); margin-top: 6px; } |
| |
| /* Tables */ |
| table { |
| width: 100%; |
| border-collapse: collapse; |
| margin-top: 8px; |
| font-size: 13px; |
| } |
| |
| thead tr { |
| background: var(--bg); |
| } |
| |
| th { |
| padding: 9px 12px; |
| text-align: left; |
| font-size: 10.5px; |
| font-weight: 700; |
| color: var(--muted); |
| text-transform: uppercase; |
| letter-spacing: 0.6px; |
| border-bottom: 1.5px solid var(--border); |
| } |
| |
| td { |
| padding: 9px 12px; |
| border-bottom: 1px solid var(--border); |
| vertical-align: middle; |
| color: var(--text); |
| } |
| |
| tr:last-child td { border-bottom: none; } |
| tr:nth-child(even) td { background: #fafbfd; } |
| |
| .bar-track { |
| height: 6px; |
| background: var(--border); |
| border-radius: 99px; |
| overflow: hidden; |
| min-width: 80px; |
| } |
| |
| .bar-fill { |
| height: 100%; |
| background: linear-gradient(90deg, var(--accent) 0%, #4d94ff 100%); |
| border-radius: 99px; |
| } |
| |
| .sev-badge { |
| display: inline-block; |
| padding: 2px 8px; |
| border-radius: 99px; |
| font-size: 10px; |
| font-weight: 700; |
| letter-spacing: 0.3px; |
| white-space: nowrap; |
| } |
| |
| /* Image preview */ |
| .media-preview { |
| text-align: center; |
| background: #0e0e14; |
| border-radius: 12px; |
| overflow: hidden; |
| padding: 16px; |
| margin-bottom: 16px; |
| } |
| |
| .media-preview img { |
| max-width: 100%; |
| max-height: 340px; |
| border-radius: 8px; |
| object-fit: contain; |
| } |
| |
| .media-preview p { |
| color: rgba(255,255,255,0.4); |
| font-size: 12px; |
| margin-top: 10px; |
| margin-bottom: 0; |
| font-family: monospace; |
| } |
| |
| /* FFT */ |
| .fft-grid { |
| display: grid; |
| grid-template-columns: 1fr 1fr; |
| gap: 12px; |
| } |
| |
| .fft-item { |
| background: var(--surface); |
| border: 1px solid var(--border); |
| border-radius: 8px; |
| padding: 12px 14px; |
| } |
| |
| .fft-item-label { |
| font-size: 10px; |
| font-weight: 700; |
| color: var(--muted); |
| text-transform: uppercase; |
| letter-spacing: 0.6px; |
| margin-bottom: 4px; |
| } |
| |
| .fft-item-value { |
| font-size: 13px; |
| font-weight: 600; |
| color: var(--text); |
| line-height: 1.5; |
| } |
| |
| .anomaly-alert { |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| background: #fff0f1; |
| border: 1.5px solid #ff475740; |
| border-radius: 8px; |
| padding: 12px 16px; |
| margin-bottom: 16px; |
| font-size: 13px; |
| font-weight: 600; |
| color: #ff4757; |
| } |
| |
| /* Disclaimer */ |
| .disclaimer { |
| background: #fffbeb; |
| border: 1px solid #f59e0b40; |
| border-radius: 10px; |
| padding: 16px 20px; |
| font-size: 12.5px; |
| color: #92400e; |
| line-height: 1.7; |
| } |
| |
| .disclaimer strong { color: #78350f; } |
| |
| /* Footer */ |
| .report-footer { |
| background: #0a0f1e; |
| color: rgba(255,255,255,0.4); |
| padding: 20px 64px; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| font-size: 11px; |
| margin-top: 60px; |
| } |
| |
| .report-footer strong { color: rgba(255,255,255,0.7); } |
| |
| @media print { |
| body { background: white; } |
| .cover { page-break-after: always; } |
| section { page-break-inside: avoid; } |
| .body { padding: 32px 48px; } |
| } |
| </style> |
| </head> |
| <body> |
| |
| <!-- ββ COVER PAGE βββββββββββββββββββββββββββββββββββββββββββ --> |
| <div class="cover"> |
| <div class="cover-logo"> |
| <div class="logo-mark">U</div> |
| <div class="logo-text"> |
| <span class="logo-name">UAIDE</span> |
| <span class="logo-sub">Unified AI Origin Detection Engine</span> |
| </div> |
| </div> |
| |
| <h1>Forensic Analysis Report</h1> |
| <p class="cover-sub">${escapeHtml(result.filename)} Β· Analysis ID: ${escapeHtml(result.analysisId)}</p> |
| |
| <div class="verdict-banner"> |
| <div class="verdict-dot"></div> |
| <span class="verdict-text">${verdictLabel}</span> |
| <span class="verdict-score"> β ${result.confidenceScore.toFixed(1)}% confidence</span> |
| </div> |
| |
| <div class="cover-meta"> |
| <div class="cover-meta-item"> |
| <span class="cover-meta-label">File</span> |
| <span class="cover-meta-value">${escapeHtml(result.filename)}</span> |
| </div> |
| <div class="cover-meta-item"> |
| <span class="cover-meta-label">Type</span> |
| <span class="cover-meta-value">${escapeHtml(result.format)}</span> |
| </div> |
| <div class="cover-meta-item"> |
| <span class="cover-meta-label">Resolution</span> |
| <span class="cover-meta-value">${escapeHtml(result.resolution)}</span> |
| </div> |
| <div class="cover-meta-item"> |
| <span class="cover-meta-label">File Size</span> |
| <span class="cover-meta-value">${escapeHtml(result.filesize)}</span> |
| </div> |
| <div class="cover-meta-item"> |
| <span class="cover-meta-label">Processing Time</span> |
| <span class="cover-meta-value">${escapeHtml(result.processingTime)}</span> |
| </div> |
| <div class="cover-meta-item"> |
| <span class="cover-meta-label">Report Generated</span> |
| <span class="cover-meta-value">${now}</span> |
| </div> |
| </div> |
| </div> |
| |
| <!-- ββ BODY βββββββββββββββββββββββββββββββββββββββββββββββββ --> |
| <div class="body"> |
| |
| <!-- 1. Executive Summary --> |
| <section> |
| <h2>1. Executive Summary</h2> |
| <div class="score-section"> |
| <div> |
| <div class="score-main">${result.confidenceScore.toFixed(1)}<span class="score-unit">%</span></div> |
| <div class="score-desc">AI Involvement Confidence Score</div> |
| </div> |
| <div class="score-label-group"> |
| <p><strong>Verdict:</strong> <span style="color:${verdictColor};font-weight:800">${verdictLabel}</span></p> |
| <p style="margin-top:6px;color:#6b7280;font-size:13px"> |
| ${result.verdict === 'authentic' |
| ? 'Multi-model ensemble analysis found no significant generative artifacts. Content is consistent with authentic human-captured media.' |
| : result.verdict === 'ai_generated' |
| ? 'Multi-model ensemble analysis detected strong generative artifacts consistent with GAN or diffusion model synthesis. Content is highly likely to be AI-generated.' |
| : 'Multi-model ensemble analysis produced inconclusive results. The media exhibits some characteristics of synthetic generation but does not conclusively meet the threshold for either verdict. Manual expert review is advised.'} |
| </p> |
| </div> |
| </div> |
| |
| <div class="summary-grid"> |
| <div class="summary-card"> |
| <div class="summary-card-label">Models Run</div> |
| <div class="summary-card-value">${result.modelBreakdown.length}</div> |
| </div> |
| <div class="summary-card"> |
| <div class="summary-card-label">Artifacts Found</div> |
| <div class="summary-card-value">${result.artifacts.length}</div> |
| </div> |
| <div class="summary-card"> |
| <div class="summary-card-label">Spectral Anomaly</div> |
| <div class="summary-card-value" style="font-size:15px;color:${result.fft.spectralAnomaly ? '#ff4757' : '#00c67a'}"> |
| ${result.fft.spectralAnomaly ? 'Detected' : 'Not Found'} |
| </div> |
| </div> |
| </div> |
| </section> |
| |
| <!-- 2. Media Preview --> |
| <section> |
| <h2>2. Media Under Analysis</h2> |
| <div class="media-preview"> |
| ${result.type === 'image' |
| ? `<img src="${previewUrl}" alt="Analysed media β ${escapeHtml(result.filename)}" />` |
| : `<p style="color:rgba(255,255,255,0.5);font-size:14px;padding:40px 0">Video preview not embedded in static report<br><span style="font-size:11px">File: ${escapeHtml(result.filename)} Β· ${escapeHtml(result.duration)} Β· ${result.totalFrames} frames</span></p>` |
| } |
| <p>${escapeHtml(result.filename)} Β· ${escapeHtml(result.resolution)} Β· ${escapeHtml(result.filesize)}</p> |
| </div> |
| </section> |
| |
| <!-- 3. Model Ensemble Results --> |
| <section> |
| <h2>3. Model Ensemble Results</h2> |
| <p>The following forensic deep learning models were run in ensemble. Each model independently scores the likelihood of AI generation; results are weighted and fused into the final confidence score.</p> |
| <table> |
| <thead> |
| <tr> |
| <th>Model</th> |
| <th>Score</th> |
| <th>Confidence Bar</th> |
| <th>Weight</th> |
| </tr> |
| </thead> |
| <tbody> |
| ${modelBreakdownRows(result.modelBreakdown)} |
| </tbody> |
| </table> |
| </section> |
| |
| <!-- 4. Temporal Analysis (video only) --> |
| ${timelineSection(result)} |
| |
| <!-- 5. Grad-CAM Artifact Localisation --> |
| <section> |
| <h2>${result.type === 'video' ? '5' : '4'}. Grad-CAM Artifact Localisation</h2> |
| <p>Gradient-weighted Class Activation Mapping (Grad-CAM) was applied to highlight spatial regions in the media that contributed most to the AI-generation classification decision.</p> |
| <table> |
| <thead> |
| <tr><th>Region</th><th>Intensity</th><th>Level</th><th>Bounding Box</th></tr> |
| </thead> |
| <tbody> |
| ${gradcamTable(result.gradcam.regions)} |
| </tbody> |
| </table> |
| </section> |
| |
| <!-- 6. Generative Fingerprints & Artifacts --> |
| <section> |
| <h2>${result.type === 'video' ? '6' : '5'}. Detected Artifacts & Generative Fingerprints</h2> |
| <table> |
| <thead> |
| <tr><th>Severity</th><th>Type</th><th>Detail</th></tr> |
| </thead> |
| <tbody> |
| ${artifactRows(result.artifacts)} |
| </tbody> |
| </table> |
| </section> |
| |
| <!-- 7. Frequency Analysis (FFT) --> |
| <section> |
| <h2>${result.type === 'video' ? '7' : '6'}. Frequency Domain Analysis (FFT)</h2> |
| |
| ${result.fft.spectralAnomaly ? `<div class="anomaly-alert">⚠ Spectral anomaly detected in the frequency domain β a strong indicator of generative upsampling or GAN synthesis.</div>` : ''} |
| |
| <div class="fft-grid"> |
| <div class="fft-item"> |
| <div class="fft-item-label">Peak Frequency</div> |
| <div class="fft-item-value mono">${escapeHtml(result.fft.peakFrequency)}</div> |
| </div> |
| <div class="fft-item"> |
| <div class="fft-item-label">Anomaly Bands</div> |
| <div class="fft-item-value mono">${escapeHtml(result.fft.anomalyBands.join(', '))}</div> |
| </div> |
| <div class="fft-item" style="grid-column: span 2"> |
| <div class="fft-item-label">DCT Coefficients</div> |
| <div class="fft-item-value">${escapeHtml(result.fft.dctCoefficients)}</div> |
| </div> |
| <div class="fft-item" style="grid-column: span 2"> |
| <div class="fft-item-label">Noise Pattern</div> |
| <div class="fft-item-value">${escapeHtml(result.fft.noisePattern)}</div> |
| </div> |
| </div> |
| </section> |
| |
| <!-- 8. File Metadata --> |
| <section> |
| <h2>${result.type === 'video' ? '8' : '7'}. File Metadata</h2> |
| <table> |
| <thead><tr><th>Field</th><th>Value</th></tr></thead> |
| <tbody>${metadataRows(result.metadata)}</tbody> |
| </table> |
| </section> |
| |
| <!-- Disclaimer --> |
| <section> |
| <div class="disclaimer"> |
| <strong>Disclaimer:</strong> This report was generated automatically by UAIDE (Unified AI Origin Detection Engine) v2.4 β Research Preview. |
| All results are probabilistic in nature and based on ensemble deep learning inference. No automated system can guarantee 100% accuracy in AI-generated content detection. |
| These findings <strong>must be corroborated with expert human review</strong> before being used as evidence in any legal, academic, or journalistic context. |
| UAIDE is developed for academic research and investigative purposes. |
| <br /><br /> |
| Report generated: ${now} Β· Analysis ID: ${escapeHtml(result.analysisId)} Β· github.com/Deshna24/UAIDE |
| </div> |
| </section> |
| |
| </div> |
| |
| <!-- ββ FOOTER βββββββββββββββββββββββββββββββββββββββββββββββ --> |
| <div class="report-footer"> |
| <div><strong>UAIDE</strong> β Unified AI Origin Detection Engine Β· Research Preview v2.4</div> |
| <div>Analysis ID: <strong>${escapeHtml(result.analysisId)}</strong></div> |
| <div>github.com/Deshna24/UAIDE</div> |
| </div> |
| |
| </body> |
| </html>`; |
|
|
| return html; |
| } |
|
|
| export function downloadReport(result, previewUrl) { |
| const html = generateReport(result, previewUrl); |
| const blob = new Blob([html], { type: 'text/html;charset=utf-8' }); |
| const url = URL.createObjectURL(blob); |
| const a = document.createElement('a'); |
| a.href = url; |
| const safeName = result.filename.replace(/\.[^.]+$/, '').replace(/[^a-zA-Z0-9_-]/g, '_'); |
| a.download = `UAIDE_Report_${safeName}_${result.analysisId}.html`; |
| document.body.appendChild(a); |
| a.click(); |
| document.body.removeChild(a); |
| setTimeout(() => URL.revokeObjectURL(url), 10000); |
| } |
|
|