Spaces:
Sleeping
Sleeping
File size: 7,684 Bytes
7d3b88b | 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 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | import { motion, AnimatePresence } from 'framer-motion'
/* ββ Ease ββββββββββββββββββββββββββββββββββββββββββββββββ */
const EASE_OUT = [0.25, 0.46, 0.45, 0.94]
/* ββ Badge config βββββββββββββββββββββββββββββββββββββββ */
const DECISION_CONF = {
needs_search: { label: 'Search', color: 'text-blue', bg: 'bg-blue-soft', icon: 'β' },
direct_answer: { label: 'Direct', color: 'text-emerald', bg: 'bg-emerald-soft', icon: 'β―' },
memory_sufficient:{ label: 'Memory', color: 'text-accent', bg: 'bg-accent-soft', icon: 'β' },
}
const CONFIDENCE_WIDTH = { high: '100%', medium: '60%', low: '28%' }
/* ββ Stat row βββββββββββββββββββββββββββββββββββββββββββ */
function Stat({ label, value }) {
return (
<div className="flex items-center justify-between py-2">
<span className="text-text-muted text-[11px]">{label}</span>
<span className="text-text-secondary text-[11px] font-medium tabular-nums">{value}</span>
</div>
)
}
/* ββ Plan steps βββββββββββββββββββββββββββββββββββββββββ */
function PlanTimeline({ steps }) {
if (!steps?.length) return null
return (
<div>
<p className="text-text-muted text-[10px] uppercase tracking-[0.1em] font-medium mb-3">
Execution plan
</p>
<div className="relative pl-4">
<div className="absolute left-[5px] top-1 bottom-1 w-px bg-border" />
{steps.map((step, i) => (
<motion.div
key={i}
className="relative flex items-start gap-3 pb-3 last:pb-0"
initial={{ opacity: 0, x: -5 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.05 * i, duration: 0.3, ease: EASE_OUT }}
>
<div className="absolute -left-4 top-[5px] w-[6px] h-[6px] rounded-full bg-surface
border border-border" />
<span className="text-text-muted text-[11px] leading-relaxed">{step}</span>
</motion.div>
))}
</div>
</div>
)
}
/* ββ Main panel βββββββββββββββββββββββββββββββββββββββββ */
export default function AgentPanel({ isOpen, onClose, metadata }) {
if (!metadata) return null
const decision = DECISION_CONF[metadata.decision] || DECISION_CONF.needs_search
const conf = metadata.confidence || 'medium'
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop */}
<motion.div
className="fixed inset-0 bg-black/8"
style={{ zIndex: 40 }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
onClick={onClose}
/>
{/* Panel */}
<motion.aside
className="fixed top-0 right-0 h-full w-[320px] bg-surface-white
border-l border-border flex flex-col shadow-xl shadow-black/5"
style={{ zIndex: 50 }}
initial={{ x: '100%' }}
animate={{ x: 0 }}
exit={{ x: '100%' }}
transition={{ type: 'spring', damping: 30, stiffness: 280 }}
>
{/* Header */}
<div className="flex items-center justify-between px-5 py-4 border-b border-border-subtle">
<div className="flex items-center gap-2">
<div className="w-[6px] h-[6px] rounded-full bg-accent" />
<h2 className="text-[13px] font-semibold text-text-primary">Insights</h2>
</div>
<motion.button
onClick={onClose}
className="w-6 h-6 rounded-md flex items-center justify-center
text-text-dim hover:text-text-muted hover:bg-surface-hover
transition-all duration-200 text-xs"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
aria-label="Close"
>
β
</motion.button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto px-5 py-5 space-y-5">
{/* Decision + confidence */}
<div className="space-y-3">
<div className="flex items-center gap-2">
<div className={`px-2.5 py-1 rounded-lg ${decision.bg} flex items-center gap-1.5`}>
<span className="text-[11px]">{decision.icon}</span>
<span className={`text-[11px] font-semibold ${decision.color}`}>
{decision.label}
</span>
</div>
<span className="text-text-ghost text-[10px]">routing</span>
</div>
<div>
<div className="flex items-center justify-between mb-1.5">
<span className="text-text-muted text-[10px] uppercase tracking-[0.1em]">
Confidence
</span>
<span className="text-text-secondary text-[11px] font-medium capitalize">
{conf}
</span>
</div>
<div className="h-[3px] rounded-full bg-surface-warm overflow-hidden">
<motion.div
className="h-full rounded-full bg-accent"
initial={{ width: 0 }}
animate={{ width: CONFIDENCE_WIDTH[conf] || '50%' }}
transition={{ duration: 0.6, ease: EASE_OUT }}
/>
</div>
</div>
</div>
<div className="h-px bg-border-subtle" />
{/* Metrics */}
<div>
<p className="text-text-muted text-[10px] uppercase tracking-[0.1em] font-medium mb-1">
Metrics
</p>
<div className="divide-y divide-border-subtle">
<Stat label="Steps taken" value={metadata.steps_taken ?? 0} />
<Stat label="Tools used" value={metadata.tools_used?.length ? metadata.tools_used.join(', ') : 'β'} />
<Stat label="LLM calls" value={metadata.llm_calls ?? 0} />
<Stat label="Memory hits" value={metadata.memory_hits ?? 0} />
<Stat label="Refinements" value={metadata.refinements ?? 0} />
<Stat label="Cache hits" value={metadata.cache_hits ?? 0} />
{(metadata.steps_skipped > 0 || metadata.early_stopped) && (
<>
<Stat label="Steps skipped" value={metadata.steps_skipped ?? 0} />
<Stat label="Early stopped" value={metadata.early_stopped ? 'Yes' : 'No'} />
</>
)}
</div>
</div>
<div className="h-px bg-border-subtle" />
<PlanTimeline steps={metadata.plan} />
</div>
<div className="px-5 py-3 border-t border-border-subtle">
<p className="text-[10px] text-text-ghost text-center">
{metadata.source || 'planner_executor'}
</p>
</div>
</motion.aside>
</>
)}
</AnimatePresence>
)
}
|