Spaces:
Running
Running
File size: 4,888 Bytes
6a2c89b 349f22c 6a2c89b deb8696 415d3b5 5e98b5a 6a2c89b 0e2c208 6a2c89b e83084c 6a2c89b e83084c 6a2c89b e83084c 6a2c89b e83084c 6a2c89b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | 'use client';
import { useEffect, useState, useCallback } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Warning as AlertTriangle, Info, WarningCircle as AlertCircle, X, CaretUp as ChevronUp } from '@phosphor-icons/react';
import type { AuditInsight } from '@/types/audit';
const SEVERITY_CONFIG = {
info: { border: 'border-accent-cyan/40', bg: 'bg-accent-cyan/5', icon: Info, glow: '0 1px 2px rgba(0,0,0,0.35), 0 10px 24px rgba(74,222,128,0.08)' },
warning: { border: 'border-yellow-500/40', bg: 'bg-yellow-500/5', icon: AlertTriangle, glow: '0 1px 2px rgba(0,0,0,0.35), 0 10px 24px rgba(234,179,8,0.08)' },
critical: { border: 'border-error/40', bg: 'bg-error-dim/5', icon: AlertCircle, glow: '0 1px 2px rgba(0,0,0,0.35), 0 10px 24px rgba(239,68,68,0.08)' },
} as const;
export function AuditInsightPanel({ sessionId }: { sessionId: string }) {
const [insight, setInsight] = useState<AuditInsight | null>(null);
const [open, setOpen] = useState(false);
const [dismissed, setDismissed] = useState(false);
useEffect(() => {
if (!sessionId) return;
const poll = async () => {
try {
const r = await fetch(`/api/backend/api/audit/insights?session=${sessionId}`);
if (!r.ok) return;
const data = await r.json();
if (data.latest) {
const newInsight = data.latest as AuditInsight;
setInsight((prev) => {
if (prev?.id !== newInsight.id) {
if (!open) setOpen(true);
return newInsight;
}
return prev;
});
}
} catch {}
};
poll();
const interval = setInterval(poll, 10_000);
return () => clearInterval(interval);
}, [sessionId, open]);
const handleClose = useCallback(() => {
setOpen(false);
setDismissed(true);
}, []);
if (dismissed && !open) return null;
if (!insight) return null;
const cfg = SEVERITY_CONFIG[insight.severity] ?? SEVERITY_CONFIG.info;
const Icon = cfg.icon;
return (
<AnimatePresence>
{open ? (
<motion.div
key="panel"
initial={{ y: 80, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
exit={{ y: 80, opacity: 0 }}
transition={{ type: 'spring', damping: 28, stiffness: 260 }}
className={`fixed bottom-6 right-6 z-50 w-80 p-4 rounded-xl border-l-4 ${cfg.border} ${cfg.bg}`}
style={{
background: 'var(--glass-panel-bg)',
backdropFilter: 'blur(20px)',
boxShadow: `var(--shadow-float), ${cfg.glow}`,
border: '1px solid rgb(var(--glass-border) / var(--glass-border-a))',
}}
>
<div className="flex items-start justify-between gap-2 mb-2">
<div className="flex items-center gap-2">
<Icon size={14} className={cfg.border.replace('border-', 'text-').replace('/40', '')} />
<span className="text-xs font-semibold uppercase tracking-wider text-text-muted">AI Audit</span>
</div>
<button onClick={handleClose} className="text-text-muted hover:text-text-primary transition-colors">
<X size={14} />
</button>
</div>
<p className="text-xs text-text-primary leading-relaxed">{insight.insight}</p>
{insight.suggestion && (
<p className="text-xs mt-2 text-text-muted leading-relaxed">
<span className="text-accent-cyan mr-1">→</span>
{insight.suggestion}
</p>
)}
{insight.affected_steps.length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-2">
{insight.affected_steps.map((s) => (
<span key={s} className="text-[10px] px-2 py-0.5 rounded-full bg-surface-2/60 text-text-muted font-mono">
{s}
</span>
))}
</div>
)}
</motion.div>
) : (
<motion.button
key="badge"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
exit={{ scale: 0 }}
whileHover={{ scale: 1.05 }}
onClick={() => { setOpen(true); setDismissed(false); }}
className="fixed bottom-6 right-6 z-50 flex items-center gap-2 px-3 py-2 rounded-full"
style={{
background: 'var(--glass-panel-bg)',
border: '1px solid rgb(var(--glass-border) / var(--glass-border-a))',
backdropFilter: 'blur(12px)',
boxShadow: cfg.glow,
}}
>
<Icon size={13} className={cfg.border.replace('border-', 'text-').replace('/40', '')} />
<span className="text-xs font-medium text-text-primary">Audit</span>
<ChevronUp size={11} className="text-text-muted" />
</motion.button>
)}
</AnimatePresence>
);
}
|