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" && (
)}
);
}