ZARAI / frontend /src /components /layout /SettingsModal.jsx
Srajal2579
Deploy JARVIS
3e646d9
Raw History Blame Contribute Delete
9.77 kB
/**
* JARVIS β€” Settings Modal
* Runtime configuration: model status, memory stats, preferences.
* Opens via keyboard shortcut Ctrl+, or settings button.
*/
import React, { useState, useEffect } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import {
X, Brain, Eye, Mic, Database, Zap, Server,
RefreshCw, Trash2, CheckCircle, XCircle, AlertCircle,
} from 'lucide-react'
import clsx from 'clsx'
import toast from 'react-hot-toast'
import { healthAPI } from '../../services/api'
import useAppStore from '../../stores/appStore'
export default function SettingsModal({ open, onClose }) {
const [health, setHealth] = useState(null)
const [loading, setLoading] = useState(false)
const { sessions, messages } = useAppStore()
const fetchHealth = async () => {
setLoading(true)
try {
const data = await healthAPI.check()
setHealth(data)
} catch {
setHealth({ status: 'offline' })
} finally {
setLoading(false)
}
}
useEffect(() => {
if (open) fetchHealth()
}, [open])
const totalMessages = Object.values(messages).reduce(
(sum, msgs) => sum + (msgs?.length || 0), 0
)
const models = [
{ key: 'llm', label: 'Language Model', icon: Brain, desc: 'TinyLlama / Mistral' },
{ key: 'blip', label: 'BLIP Vision', icon: Eye, desc: 'Image captioning' },
{ key: 'clip', label: 'CLIP Vision', icon: Eye, desc: 'Image-text similarity' },
{ key: 'whisper', label: 'Whisper STT', icon: Mic, desc: 'Speech-to-text' },
]
return (
<AnimatePresence>
{open && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40"
onClick={onClose}
/>
{/* Modal */}
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 16 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95, y: 16 }}
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
className="fixed inset-0 flex items-center justify-center z-50 p-4"
>
<div className="w-full max-w-md bg-jarvis-surface border border-jarvis-border
rounded-2xl shadow-2xl overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between px-5 py-4
border-b border-jarvis-border">
<div>
<h2 className="font-display text-sm font-bold text-jarvis-accent tracking-widest">
SYSTEM STATUS
</h2>
<p className="text-[10px] text-jarvis-dim font-mono mt-0.5">
JARVIS Multi-Modal AI Assistant v1.0
</p>
</div>
<button
onClick={onClose}
className="p-1.5 rounded-lg text-jarvis-dim hover:text-jarvis-text
hover:bg-jarvis-panel transition-colors"
>
<X size={15} />
</button>
</div>
<div className="p-5 space-y-5 max-h-[70vh] overflow-y-auto">
{/* Backend status */}
<section>
<SectionLabel icon={Server} label="Backend" />
<div className="flex items-center gap-2 mt-2">
<StatusDot ok={health?.status === 'online'} />
<span className="text-sm font-mono text-jarvis-text">
{health?.status === 'online' ? 'FastAPI Online' : 'Connecting…'}
</span>
<button
onClick={fetchHealth}
disabled={loading}
className="ml-auto text-jarvis-dim hover:text-jarvis-accent
transition-colors p-1"
>
<RefreshCw size={12} className={loading ? 'animate-spin' : ''} />
</button>
</div>
</section>
{/* AI Models */}
<section>
<SectionLabel icon={Brain} label="AI Models" />
<div className="mt-2 space-y-2">
{models.map(({ key, label, icon: Icon, desc }) => {
const status = health?.models?.[key]
return (
<div key={key}
className="flex items-center gap-3 px-3 py-2.5
bg-jarvis-panel rounded-lg border border-jarvis-border">
<Icon size={14} className="text-jarvis-dim flex-shrink-0" />
<div className="flex-1 min-w-0">
<p className="text-xs font-medium text-jarvis-text">{label}</p>
<p className="text-[10px] text-jarvis-dim font-mono">{desc}</p>
</div>
<ModelStatus status={status} />
</div>
)
})}
</div>
</section>
{/* Memory stats */}
<section>
<SectionLabel icon={Database} label="Memory & Storage" />
<div className="mt-2 grid grid-cols-2 gap-2">
{[
{ label: 'Sessions', value: sessions.length },
{ label: 'Messages', value: totalMessages },
{
label: 'FAISS Vectors',
value: health?.memory?.total_vectors ?? 'β€”',
},
{
label: 'Memory Store',
value: health?.memory?.initialized ? 'Ready' : 'Not ready',
},
].map(({ label, value }) => (
<div key={label}
className="bg-jarvis-panel rounded-lg border border-jarvis-border
px-3 py-2">
<p className="text-[10px] text-jarvis-dim font-mono uppercase tracking-wider">
{label}
</p>
<p className="text-lg font-bold text-jarvis-accent font-mono mt-0.5">
{value}
</p>
</div>
))}
</div>
</section>
{/* API endpoints */}
<section>
<SectionLabel icon={Zap} label="API Endpoints" />
<div className="mt-2 space-y-1 font-mono text-[11px]">
{[
{ method: 'POST', path: '/api/chat/message', desc: 'Chat' },
{ method: 'POST', path: '/api/image/analyze', desc: 'Vision' },
{ method: 'POST', path: '/api/voice/interact', desc: 'Voice' },
{ method: 'POST', path: '/api/tools/execute', desc: 'Tools' },
{ method: 'WS', path: '/api/ws/chat/{id}', desc: 'Streaming' },
].map(({ method, path, desc }) => (
<div key={path}
className="flex items-center gap-2 px-2 py-1 rounded">
<span className={clsx(
'text-[9px] font-bold px-1.5 py-0.5 rounded',
method === 'POST' ? 'bg-jarvis-accent/20 text-jarvis-accent' :
method === 'WS' ? 'bg-jarvis-success/20 text-jarvis-success' :
'bg-jarvis-muted/20 text-jarvis-muted',
)}>
{method}
</span>
<span className="text-jarvis-muted">{path}</span>
<span className="ml-auto text-jarvis-dim">{desc}</span>
</div>
))}
</div>
</section>
{/* Docs link */}
<div className="text-center pt-1">
<a
href="/api/docs"
target="_blank"
rel="noopener noreferrer"
className="text-xs text-jarvis-accent hover:underline font-mono"
>
Open Interactive API Docs β†’
</a>
</div>
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
)
}
// ── Helpers ────────────────────────────────────────────────────
function SectionLabel({ icon: Icon, label }) {
return (
<div className="flex items-center gap-2">
<Icon size={11} className="text-jarvis-accent" />
<span className="text-[10px] font-mono uppercase tracking-widest text-jarvis-dim">
{label}
</span>
</div>
)
}
function StatusDot({ ok }) {
return (
<div className={clsx(
'w-2 h-2 rounded-full',
ok ? 'bg-jarvis-success animate-pulse-slow' : 'bg-jarvis-dim',
)} />
)
}
function ModelStatus({ status }) {
if (status === true) return <CheckCircle size={13} className="text-jarvis-success" />
if (status === false) return <XCircle size={13} className="text-jarvis-error" />
return <AlertCircle size={13} className="text-jarvis-dim" />
}