File size: 6,184 Bytes
3e646d9 | 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 | /**
* JARVIS — Sidebar
* Session history, new chat button, model status indicators
*/
import React, { useState } from 'react'
import { motion } from 'framer-motion'
import {
Plus, MessageSquare, Trash2, ChevronRight,
Bot, Zap, Eye, Mic, Brain, Settings,
} from 'lucide-react'
import { formatDistanceToNow } from 'date-fns'
import clsx from 'clsx'
import useAppStore from '../../stores/appStore'
import toast from 'react-hot-toast'
export default function Sidebar() {
const {
sessions, activeSessionId, modelStatus,
newSession, switchSession, deleteSession,
} = useAppStore()
const [hoveredId, setHoveredId] = useState(null)
const handleNew = () => {
newSession()
toast('New chat started', { icon: '✨' })
}
const handleDelete = async (e, id) => {
e.stopPropagation()
if (sessions.length === 1) return toast.error('Cannot delete the only session')
await deleteSession(id)
toast('Session deleted')
}
const models = [
{ key: 'llm', label: 'LLM', icon: Brain },
{ key: 'blip', label: 'Vision', icon: Eye },
{ key: 'whisper', label: 'Voice', icon: Mic },
]
return (
<aside className="w-64 h-full bg-jarvis-surface border-r border-jarvis-border flex flex-col">
{/* Header */}
<div className="p-4 border-b border-jarvis-border">
<div className="flex items-center gap-2 mb-4">
<div className="w-8 h-8 rounded-lg bg-jarvis-accent/10 border border-jarvis-accent/30
flex items-center justify-center animate-glow">
<Bot size={16} className="text-jarvis-accent" />
</div>
<div>
<h1 className="font-display text-sm font-bold text-jarvis-accent tracking-widest">
JARVIS
</h1>
<p className="text-[10px] text-jarvis-dim font-mono">v1.0 · MULTI-MODAL</p>
</div>
</div>
<button
onClick={handleNew}
className="w-full flex items-center gap-2 px-3 py-2 rounded-lg
bg-jarvis-accent/10 border border-jarvis-accent/30
text-jarvis-accent text-sm font-medium
hover:bg-jarvis-accent/20 transition-all duration-200
active:scale-95"
>
<Plus size={14} />
New Chat
</button>
</div>
{/* Sessions */}
<div className="flex-1 overflow-y-auto py-2">
<p className="px-4 py-1 text-[10px] uppercase tracking-widest text-jarvis-dim font-mono">
Conversations
</p>
{sessions.length === 0 && (
<p className="px-4 py-3 text-jarvis-dim text-xs">No sessions yet.</p>
)}
{sessions.map((session) => (
<motion.div
key={session.id}
layout
initial={{ opacity: 0, x: -8 }}
animate={{ opacity: 1, x: 0 }}
className={clsx(
'group relative mx-2 mb-0.5 rounded-lg cursor-pointer',
'transition-colors duration-150',
activeSessionId === session.id
? 'bg-jarvis-accent/10 border border-jarvis-accent/20'
: 'hover:bg-jarvis-panel border border-transparent',
)}
onClick={() => switchSession(session.id)}
onMouseEnter={() => setHoveredId(session.id)}
onMouseLeave={() => setHoveredId(null)}
>
<div className="flex items-center gap-2 px-3 py-2.5">
<MessageSquare
size={13}
className={clsx(
'flex-shrink-0',
activeSessionId === session.id ? 'text-jarvis-accent' : 'text-jarvis-dim',
)}
/>
<div className="flex-1 min-w-0">
<p className={clsx(
'text-xs font-medium truncate',
activeSessionId === session.id ? 'text-jarvis-text' : 'text-jarvis-muted',
)}>
{session.title || 'New Chat'}
</p>
<p className="text-[10px] text-jarvis-dim truncate">
{session.updated_at
? formatDistanceToNow(new Date(session.updated_at), { addSuffix: true })
: '—'}
</p>
</div>
{/* Delete button */}
{hoveredId === session.id && (
<button
onClick={(e) => handleDelete(e, session.id)}
className="flex-shrink-0 p-1 rounded hover:text-jarvis-error
text-jarvis-dim transition-colors"
>
<Trash2 size={11} />
</button>
)}
</div>
</motion.div>
))}
</div>
{/* Model status */}
<div className="p-3 border-t border-jarvis-border">
<p className="text-[10px] uppercase tracking-widest text-jarvis-dim font-mono mb-2">
AI Modules
</p>
<div className="space-y-1">
{models.map(({ key, label, icon: Icon }) => {
const loaded = modelStatus?.models?.[key]
return (
<div key={key} className="flex items-center gap-2">
<div className={clsx(
'w-1.5 h-1.5 rounded-full',
loaded === true ? 'bg-jarvis-success animate-pulse-slow' :
loaded === false ? 'bg-jarvis-error' :
'bg-jarvis-dim',
)} />
<Icon size={11} className="text-jarvis-dim" />
<span className="text-[11px] text-jarvis-muted font-mono">{label}</span>
<span className={clsx(
'ml-auto text-[9px] font-mono',
loaded === true ? 'text-jarvis-success' :
loaded === false ? 'text-jarvis-error' :
'text-jarvis-dim',
)}>
{loaded === true ? 'ONLINE' : loaded === false ? 'OFFLINE' : '—'}
</span>
</div>
)
})}
</div>
</div>
</aside>
)
}
|