191 lines
5.8 KiB
React
191 lines
5.8 KiB
React
import { useEffect, useState } from 'react'
|
|
import { api } from '../api'
|
|
import { navigate } from '../App'
|
|
import {
|
|
FolderIcon,
|
|
DatabaseIcon,
|
|
RocketIcon,
|
|
TagIcon,
|
|
BarChartIcon,
|
|
SunIcon,
|
|
MoonIcon,
|
|
CpuIcon,
|
|
LayersIcon,
|
|
ZapIcon,
|
|
} from './Icons'
|
|
|
|
|
|
export default function Sidebar({ route, currentProject, theme, onToggleTheme }) {
|
|
const [health, setHealth] = useState(null)
|
|
const [notice, setNotice] = useState('')
|
|
|
|
useEffect(() => {
|
|
let timer
|
|
const fetchHealth = () => {
|
|
api.health().then(setHealth).catch(() => {})
|
|
}
|
|
fetchHealth()
|
|
timer = setInterval(fetchHealth, 3000)
|
|
return () => clearInterval(timer)
|
|
}, [])
|
|
|
|
// REQ-192: free the SAM3 model's VRAM on demand; the 3 s health poll is what
|
|
// confirms it, this only flips the badge straight away when it worked.
|
|
const releaseSam3 = async () => {
|
|
try {
|
|
await api.releaseSam3()
|
|
setHealth((current) => (current ? { ...current, model_loaded: false } : current))
|
|
} catch (exc) {
|
|
setNotice(exc.message)
|
|
setTimeout(() => setNotice(''), 4000)
|
|
}
|
|
}
|
|
|
|
|
|
const pId = currentProject?.id || route.projectId || 1
|
|
|
|
const handleNav = (e, path) => {
|
|
e.preventDefault()
|
|
navigate(path)
|
|
}
|
|
|
|
return (
|
|
<header className="roboflow-topbar">
|
|
<div className="topbar-logo" onClick={(e) => handleNav(e, '/projects')} style={{ cursor: 'pointer' }}>
|
|
<span style={{ fontWeight: 700, fontSize: '0.95rem', letterSpacing: '-0.01em', color: '#f4f4f5' }}>
|
|
Dataset Enrichment
|
|
</span>
|
|
</div>
|
|
|
|
<nav className="topbar-nav">
|
|
<a
|
|
href="#/projects"
|
|
onClick={(e) => handleNav(e, '/projects')}
|
|
className={`topbar-item ${route.name === 'projects' ? 'active' : ''}`}
|
|
>
|
|
<FolderIcon size={15} />
|
|
<span>Projects</span>
|
|
</a>
|
|
|
|
<a
|
|
href={`#/projects/${pId}`}
|
|
onClick={(e) => handleNav(e, `/projects/${pId}`)}
|
|
className={`topbar-item ${route.name === 'library' ? 'active' : ''}`}
|
|
>
|
|
<DatabaseIcon size={15} />
|
|
<span>Video Archive</span>
|
|
</a>
|
|
|
|
<a
|
|
href={`#/projects/${pId}/batches`}
|
|
onClick={(e) => handleNav(e, `/projects/${pId}/batches`)}
|
|
className={`topbar-item ${route.name === 'batches' || route.name === 'review' ? 'active' : ''}`}
|
|
>
|
|
<TagIcon size={15} />
|
|
<span>Batches</span>
|
|
</a>
|
|
|
|
<a
|
|
href={`#/projects/${pId}/data-prep`}
|
|
onClick={(e) => handleNav(e, `/projects/${pId}/data-prep`)}
|
|
className={`topbar-item ${route.name === 'data-prep' ? 'active' : ''}`}
|
|
>
|
|
<BarChartIcon size={15} />
|
|
<span>Data Preparation</span>
|
|
</a>
|
|
|
|
|
|
|
|
<a
|
|
href={`#/projects/${pId}/datasets`}
|
|
onClick={(e) => handleNav(e, `/projects/${pId}/datasets`)}
|
|
className={`topbar-item ${route.name === 'datasets' ? 'active' : ''}`}
|
|
>
|
|
<LayersIcon size={15} />
|
|
<span>Datasets</span>
|
|
</a>
|
|
|
|
<a
|
|
href={`#/projects/${pId}/models`}
|
|
onClick={(e) => handleNav(e, `/projects/${pId}/models`)}
|
|
className={`topbar-item ${route.name === 'models' ? 'active' : ''}`}
|
|
>
|
|
<RocketIcon size={15} />
|
|
<span>Models & Training</span>
|
|
</a>
|
|
|
|
<a
|
|
href={`#/projects/${pId}/live-count`}
|
|
onClick={(e) => handleNav(e, `/projects/${pId}/live-count`)}
|
|
className={`topbar-item ${route.name === 'live-count' ? 'active' : ''}`}
|
|
>
|
|
<ZapIcon size={15} />
|
|
<span>Live Counting</span>
|
|
</a>
|
|
|
|
<a
|
|
href={`#/projects/${pId}/counting-bench`}
|
|
onClick={(e) => handleNav(e, `/projects/${pId}/counting-bench`)}
|
|
className={`topbar-item ${route.name === 'counting-bench' ? 'active' : ''}`}
|
|
>
|
|
<DatabaseIcon size={15} />
|
|
<span>Counting Accuracy</span>
|
|
</a>
|
|
|
|
<a
|
|
href="#/sam3-playground"
|
|
onClick={(e) => handleNav(e, '/sam3-playground')}
|
|
className={`topbar-item ${route.name === 'sam3-playground' ? 'active' : ''}`}
|
|
>
|
|
<CpuIcon size={15} />
|
|
<span>SAM3 Playground</span>
|
|
</a>
|
|
</nav>
|
|
|
|
<div className="topbar-right">
|
|
{health && (
|
|
<div className="topbar-health">
|
|
<span className="health-badge" title={health.gpu || ''}>
|
|
GPU: {health.gpu ? health.gpu.replace('NVIDIA GeForce ', '').replace(' Laptop GPU', '') : 'CPU'}
|
|
</span>
|
|
<span className="health-badge">
|
|
VRAM: {health.vram_free_gb ? `${health.vram_free_gb.toFixed(1)}GB` : 'N/A'}
|
|
</span>
|
|
{notice && <span className="health-badge">{notice}</span>}
|
|
{!health.sam3_ready ? (
|
|
<span className="health-badge" title="Not enough free VRAM for SAM3 — free the card first">
|
|
SAM3: Off
|
|
</span>
|
|
) : health.model_loaded ? (
|
|
<button
|
|
type="button"
|
|
className="health-badge ready"
|
|
title="SAM3 is in VRAM (~3.9 GB) — click to free it now"
|
|
onClick={releaseSam3}
|
|
>
|
|
SAM3: Loaded
|
|
</button>
|
|
) : (
|
|
<span
|
|
className="health-badge ready"
|
|
title="SAM3 not in VRAM — it loads on the next use (~12 s)"
|
|
>
|
|
SAM3: Idle
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<button
|
|
className="topbar-theme-toggle"
|
|
onClick={onToggleTheme}
|
|
title={theme === 'dark' ? 'Switch to Light Mode' : 'Switch to Dark Mode'}
|
|
>
|
|
{theme === 'dark' ? <SunIcon size={15} /> : <MoonIcon size={15} />}
|
|
</button>
|
|
</div>
|
|
</header>
|
|
)
|
|
}
|
|
|