"use client"
import { useState, useEffect } from "react"
import { motion, AnimatePresence } from "framer-motion"
import type { AnalysisResult } from "@/app/page"
interface SavedAnalysis {
id: string
summary: string
findings: AnalysisResult["findings"]
report: AnalysisResult["report"]
created_at: string
}
interface CompareAnalysesProps {
a: SavedAnalysis
b: SavedAnalysis
onClose: () => void
}
function formatDate(d: string) {
if (!d) return "—"
return new Date(d).toLocaleDateString("ar-SA", { day: "numeric", month: "short", year: "numeric" })
}
const STATUS_CFG = {
normal: { label: "طبيعي", cls: "text-success bg-success/10", dot: "bg-success" },
high: { label: "مرتفع", cls: "text-destructive bg-destructive/10", dot: "bg-destructive" },
low: { label: "منخفض", cls: "text-warning bg-warning/10", dot: "bg-warning" },
} as const
function getStatusCfg(s: string) {
return STATUS_CFG[s as keyof typeof STATUS_CFG] ?? STATUS_CFG.normal
}
function getTrend(rowA: AnalysisResult["findings"][0] | null, rowB: AnalysisResult["findings"][0] | null) {
if (!rowA || !rowB) return null
const vA = parseFloat(rowA.value)
const vB = parseFloat(rowB.value)
if (isNaN(vA) || isNaN(vB)) return null
const improved = rowA.status !== "normal" && rowB.status === "normal"
const worsened = rowA.status === "normal" && rowB.status !== "normal"
const diff = vB - vA
const deltaPct = vA !== 0 ? Math.abs((diff / vA) * 100) : 0
if (Math.abs(diff) < 0.001) return { icon: "—", colorCls: "text-muted-foreground", direction: "stable" as const, deltaPct: 0 }
return {
icon: diff > 0 ? "↑" : "↓",
colorCls: improved ? "text-success" : worsened ? "text-destructive" : "text-muted-foreground",
direction: diff > 0 ? ("up" as const) : ("down" as const),
deltaPct: Math.round(deltaPct),
}
}
function calcPct(value: string, range: string) {
const nums = range?.match(/[\d.]+/g)
if (!nums || nums.length < 2) return null
const lo = parseFloat(nums[0]), hi = parseFloat(nums[1]), val = parseFloat(value)
if (isNaN(val) || isNaN(lo) || isNaN(hi) || hi === lo) return null
return Math.max(4, Math.min(96, ((val - lo) / (hi - lo)) * 100))
}
/* ─── Skeleton pieces ─── */
function Sk({ w = "100%", h = 16, rounded = "rounded-xl" }: { w?: string | number; h?: number; rounded?: string }) {
return (
)
}
function CompareSkeleton() {
return (
{/* Header skeleton */}
{/* Stat cards */}
{[0, 1, 2].map(i => (
))}
{/* Column headers */}
{/* Table rows */}
{[100, 85, 90, 75, 95, 80].map((w, i) => (
))}
{/* Footer */}
)
}
/* ─── Real content ─── */
const BACKEND = process.env.NEXT_PUBLIC_BACKEND_URL ?? "http://localhost:8000"
function CompareContent({
a, b, onClose,
}: {
a: SavedAnalysis
b: SavedAnalysis
onClose: () => void
}) {
const [groqSummary, setGroqSummary] = useState(null)
const [summaryLoading, setSummaryLoading] = useState(true)
const dateA = formatDate(a.created_at)
const dateB = formatDate(b.created_at)
useEffect(() => {
setSummaryLoading(true)
fetch(`${BACKEND}/api/compare/summary`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
findings_a: a.findings ?? [],
findings_b: b.findings ?? [],
summary_a: a.summary ?? "",
summary_b: b.summary ?? "",
date_a: dateA,
date_b: dateB,
}),
})
.then(r => r.ok ? r.json() : Promise.reject())
.then(d => setGroqSummary(d.summary ?? null))
.catch(() => setGroqSummary(null))
.finally(() => setSummaryLoading(false))
}, [a.id, b.id])
const mapA = Object.fromEntries((a.findings ?? []).map(f => [f.name, f]))
const mapB = Object.fromEntries((b.findings ?? []).map(f => [f.name, f]))
const allNames = [...new Set([...Object.keys(mapA), ...Object.keys(mapB)])]
const rows = allNames.map(name => ({ name, a: mapA[name] ?? null, b: mapB[name] ?? null }))
const improved = rows.filter(r => r.a?.status !== "normal" && r.b?.status === "normal")
const worsened = rows.filter(r => r.a?.status === "normal" && r.b?.status !== "normal")
const stable = rows.filter(r => r.a?.status === r.b?.status)
const summaryStats = [
{ num: improved.length, label: "تحسّن", icon: "↑", bg: "bg-success/8", border: "border-success/20", text: "text-success" },
{ num: worsened.length, label: "تراجع", icon: "!", bg: "bg-destructive/8", border: "border-destructive/20", text: "text-destructive" },
{ num: stable.length, label: "مستقر", icon: "→", bg: "bg-muted/60", border: "border-border", text: "text-muted-foreground" },
]
const insights = [
...improved.map(r => ({
type: "success" as const,
icon: "↑",
text: `تحسّن مستوى ${r.name} من ${r.a?.value ?? "—"} إلى ${r.b?.value ?? "—"} ${r.b?.unit ?? ""}`,
})),
...worsened.map(r => ({
type: "warning" as const,
icon: "!",
text: `تراجع مستوى ${r.name} — يُنصح بمراجعة الطبيب`,
})),
]
return (
{/* ── Header ── */}
مقارنة التحاليل
{worsened.length > 0 && (
{worsened.length} تحتاج متابعة
)}
{improved.length > 0 && (
{improved.length} تحسّنت ✓
)}
{dateA} مقابل {dateB}
{/* ── Summary Stats ── */}
{summaryStats.map((s, i) => (
{s.num}
{s.label} {s.icon}
))}
{/* ── Analysis Column Labels ── */}
{[
{ label: "التحليل الأول", date: dateA, summary: a.summary, accent: "border-primary/30 bg-primary/5" },
{ label: "التحليل الثاني", date: dateB, summary: b.summary, accent: "border-secondary/30 bg-secondary/5" },
].map((col, i) => (
{col.label}
{col.date}
{col.summary}
))}
{/* ── Comparison Rows ── */}
{rows.map((row, i) => {
const cfgA = row.a ? getStatusCfg(row.a.status) : null
const cfgB = row.b ? getStatusCfg(row.b.status) : null
const trend = getTrend(row.a, row.b)
const pctA = row.a ? calcPct(row.a.value, row.a.range ?? "") : null
const pctB = row.b ? calcPct(row.b.value, row.b.range ?? "") : null
const isWorsened = row.a?.status === "normal" && row.b?.status !== "normal"
return (
{/* Test name */}
{row.name}
{(row.a?.unit || row.b?.unit) && (
{row.a?.unit ?? row.b?.unit}
)}
{(row.a?.range || row.b?.range) && (
طبيعي: {row.a?.range ?? row.b?.range}
)}
{/* Value A */}
{row.a ? (
{row.a.value}
{cfgA!.label}
{pctA !== null && (
)}
) : (
—
)}
{/* Value B + trend + delta % */}
{row.b ? (
{row.b.value}
{trend && trend.direction !== "stable" && (
{trend.icon}
)}
{trend && trend.direction !== "stable" && trend.deltaPct > 0 && (
{trend.deltaPct}%
)}
{cfgB!.label}
{pctB !== null && (
)}
) : (
—
)}
)
})}
{/* ── Groq AI Summary ── */}
ملخص ذكي مقارَن
{summaryLoading ? (
) : groqSummary ? (
{groqSummary}
) : null}
{/* ── Insights Section ── */}
{insights.length > 0 && (
تفاصيل التغيرات
{insights.map((ins, i) => (
{ins.icon}
{ins.text}
))}
)}
{/* ── Footer ── */}
{rows.length} فحص مقارَن
)
}
/* ─── Main export ─── */
export function CompareAnalyses({ a, b, onClose }: CompareAnalysesProps) {
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
const t = setTimeout(() => setIsLoading(false), 700)
return () => clearTimeout(t)
}, [])
return (
e.stopPropagation()}
className="w-full max-w-3xl max-h-[90vh] flex flex-col rounded-3xl overflow-hidden bg-card"
style={{ boxShadow: "0 40px 96px -16px oklch(0 0 0 / 0.32), 0 0 0 1px oklch(0 0 0 / 0.07)" }}
>
{isLoading
?
:
}
)
}