Correspondents/src/components/pinned-agents-drawer.tsx

211 lines
9.5 KiB
TypeScript

"use client"
import { useState, useEffect } from "react"
import { motion, AnimatePresence, Reorder } from "framer-motion"
import { Trash2, MessageSquare, GripVertical } from "lucide-react"
import type { PinnedAgent } from "@/lib/types"
import { Button } from "@/components/ui/button"
interface PinnedAgentsDrawerProps {
isOpen: boolean
onClose: () => void
onSelectAgent: (agent: PinnedAgent) => void
activeAgentId?: string // Highlight the currently active agent
}
export function PinnedAgentsDrawer({ isOpen, onClose, onSelectAgent, activeAgentId }: PinnedAgentsDrawerProps) {
const [agents, setAgents] = useState<PinnedAgent[]>([])
const [isMobile, setIsMobile] = useState(true) // Assume mobile until we can check
const [draggingId, setDraggingId] = useState<string | null>(null)
// Detect mobile vs desktop
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 640)
}
// Check immediately on mount
if (typeof window !== "undefined") {
checkMobile()
}
window.addEventListener("resize", checkMobile)
return () => window.removeEventListener("resize", checkMobile)
}, [])
// Load pinned agents from localStorage
useEffect(() => {
if (isOpen) {
const stored = localStorage.getItem("pinned-agents")
if (stored) {
try {
const parsed = JSON.parse(stored)
setTimeout(() => {
setAgents(parsed)
}, 0)
} catch (error) {
console.error("Failed to parse pinned agents:", error)
}
}
}
}, [isOpen])
// Save agents to localStorage when order changes
const handleReorder = (newOrder: PinnedAgent[]) => {
setAgents(newOrder)
localStorage.setItem("pinned-agents", JSON.stringify(newOrder))
}
// Remove agent from pinned list
const handleRemove = (agentId: string) => {
const updated = agents.filter((a) => a.agentId !== agentId)
setAgents(updated)
localStorage.setItem("pinned-agents", JSON.stringify(updated))
}
// Start chat with selected agent
const handleStartChat = (agent: PinnedAgent) => {
onSelectAgent(agent)
onClose()
}
return (
<AnimatePresence>
{isOpen && (
<>
{/* Backdrop - mobile only */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
className="fixed inset-0 z-40 bg-black/40 backdrop-blur-sm sm:hidden"
/>
{/* Drawer */}
<motion.div
initial={isMobile ? { y: 60, opacity: 0 } : { x: -100, opacity: 0 }}
animate={isMobile ? { y: 0, opacity: 1 } : { x: 0, opacity: 0.8 }}
exit={isMobile ? { y: 60, opacity: 0 } : { x: -200, opacity: 0 }}
transition={{
type: "spring",
damping: 32,
stiffness: 300,
opacity: { duration: 0.28, ease: "easeIn" }
}}
className="fixed inset-x-0 bottom-0 z-50 max-h-[85vh] w-full overflow-hidden rounded-t-[2rem] border-t border-white/20 bg-gradient-to-br from-charcoal-ink/98 to-sage-concrete/98 shadow-2xl backdrop-blur-[28px] sm:absolute sm:bottom-auto sm:left-[calc(100%-80px)] sm:right-auto sm:top-1/2 sm:z-[5] sm:-translate-y-1/2 sm:h-[calc(100%-3rem)] sm:max-h-none sm:w-[28rem] sm:rounded-[1.5rem] sm:border-none sm:bg-[rgba(60,60,60,0.95)] sm:shadow-[0_8px_20px_rgba(0,0,0,0.25)] sm:backdrop-blur-none"
>
{/* Drag handle indicator - mobile only */}
<div className="flex justify-center py-3 sm:hidden">
<div className="h-1 w-12 rounded-full bg-white/20" />
</div>
{/* Header */}
<div className="border-b border-white/10 px-6 py-4 sm:border-b-0 sm:pl-20 sm:pr-6 sm:py-4">
<h2 className="font-heading text-lg text-white sm:text-[1.25rem]">Pinned Agents</h2>
<p className="mt-1 text-xs text-white/60 sm:text-xs">
{agents.length} {agents.length === 1 ? "correspondent" : "correspondents"}
</p>
</div>
{/* Agent list */}
<div
className="h-full overflow-y-auto px-6 py-4 sm:pl-20 sm:pr-6 sm:py-5"
style={isMobile ? { maxHeight: "calc(85vh - 8rem)" } : undefined}
>
{agents.length === 0 ? (
<div className="flex min-h-[40vh] flex-col items-center justify-center text-center py-12 sm:min-h-[30vh]">
<div className="mb-4 flex h-20 w-20 items-center justify-center rounded-full bg-white/5">
<MessageSquare className="h-10 w-10 text-white/40" />
</div>
<p className="text-lg text-white/70">No pinned agents yet</p>
<p className="mt-2 text-sm text-white/50 max-w-md">
Ask Morgan to create a custom agent and pin it for quick access
</p>
</div>
) : (
<Reorder.Group
axis="y"
values={agents}
onReorder={handleReorder}
className="space-y-3 sm:space-y-4"
>
{agents.map((agent) => (
<Reorder.Item
key={agent.agentId}
value={agent}
onDragStart={() => setDraggingId(agent.agentId)}
onDragEnd={() => setDraggingId(null)}
>
<motion.div
layout
style={{ zIndex: draggingId === agent.agentId ? 20 : 1 }}
className={`group relative overflow-hidden rounded-xl border border-white/15 bg-white/10 p-3 shadow-sm backdrop-blur-sm transition-all duration-300 hover:border-white/25 hover:bg-white/15 hover:shadow-md sm:rounded-2xl sm:p-3 ${
draggingId === agent.agentId ? "shadow-[0_15px_35px_rgba(45,45,45,0.2)]" : ""
}`}
>
{/* Drag handle */}
<div className="absolute left-2 top-1/2 -translate-y-1/2 cursor-grab active:cursor-grabbing sm:left-2.5">
<GripVertical className="h-4 w-4 text-white/20 group-hover:text-white/40 sm:h-4 sm:w-4" />
</div>
<div className="pl-5 sm:pl-6">
{/* Agent header */}
<div className="flex items-start justify-between">
<div className="flex items-center gap-2 min-w-0 flex-1 sm:gap-2.5">
<span className="text-lg shrink-0 sm:text-xl">
{agent.recommendedIcon || "🤖"}
</span>
<div className="min-w-0 flex-1">
<h3 className="font-heading text-sm text-white truncate sm:text-base">
{agent.displayName}
</h3>
{/* Handle & Summary - shown on hover */}
<div className="grid grid-rows-[0fr] opacity-0 transition-all duration-300 group-hover:grid-rows-[1fr] group-hover:opacity-100">
<div className="overflow-hidden">
<p className="mt-1 text-[0.65rem] text-white/70 sm:text-xs">
@{agent.agentId}
</p>
{agent.summary && (
<p className="mt-1.5 text-xs text-white/70 sm:text-sm">
{agent.summary}
</p>
)}
</div>
</div>
</div>
</div>
<Button
onClick={() => handleRemove(agent.agentId)}
variant="ghost"
size="icon"
className="h-6 w-6 shrink-0 rounded-full text-white/40 hover:bg-destructive/20 hover:text-destructive sm:h-7 sm:w-7"
>
<Trash2 className="h-3 w-3 sm:h-3.5 sm:w-3.5" />
</Button>
</div>
{/* Action button */}
<div className="mt-0 max-h-0 overflow-hidden opacity-0 transition-all duration-300 group-hover:mt-2 group-hover:max-h-20 group-hover:opacity-100 sm:group-hover:mt-2.5 pointer-events-auto">
<Button
onClick={() => handleStartChat(agent)}
className="w-full rounded-lg bg-white/90 text-charcoal py-1.5 text-xs font-medium shadow-md transition-all hover:bg-white hover:scale-[1.01] hover:shadow-lg active:brightness-125 border border-white/80 sm:rounded-xl sm:text-sm"
>
<MessageSquare className="mr-1.5 h-3 w-3 sm:mr-2 sm:h-3.5 sm:w-3.5" />
Start chat
</Button>
</div>
</div>
</motion.div>
</Reorder.Item>
))}
</Reorder.Group>
)}
</div>
</motion.div>
</>
)}
</AnimatePresence>
)
}