211 lines
9.5 KiB
TypeScript
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>
|
|
)
|
|
}
|
|
|