import { useEffect, useState } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { useAnalysis, useDeleteAnalysis } from "../hooks/useAnalysis"; import { useAnalysisStore, type AnalysisTab } from "../stores/analysisStore"; import { getAnalysisImageUrl, checkAIInsightsAvailable, generateAIInsights } from "../api/analysis"; import { AuthImage } from "../components/AuthImage"; import type { Insight } from "../types/analysis"; function getScoreInfo(score: number) { if (score >= 55) return { label: "High", color: "#16a34a", bg: "#f0fdf4", desc: "Strong design effectiveness — clear visual hierarchy with intentional attention flow" }; if (score >= 35) return { label: "Medium", color: "#d97706", bg: "#fffbeb", desc: "Moderate effectiveness — some competing elements dilute the primary focus" }; return { label: "Low", color: "#dc2626", bg: "#fef2f2", desc: "Low effectiveness — attention is scattered without clear hierarchy" }; } function ScoreCard({ score, entropyScore }: { score: number; entropyScore?: number }) { const info = getScoreInfo(score); const [showTooltip, setShowTooltip] = useState(false); return (
setShowTooltip(true)} onMouseLeave={() => setShowTooltip(false)} >
{score.toFixed(0)} /100
Design Effectiveness {info.label}
{showTooltip && (

Design Effectiveness Score

{info.desc}

55-100: High — clear hierarchy, dominant focal point, coherent gaze flow
35-54: Medium — some competing elements dilute focus
0-34: Low — scattered attention, no clear hierarchy

Composite of peak dominance, hierarchy clarity, gaze coherence, and entropy concentration.

{entropyScore !== undefined && (
Entropy Concentration: {entropyScore.toFixed(0)}/100

Raw measure of how spatially concentrated attention is. Lower values mean attention spreads across more of the image.

)}
)}
); } function ZoomableImage({ src, alt }: { src: string; alt: string }) { const zoom = useAnalysisStore((s) => s.zoom); return (
); } function ZoomControls() { const zoom = useAnalysisStore((s) => s.zoom); const setZoom = useAnalysisStore((s) => s.setZoom); return (
{Math.round(zoom * 100)}% {zoom !== 1 && ( )}
); } import { downloadReport } from "../api/reports"; import HeatmapOverlay from "../components/analysis/HeatmapOverlay"; import HeatmapControls from "../components/analysis/HeatmapControls"; import GazeSequence from "../components/analysis/GazeSequence"; import HotspotList from "../components/analysis/HotspotList"; import InsightsPanel from "../components/analysis/InsightsPanel"; import AOICanvas from "../components/aoi/AOICanvas"; import AOIResults from "../components/aoi/AOIResults"; import Card from "../components/common/Card"; import Button from "../components/common/Button"; import LoadingSpinner from "../components/common/LoadingSpinner"; const tabs: { key: AnalysisTab; label: string }[] = [ { key: "heatmap", label: "Heatmap" }, { key: "gaze", label: "Gaze Sequence" }, { key: "hotspots", label: "Hotspots" }, { key: "aoi", label: "AOI Analysis" }, ]; export default function AnalysisView() { const { analysisId } = useParams<{ analysisId: string }>(); const navigate = useNavigate(); const { data: analysis, isLoading, error } = useAnalysis(analysisId); const deleteAnalysisMutation = useDeleteAnalysis(); const activeTab = useAnalysisStore((s) => s.activeTab); const setActiveTab = useAnalysisStore((s) => s.setActiveTab); const setCurrentAnalysis = useAnalysisStore((s) => s.setCurrentAnalysis); const aoiResults = useAnalysisStore((s) => s.aoiResults); const reset = useAnalysisStore((s) => s.reset); const [aiAvailable, setAiAvailable] = useState(false); const [aiInsights, setAiInsights] = useState([]); const [aiScore, setAiScore] = useState(null); const [aiScoreReason, setAiScoreReason] = useState(null); const [aiCostUsd, setAiCostUsd] = useState(null); const [aiLoading, setAiLoading] = useState(false); const [aiError, setAiError] = useState(null); useEffect(() => { checkAIInsightsAvailable().then(setAiAvailable); }, []); // Load saved AI insights from DB when analysis loads useEffect(() => { if (analysis?.ai_insights && analysis.ai_insights.length > 0) { setAiInsights(analysis.ai_insights); setAiScore(analysis.ai_score ?? null); setAiScoreReason(analysis.ai_score_reason ?? null); setAiCostUsd(analysis.ai_cost_usd ?? null); } }, [analysis]); const handleGenerateAI = async () => { if (!analysisId) return; setAiLoading(true); setAiError(null); try { const result = await generateAIInsights(analysisId); setAiInsights(result.insights); setAiScore(result.ai_score); setAiScoreReason(result.score_reason); setAiCostUsd(result.cost_usd); } catch (err: any) { setAiError(err?.response?.data?.detail || "AI analysis failed"); } finally { setAiLoading(false); } }; useEffect(() => { if (analysis) { setCurrentAnalysis(analysis); } return () => { reset(); }; }, [analysis, setCurrentAnalysis, reset]); const handleDeleteAnalysis = async () => { if (!analysisId) return; if (!window.confirm("Are you sure you want to delete this analysis? This cannot be undone.")) return; await deleteAnalysisMutation.mutateAsync(analysisId); navigate(-1); }; const handleDownloadPdf = async () => { if (!analysisId) return; try { await downloadReport(analysisId); } catch (err) { console.error("Failed to download report:", err); } }; if (isLoading) { return ; } if (error || !analysis) { return (

Failed to load analysis. It may not exist or is still processing.

); } const originalUrl = getAnalysisImageUrl(analysis.id, "original"); const saliencyUrl = getAnalysisImageUrl(analysis.id, "saliency-raw"); const gazeUrl = getAnalysisImageUrl(analysis.id, "gaze-sequence"); return (
{/* Header */}

{analysis.name}

Model: {(analysis.model_used || analysis.model).replace(/_/g, " ").replace(/\b\w/g, c => c.toUpperCase())} {new Date(analysis.created_at).toLocaleDateString()}
{analysis.overall_score !== undefined && (
)}
{aiAvailable && analysis.status === "completed" && aiInsights.length === 0 && ( )}
{/* Tabs + Zoom */}
{tabs.map((tab) => ( ))}
{/* Insights */} {((analysis.insights && analysis.insights.length > 0) || aiInsights.length > 0) && ( )} {aiError && (
{aiError}
)} {/* Tab content */} {activeTab === "heatmap" && (
)} {activeTab === "gaze" && ( ({ rank: p.rank, x: p.x, y: p.y, intensity: p.probability ?? p.intensity ?? 0, })) } /> )} {activeTab === "hotspots" && (
)} {activeTab === "aoi" && (
)}
); }