tebyan-medical-backend / frontend /components /analysis-history.tsx
رغد
feat: complete platform — auth, deployment, hardening
344e369
raw
history blame
34.9 kB
"use client"
import React, { useState, useEffect, useCallback, useRef } from "react"
import { motion, AnimatePresence } from "framer-motion"
import type { AnalysisResult } from "@/app/page"
import { HealthTrendChart } from "./health-trend-chart"
import { apiGet, apiPost } from "@/lib/api"
interface SavedAnalysis {
id: string
summary: string
findings: AnalysisResult["findings"]
report: AnalysisResult["report"]
created_at: string
}
interface AnalysisHistoryProps {
sessionId?: string
refreshTrigger?: number
onSelect: (a: SavedAnalysis) => void
onCompare: (a: SavedAnalysis, b: SavedAnalysis) => void
mockData?: SavedAnalysis[]
inline?: boolean
}
function formatDate(s: string) {
if (!s) return "—"
return new Date(s).toLocaleDateString("ar-SA", { day: "numeric", month: "long", year: "numeric" })
}
function formatDateShort(s: string) {
if (!s) return "—"
return new Date(s).toLocaleDateString("ar-SA", { day: "numeric", month: "short" })
}
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))
}
const STATUS = {
high: { label: "مرتفع", textCls: "text-destructive", bgCls: "bg-destructive/10", barCls: "bg-destructive" },
low: { label: "منخفض", textCls: "text-warning", bgCls: "bg-warning/10", barCls: "bg-warning" },
normal: { label: "طبيعي", textCls: "text-success", bgCls: "bg-success/10", barCls: "bg-success" },
} as const
const getS = (s: string) => STATUS[s as keyof typeof STATUS] ?? STATUS.normal
function counts(a: SavedAnalysis) {
const f = a.findings ?? []
return {
total: f.length,
high: f.filter(x => x.status === "high").length,
low: f.filter(x => x.status === "low").length,
normal: f.filter(x => x.status === "normal").length,
}
}
/* ─── Icons ─── */
function ScalesIcon({ className = "w-3.5 h-3.5" }: { className?: string }) {
return (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.8">
<path strokeLinecap="round" strokeLinejoin="round"
d="M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3" />
</svg>
)
}
function ChevronIcon({ className = "w-3.5 h-3.5" }: { className?: string }) {
return (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2.5">
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
)
}
/* ─────────────────────────────────────────────
Compare Hint
───────────────────────────────────────────── */
function CompareHint() {
return (
<div className="flex justify-center">
<div className="inline-flex items-center gap-3 px-5 py-3 rounded-full glass border border-primary/20">
<div className="w-7 h-7 rounded-full bg-primary/12 flex items-center justify-center shrink-0">
<ScalesIcon className="w-3.5 h-3.5 text-primary" />
</div>
<p className="text-sm text-muted-foreground whitespace-nowrap">
اضغط <span className="text-foreground font-medium">«قارن»</span> على أي تحليلين لمتابعة تغيّراتك الصحية
</p>
</div>
</div>
)
}
/* ─────────────────────────────────────────────
Floating Compare Bar
───────────────────────────────────────────── */
function FloatingCompareBar({
selected, onCompare, onCancel, fixed = false,
}: {
selected: SavedAnalysis[]
onCompare: () => void
onCancel: () => void
fixed?: boolean
}) {
return (
<motion.div
initial={{ y: "110%", opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: "110%", opacity: 0 }}
transition={{ type: "spring", damping: 26, stiffness: 300 }}
className={`p-3 z-10 ${fixed ? "fixed bottom-0 left-0 right-0" : "absolute bottom-0 left-0 right-0"}`}
>
<div
className="glass-strong rounded-2xl border border-border/50 overflow-hidden"
style={{ boxShadow: "0 -8px 40px oklch(0 0 0 / 0.14), 0 0 0 1px oklch(0 0 0 / 0.05)" }}
>
{selected.length === 1 ? (
<div className="px-4 py-3 flex items-center justify-between gap-3">
<button onClick={onCancel} className="text-xs text-muted-foreground/60 hover:text-muted-foreground transition-colors shrink-0">
إلغاء
</button>
<div className="text-right">
<div className="flex items-center gap-1.5 justify-end mb-0.5">
<span className="w-1.5 h-1.5 rounded-full bg-primary animate-pulse" />
<p className="text-xs font-bold text-foreground">تم تحديد تحليل واحد</p>
</div>
<p className="text-[11px] text-muted-foreground">حدد تحليلاً ثانياً لبدء المقارنة</p>
</div>
</div>
) : (
<div className="px-4 pt-3 pb-3 space-y-3">
<div className="space-y-2 text-right">
{selected.map((s, i) => (
<motion.div
key={s.id}
initial={{ opacity: 0, x: 8 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: i * 0.07 }}
className="flex items-center gap-2"
>
<div className="w-4 h-4 rounded-full bg-primary text-primary-foreground text-[9px] font-bold flex items-center justify-center shrink-0">
{i + 1}
</div>
<div className="flex-1 min-w-0">
<p className="text-xs text-foreground font-medium line-clamp-1">{s.summary}</p>
<p className="text-[10px] text-muted-foreground">{formatDateShort(s.created_at)}</p>
</div>
</motion.div>
))}
</div>
<div className="h-px bg-border/50" />
<div className="flex gap-2">
<button
onClick={onCancel}
className="px-3 py-2 rounded-xl text-xs text-muted-foreground hover:bg-muted/50 transition-colors border border-border"
>
إلغاء
</button>
<motion.button
whileTap={{ scale: 0.97 }}
onClick={onCompare}
className="flex-1 py-2 rounded-xl text-xs font-bold gradient-primary text-primary-foreground shadow-glow-primary hover:opacity-90 transition-opacity flex items-center justify-center gap-1.5"
>
<ScalesIcon className="w-3.5 h-3.5" />
قارن الآن
</motion.button>
</div>
</div>
)}
</div>
</motion.div>
)
}
/* ─────────────────────────────────────────────
Inline Detail Panel
───────────────────────────────────────────── */
function InlineDetail({
item, onUse,
}: {
item: SavedAnalysis
onUse: (a: SavedAnalysis) => void
}) {
const c = counts(item)
return (
<div className="border-t border-border/40 bg-muted/15">
<div className="px-6 pt-5 pb-5 space-y-4">
{/* Header mini */}
<div className="flex items-center justify-between text-right">
<div className="flex gap-1.5">
{c.high > 0 && (
<span className="text-[10px] px-2 py-0.5 rounded-full bg-destructive/10 text-destructive font-semibold">
{c.high} مرتفع
</span>
)}
{c.low > 0 && (
<span className="text-[10px] px-2 py-0.5 rounded-full bg-warning/10 text-warning font-semibold">
{c.low} منخفض
</span>
)}
{c.high === 0 && c.low === 0 && (
<span className="text-[10px] px-2 py-0.5 rounded-full bg-success/10 text-success font-semibold">
كل القيم طبيعية ✓
</span>
)}
</div>
<p className="text-[11px] text-muted-foreground font-medium">{formatDate(item.created_at)}</p>
</div>
{/* Stats row */}
<div className="grid grid-cols-3 gap-3">
{[
{ num: c.normal, label: "طبيعي", bg: "bg-success/8 border-success/20", text: "text-success" },
{ num: c.high, label: "مرتفع", bg: "bg-destructive/8 border-destructive/20", text: "text-destructive" },
{ num: c.low, label: "منخفض", bg: "bg-warning/8 border-warning/20", text: "text-warning" },
].map((s, i) => (
<motion.div
key={i}
initial={{ opacity: 0, scale: 0.88 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.04 + i * 0.05, duration: 0.2 }}
className={`rounded-2xl border text-center py-4 ${s.bg}`}
>
<p className={`text-2xl font-bold ${s.text}`}>{s.num}</p>
<p className={`text-xs font-semibold ${s.text} mt-0.5`}>{s.label}</p>
</motion.div>
))}
</div>
{/* Summary */}
{item.summary && (
<motion.div
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1, duration: 0.2 }}
className="rounded-2xl bg-card border border-border/50 px-5 py-4 text-right"
>
<p className="text-[11px] font-bold text-muted-foreground uppercase tracking-widest mb-2">الملخص</p>
<p className="text-sm text-foreground leading-relaxed">{item.summary}</p>
</motion.div>
)}
{/* Mini-cards grid */}
{item.findings && item.findings.length > 0 && (
<div className="grid grid-cols-2 gap-2.5">
{item.findings.map((f, i) => {
const s = getS(f.status)
const pct = calcPct(f.value, f.range ?? "")
return (
<motion.div
key={i}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 + i * 0.04, duration: 0.18 }}
className="rounded-2xl border border-border/50 overflow-hidden bg-card"
style={{
borderRightWidth: "3px",
borderRightColor: `var(--${
f.status === "high" ? "destructive" :
f.status === "low" ? "warning" : "success"
})`,
}}
>
<div className="px-4 py-3.5">
<div className="flex items-start justify-between gap-1 mb-2">
<span className={`text-[10px] font-bold px-2 py-0.5 rounded-full shrink-0 ${s.bgCls} ${s.textCls}`}>
{s.label}
</span>
<p className="text-xs font-semibold text-foreground text-right leading-snug">{f.name}</p>
</div>
<div className="flex items-baseline gap-1 justify-end mb-2">
<span className={`text-base font-bold ${s.textCls}`}>{f.value}</span>
{f.unit && <span className="text-xs text-muted-foreground">{f.unit}</span>}
</div>
{pct !== null && (
<div className="h-1.5 rounded-full bg-muted overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={{ width: `${pct}%` }}
transition={{ duration: 0.7, delay: 0.15 + i * 0.04, ease: "easeOut" }}
className={`h-full rounded-full ${s.barCls}`}
/>
</div>
)}
{f.range && (
<p className="text-[10px] text-muted-foreground/55 mt-1.5 text-right">طبيعي: {f.range}</p>
)}
</div>
</motion.div>
)
})}
</div>
)}
{(!item.findings || item.findings.length === 0) && (
<p className="text-xs text-muted-foreground text-center py-4">لا توجد بيانات فحوصات</p>
)}
{/* Use button */}
<motion.button
initial={{ opacity: 0, y: 4 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
onClick={() => onUse(item)}
className="w-full py-4 rounded-2xl text-sm font-bold gradient-primary text-primary-foreground shadow-glow-primary hover:opacity-90 transition-opacity"
>
استخدم هذا التحليل
</motion.button>
</div>
</div>
)
}
/* ─────────────────────────────────────────────
Shared Card List (used in both inline & drawer)
───────────────────────────────────────────── */
function AnalysisCardList({
analyses,
loading,
selected,
expandedId,
cardRefs,
listRef,
toggleExpand,
toggleSelect,
handleUse,
containerClassName = "space-y-2",
}: {
analyses: SavedAnalysis[]
loading: boolean
selected: SavedAnalysis[]
expandedId: string | null
cardRefs: React.RefObject<Map<string, HTMLDivElement>>
listRef: React.RefObject<HTMLDivElement | null>
toggleExpand: (id: string) => void
toggleSelect: (a: SavedAnalysis) => void
handleUse: (a: SavedAnalysis) => void
containerClassName?: string
}) {
return (
<div
ref={listRef}
className={containerClassName}
style={{
paddingBottom: selected.length > 0 ? "148px" : "0px",
transition: "padding-bottom 0.3s cubic-bezier(0.22, 1, 0.36, 1)",
}}
>
{loading && (
<div className="flex justify-center py-16">
<motion.div
animate={{ rotate: 360 }}
transition={{ duration: 1.2, repeat: Infinity, ease: "linear" }}
className="w-6 h-6 border-2 border-primary border-t-transparent rounded-full"
/>
</div>
)}
{!loading && analyses.length === 0 && (
<div className="col-span-full text-center py-14">
<p className="text-sm font-medium text-muted-foreground">لا توجد تحاليل محفوظة</p>
<p className="text-xs text-muted-foreground/55 mt-1">ارفع تحليلاً وسيُحفظ تلقائياً</p>
</div>
)}
{!loading && analyses.map((a, i) => {
const c = counts(a)
const isExpanded = expandedId === a.id
const selIdx = selected.findIndex(s => s.id === a.id)
const isChosen = selIdx !== -1
const selNum = selIdx + 1
const maxed = selected.length >= 2 && !isChosen
return (
<motion.div
key={a.id}
ref={(el: HTMLDivElement | null) => {
if (el) cardRefs.current.set(a.id, el)
else cardRefs.current.delete(a.id)
}}
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: maxed ? 0.35 : 1, y: 0 }}
transition={{ delay: i * 0.04 }}
className="rounded-3xl border overflow-hidden"
style={{
borderColor: isChosen
? "var(--primary)"
: isExpanded
? "color-mix(in oklch, var(--primary) 45%, var(--border))"
: "var(--border)",
background: isChosen
? "color-mix(in oklch, var(--primary) 5%, var(--card))"
: "var(--card)",
boxShadow: isChosen
? "0 0 0 1px color-mix(in oklch, var(--primary) 20%, transparent), 0 8px 32px -8px oklch(0 0 0 / 0.18)"
: isExpanded
? "0 8px 32px -8px oklch(0 0 0 / 0.12)"
: "0 1px 3px oklch(0 0 0 / 0.05), 0 4px 12px -4px oklch(0 0 0 / 0.07)",
transition: "border-color 0.22s, background 0.22s, box-shadow 0.28s",
}}
>
{/* Card body */}
<div className="px-6 pt-6 pb-5 text-right">
{/* Date + selection badge row */}
<div className="flex items-center justify-between gap-2 mb-3">
<div className="flex items-center gap-1.5">
{isChosen && (
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="w-5 h-5 rounded-full bg-primary text-primary-foreground text-[10px] font-bold flex items-center justify-center"
>
{selNum}
</motion.div>
)}
</div>
<span className="text-xs text-muted-foreground">{formatDate(a.created_at)}</span>
</div>
{/* Summary title */}
<p className="text-base font-bold text-foreground leading-snug mb-4">{a.summary}</p>
{/* Stats pills */}
<div className="flex gap-2 flex-wrap justify-end">
<span className="text-xs px-3 py-1 rounded-full bg-muted text-muted-foreground font-medium">
{c.total} فحص
</span>
{c.high > 0 && (
<span className="text-xs px-3 py-1 rounded-full bg-destructive/10 text-destructive font-medium">
{c.high} مرتفع
</span>
)}
{c.low > 0 && (
<span className="text-xs px-3 py-1 rounded-full bg-warning/10 text-warning font-medium">
{c.low} منخفض
</span>
)}
{c.high === 0 && c.low === 0 && (
<span className="text-xs px-3 py-1 rounded-full bg-success/10 text-success font-medium">
✓ كل القيم طبيعية
</span>
)}
</div>
</div>
{/* Divider */}
<div className="mx-6 h-px bg-border/50" />
{/* Action Row */}
<div className="px-5 py-4 flex items-center gap-3">
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.97 }}
onClick={() => toggleExpand(a.id)}
className={`flex-1 flex items-center justify-center gap-2 py-3 rounded-2xl text-sm font-semibold transition-all duration-200 ${
isExpanded
? "bg-primary/10 text-primary border border-primary/25"
: "bg-muted/60 text-foreground hover:bg-muted border border-transparent"
}`}
>
<motion.span
animate={{ rotate: isExpanded ? 180 : 0 }}
transition={{ duration: 0.28, ease: [0.22, 1, 0.36, 1] }}
className="inline-flex"
>
<ChevronIcon className="w-4 h-4" />
</motion.span>
{isExpanded ? "إخفاء التفاصيل" : "عرض التفاصيل"}
</motion.button>
<motion.button
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.95 }}
onClick={() => !maxed && toggleSelect(a)}
disabled={maxed}
className={`flex items-center gap-2 px-5 py-3 rounded-2xl text-sm font-semibold transition-all duration-200 ${
isChosen
? "bg-primary/12 text-primary border border-primary/30"
: maxed
? "text-muted-foreground/30 cursor-not-allowed border border-border/30"
: "glass text-muted-foreground hover:text-foreground border border-border/50 hover:border-border"
}`}
>
{isChosen ? (
<>
<svg className="w-3.5 h-3.5 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2.5">
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
</svg>
<span>محدد</span>
</>
) : (
<>
<ScalesIcon className="w-3.5 h-3.5 shrink-0" />
<span>{maxed ? "اكتمل" : "قارن"}</span>
</>
)}
</motion.button>
</div>
{/* Expandable Detail */}
<AnimatePresence initial={false}>
{isExpanded && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{
height: { duration: 0.38, ease: [0.22, 1, 0.36, 1] },
opacity: { duration: 0.24 },
}}
className="overflow-hidden"
>
<InlineDetail item={a} onUse={handleUse} />
</motion.div>
)}
</AnimatePresence>
</motion.div>
)
})}
</div>
)
}
/* ─────────────────────────────────────────────
Main Component
───────────────────────────────────────────── */
export function AnalysisHistory({
sessionId = "anonymous",
refreshTrigger = 0,
onSelect,
onCompare,
mockData,
inline = false,
}: AnalysisHistoryProps) {
const [analyses, setAnalyses] = useState<SavedAnalysis[]>(mockData ?? [])
const [loading, setLoading] = useState(false)
const [open, setOpen] = useState(false)
const [selected, setSelected] = useState<SavedAnalysis[]>([])
const [expandedId, setExpandedId] = useState<string | null>(null)
const [search, setSearch] = useState("")
const [searchScores, setSearchScores] = useState<Record<string, number> | null>(null)
const cardRefs = useRef<Map<string, HTMLDivElement>>(new Map())
const listRef = useRef<HTMLDivElement>(null)
const searchTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
useEffect(() => { if (mockData) setAnalyses(mockData) }, [mockData])
const fetchAnalyses = useCallback(async () => {
if (mockData) return
if (!sessionId || sessionId === "anonymous") return
setLoading(true)
try {
const r = await apiGet("/api/analyses/list", { session_id: sessionId })
if (r.ok) setAnalyses((await r.json()).analyses ?? [])
} catch {}
finally { setLoading(false) }
}, [sessionId, mockData])
useEffect(() => { if (inline && !mockData) fetchAnalyses() }, [inline, fetchAnalyses, mockData])
useEffect(() => { if (open && !mockData) fetchAnalyses() }, [open, fetchAnalyses, mockData])
useEffect(() => { if (refreshTrigger > 0 && !mockData) fetchAnalyses() }, [refreshTrigger, fetchAnalyses, mockData])
useEffect(() => {
if (searchTimer.current) clearTimeout(searchTimer.current)
if (!search.trim() || analyses.length === 0) { setSearchScores(null); return }
searchTimer.current = setTimeout(async () => {
try {
const body = {
query: search,
analyses: analyses.map(a => ({
id: a.id,
summary: a.summary,
findings_text: (a.findings ?? []).map(f => f.name).join(" "),
})),
}
const r = await apiPost("/api/search", body)
if (r.ok) {
const data = await r.json()
const scores: Record<string, number> = {}
for (const item of data.results) scores[item.id] = item.score
setSearchScores(scores)
}
} catch { setSearchScores(null) }
}, 300)
}, [search, analyses])
const toggleSelect = (a: SavedAnalysis) => {
setSelected(prev => {
const exists = prev.find(s => s.id === a.id)
if (exists) return prev.filter(s => s.id !== a.id)
if (prev.length >= 2) return prev
return [...prev, a]
})
}
const toggleExpand = (id: string) => {
setExpandedId(prev => {
const next = prev === id ? null : id
if (next) {
setTimeout(() => {
cardRefs.current.get(id)?.scrollIntoView({ behavior: "smooth", block: "nearest" })
}, 120)
}
return next
})
}
const handleCompare = () => {
if (selected.length !== 2) return
onCompare(selected[0], selected[1])
setSelected([])
setOpen(false)
setExpandedId(null)
}
const handleUse = (a: SavedAnalysis) => {
onSelect({ ...a, report: a.report ?? {} as AnalysisResult["report"] })
setOpen(false)
setSelected([])
setExpandedId(null)
}
const closeAll = () => { setOpen(false); setSelected([]); setExpandedId(null) }
if (!mockData && !inline && (!sessionId || sessionId === "anonymous")) return null
/* ── INLINE MODE (mockData provided, or inline=true with real session) ── */
if (mockData || inline) {
const filtered = search.trim()
? searchScores
? analyses
.filter(a => (searchScores[a.id] ?? 0) > 0)
.sort((a, b) => (searchScores[b.id] ?? 0) - (searchScores[a.id] ?? 0))
: analyses.filter(a =>
a.summary.includes(search) || formatDate(a.created_at).includes(search)
)
: analyses
return (
<div className="w-full relative" dir="rtl">
{/* Search */}
<div className="flex justify-center mb-10">
<div className="relative w-full max-w-2xl">
<svg className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
</svg>
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="ابحث في سجل التحاليل..."
className="w-full pr-11 pl-11 py-3.5 rounded-2xl glass text-sm text-foreground placeholder:text-muted-foreground focus:outline-none border border-border/60 focus:border-primary/50 transition-colors"
/>
{search ? (
<button
onClick={() => setSearch("")}
className="absolute left-4 top-1/2 -translate-y-1/2 w-6 h-6 rounded-full bg-muted flex items-center justify-center text-muted-foreground hover:text-foreground hover:bg-muted/70 transition-colors"
>
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
) : (
<kbd className="absolute left-4 top-1/2 -translate-y-1/2 hidden sm:flex items-center gap-1 px-2 py-0.5 rounded-md bg-muted/80 text-muted-foreground text-[10px] font-medium border border-border/40">
⌘K
</kbd>
)}
</div>
</div>
{/* Compare Hint */}
<AnimatePresence>
{selected.length === 0 && analyses.length > 1 && (
<motion.div
initial={{ opacity: 0, y: -6 }}
animate={{ opacity: 1, y: 0 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.22 }}
className="overflow-hidden mb-8"
>
<CompareHint />
</motion.div>
)}
</AnimatePresence>
<AnalysisCardList
analyses={filtered}
loading={loading}
selected={selected}
expandedId={expandedId}
cardRefs={cardRefs}
listRef={listRef}
toggleExpand={toggleExpand}
toggleSelect={toggleSelect}
handleUse={handleUse}
containerClassName="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
/>
{analyses.length >= 2 && (
<div className="mt-12 max-w-3xl mx-auto">
<HealthTrendChart analyses={analyses} />
</div>
)}
{/* Floating Compare Bar — fixed to bottom of viewport in inline mode */}
<AnimatePresence>
{selected.length > 0 && (
<FloatingCompareBar
selected={selected}
onCompare={handleCompare}
onCancel={() => setSelected([])}
fixed={true}
/>
)}
</AnimatePresence>
</div>
)
}
/* ── DRAWER MODE (normal usage with sessionId) ── */
return (
<>
{/* Trigger button */}
<button
onClick={() => setOpen(true)}
className="flex items-center gap-2 px-3 py-1.5 rounded-xl glass text-sm font-medium text-foreground hover:bg-muted/60 transition-colors"
>
<svg className="w-4 h-4 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="1.8">
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" />
</svg>
سجل التحاليل
{analyses.length > 0 && (
<span className="w-5 h-5 rounded-full bg-primary/15 text-primary text-xs flex items-center justify-center font-bold">
{analyses.length}
</span>
)}
</button>
<AnimatePresence>
{open && (
<>
<motion.div
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-40"
style={{ background: "oklch(0 0 0 / 0.52)", backdropFilter: "blur(8px)", WebkitBackdropFilter: "blur(8px)" }}
onClick={closeAll}
/>
<motion.div
initial={{ x: "-100%" }}
animate={{ x: 0 }}
exit={{ x: "-100%" }}
transition={{ type: "spring", damping: 28, stiffness: 280 }}
className="fixed top-0 left-0 h-full z-50 flex flex-col bg-card"
style={{
width: "min(440px, 96vw)",
borderRight: "1px solid var(--border)",
boxShadow: "8px 0 48px oklch(0 0 0 / 0.20), 1px 0 0 var(--border)",
}}
>
<div className="flex items-center justify-between px-5 py-4 border-b border-border shrink-0">
<button onClick={closeAll} className="w-8 h-8 rounded-xl bg-muted flex items-center justify-center hover:bg-muted/70 transition-colors">
<svg className="w-4 h-4 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div className="text-right">
<h2 className="text-sm font-bold text-foreground">سجل التحاليل</h2>
{analyses.length > 0 && <p className="text-[11px] text-muted-foreground">{analyses.length} تحليل محفوظ</p>}
</div>
</div>
<AnimatePresence>
{selected.length === 0 && !loading && analyses.length > 1 && (
<motion.div
initial={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.22 }}
className="overflow-hidden shrink-0"
>
<CompareHint />
</motion.div>
)}
</AnimatePresence>
<div className="flex-1 overflow-y-auto px-4 py-3 relative">
<AnalysisCardList
analyses={analyses}
loading={loading}
selected={selected}
expandedId={expandedId}
cardRefs={cardRefs}
listRef={listRef}
toggleExpand={toggleExpand}
toggleSelect={toggleSelect}
handleUse={handleUse}
/>
{analyses.length >= 2 && (
<div className="px-1 pb-4">
<HealthTrendChart analyses={analyses} />
</div>
)}
</div>
<AnimatePresence>
{selected.length > 0 && (
<FloatingCompareBar
selected={selected}
onCompare={handleCompare}
onCancel={() => setSelected([])}
/>
)}
</AnimatePresence>
</motion.div>
</>
)}
</AnimatePresence>
</>
)
}