feat: reveal all hidden shapes with Shift+H, release SAM3 VRAM on idle and on demand (REQ-191, REQ-192)
This commit is contained in:
1 parent
f5be7880b0
commit
fd608848bf
16 files changed
+290
-20
No files matched your search
@@ -39,6 +39,7 @@ async function request(path, { method = 'GET', body, form, signal } = {}) {
|
||||
|
||||
export const api = {
|
||||
health: () => request('/health'),
|
||||
releaseSam3: () => request('/sam3/release', { method: 'POST' }),
|
||||
|
||||
listProjects: () => request('/projects'),
|
||||
getProject: (id) => request(`/projects/${id}`),
|
||||
|
||||
@@ -700,6 +700,23 @@ main.page {
|
||||
|
||||
.side-panel { padding: 12px 14px; display: grid; gap: 10px; }
|
||||
|
||||
.reveal-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: 8px;
|
||||
padding: 2px 7px;
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: 999px;
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.class-list, .shape-list { display: grid; gap: 4px; }
|
||||
|
||||
.class-chip {
|
||||
|
||||
@@ -14,6 +14,7 @@ export default function ReviewSidebar({
|
||||
onToggleHide,
|
||||
hiddenShapeIds,
|
||||
overriddenShapeIds,
|
||||
revealAll,
|
||||
onRestoreShape,
|
||||
annotations,
|
||||
selectedId,
|
||||
@@ -76,7 +77,14 @@ export default function ReviewSidebar({
|
||||
</div>
|
||||
|
||||
<div className="panel side-panel">
|
||||
<h2>Shapes on this frame ({annotations.length})</h2>
|
||||
<h2>
|
||||
Shapes on this frame ({annotations.length})
|
||||
{revealAll && (
|
||||
<span className="reveal-chip" title="Revealing H-hidden shapes and hidden classes — press Shift+H to go back">
|
||||
<EyeIcon size={11} /> revealing hidden
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
{annotations.length === 0 ? (
|
||||
<div>
|
||||
<p className="hint" style={{ marginBottom: 8 }}>
|
||||
|
||||
@@ -47,6 +47,10 @@ export default function ShortcutsPanel() {
|
||||
<dt><kbd>H</kbd></dt>
|
||||
<dd>Hide / show selected shapes</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt><kbd>Shift</kbd>+<kbd>H</kbd></dt>
|
||||
<dd>Reveal all hidden shapes (press again to hide)</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt><kbd>←</kbd> <kbd>→</kbd></dt>
|
||||
<dd>Prev / next frame</dd>
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
|
||||
export default function Sidebar({ route, currentProject, theme, onToggleTheme }) {
|
||||
const [health, setHealth] = useState(null)
|
||||
const [notice, setNotice] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
let timer
|
||||
@@ -28,6 +29,18 @@ export default function Sidebar({ route, currentProject, theme, onToggleTheme })
|
||||
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
|
||||
|
||||
@@ -138,9 +151,28 @@ export default function Sidebar({ route, currentProject, theme, onToggleTheme })
|
||||
<span className="health-badge">
|
||||
VRAM: {health.vram_free_gb ? `${health.vram_free_gb.toFixed(1)}GB` : 'N/A'}
|
||||
</span>
|
||||
<span className={`health-badge ${health.sam3_ready ? 'ready' : ''}`}>
|
||||
SAM3: {health.sam3_ready ? 'Ready' : 'Off'}
|
||||
</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>
|
||||
)}
|
||||
|
||||
|
||||
@@ -26,6 +26,9 @@ export default function ReviewPage({ batchId: rawBatchId, projectId, onProject }
|
||||
const [hiddenClasses, setHiddenClasses] = useState(() => new Set())
|
||||
const [hiddenShapeIds, setHiddenShapeIds] = useState(() => new Set())
|
||||
const [overriddenShapeIds, setOverriddenShapeIds] = useState(() => new Set())
|
||||
// REQ-191: overlay only — Shift+H shows everything without touching the
|
||||
// real hidden sets, a second press restores them as they were.
|
||||
const [revealAll, setRevealAll] = useState(false)
|
||||
const [trackFrames, setTrackFrames] = useState(() => {
|
||||
const stored = Number(localStorage.getItem('trackFrames'))
|
||||
return Number.isFinite(stored) && stored >= 1 ? Math.min(100, Math.round(stored)) : 5
|
||||
@@ -230,9 +233,11 @@ export default function ReviewPage({ batchId: rawBatchId, projectId, onProject }
|
||||
|
||||
// REQ-185: neither the shape's own hide nor its class's hide (without an
|
||||
// override) may reach the canvas. Session-only, never persisted.
|
||||
// REQ-191: while revealAll is on, both kinds of hidden shape come back.
|
||||
const isShapeVisible = (row) =>
|
||||
!hiddenShapeIds.has(row.id) &&
|
||||
(!hiddenClasses.has(row.class_id) || overriddenShapeIds.has(row.id))
|
||||
revealAll ||
|
||||
(!hiddenShapeIds.has(row.id) &&
|
||||
(!hiddenClasses.has(row.class_id) || overriddenShapeIds.has(row.id)))
|
||||
|
||||
const visibleAnnotations = annotations.filter(isShapeVisible)
|
||||
|
||||
@@ -459,7 +464,7 @@ export default function ReviewPage({ batchId: rawBatchId, projectId, onProject }
|
||||
}, [annotations, selectedId, index, frames, busy, trackFrames, project])
|
||||
|
||||
const stateRef = useRef({})
|
||||
stateRef.current = { frames, index, project, selectedId, setStatus, removeSelected, reclass, jumpToPending, jumpToNextAnnotated, setAssistMode, copyPrevious, trackForward, mode, setMode, markedIds, removeMarked, reclassMarked, setMarkedIds, toggleHideSelected }
|
||||
stateRef.current = { frames, index, project, selectedId, setStatus, removeSelected, reclass, jumpToPending, jumpToNextAnnotated, setAssistMode, copyPrevious, trackForward, mode, setMode, markedIds, removeMarked, reclassMarked, setMarkedIds, toggleHideSelected, revealAll, setRevealAll }
|
||||
|
||||
useEffect(() => {
|
||||
function onKeyDown(event) {
|
||||
@@ -467,7 +472,7 @@ export default function ReviewPage({ batchId: rawBatchId, projectId, onProject }
|
||||
// Ctrl/Cmd/Alt combos belong to the browser and the OS: without this,
|
||||
// Ctrl+S approves the frame and Ctrl+A/C/X/N/T all fire review actions.
|
||||
if (event.ctrlKey || event.metaKey || event.altKey) return
|
||||
const { frames, project, setStatus, removeSelected, reclass, jumpToPending, jumpToNextAnnotated, setAssistMode, copyPrevious, trackForward, mode, setMode, markedIds, removeMarked, reclassMarked, setMarkedIds, toggleHideSelected } = stateRef.current
|
||||
const { frames, project, setStatus, removeSelected, reclass, jumpToPending, jumpToNextAnnotated, setAssistMode, copyPrevious, trackForward, mode, setMode, markedIds, removeMarked, reclassMarked, setMarkedIds, toggleHideSelected, revealAll, setRevealAll } = stateRef.current
|
||||
const key = event.key
|
||||
const isShortcutKey = /^[1-9]$/.test(key) || ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Delete', 'Backspace', 'a', 'A', 'x', 'X', 'u', 'U', 's', 'S', 'n', 'N', 'c', 'C', 't', 'T', 'v', 'V', 'h', 'H', 'Escape'].includes(key)
|
||||
if (isShortcutKey) { event.preventDefault(); event.stopPropagation() }
|
||||
@@ -494,7 +499,13 @@ export default function ReviewPage({ batchId: rawBatchId, projectId, onProject }
|
||||
else if (key === 'n' || key === 'N') jumpToNextAnnotated?.()
|
||||
else if (key === 'c' || key === 'C') copyPrevious?.()
|
||||
else if (key === 't' || key === 'T') trackForward?.(false)
|
||||
else if (key === 'h' || key === 'H') toggleHideSelected?.()
|
||||
else if (key === 'h' || key === 'H') {
|
||||
// REQ-191: Shift+H toggles the reveal-all overlay; plain H first
|
||||
// drops an active overlay so it edits the real hidden state.
|
||||
if (event.shiftKey) setRevealAll?.((on) => !on)
|
||||
else if (revealAll) setRevealAll?.(false)
|
||||
else toggleHideSelected?.()
|
||||
}
|
||||
else if (key === 'Delete' || key === 'Backspace') removeSelected?.()
|
||||
else if (/^[1-9]$/.test(key)) {
|
||||
const classId = Number(key) - 1
|
||||
@@ -810,6 +821,7 @@ export default function ReviewPage({ batchId: rawBatchId, projectId, onProject }
|
||||
onToggleHide={toggleHide}
|
||||
hiddenShapeIds={hiddenShapeIds}
|
||||
overriddenShapeIds={overriddenShapeIds}
|
||||
revealAll={revealAll}
|
||||
onRestoreShape={restoreShape}
|
||||
annotations={annotations}
|
||||
selectedId={selectedId}
|
||||
|
||||
@@ -87,6 +87,20 @@
|
||||
border-color: rgba(34, 197, 94, 0.25);
|
||||
}
|
||||
|
||||
/* REQ-192: the Loaded badge is a button (click frees the VRAM) — give it the
|
||||
affordances a span doesn't need. */
|
||||
button.health-badge {
|
||||
font-family: inherit;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
button.health-badge:hover { background: rgba(255, 255, 255, 0.14); }
|
||||
button.health-badge.ready:hover { background: rgba(34, 197, 94, 0.22); }
|
||||
button.health-badge:focus-visible {
|
||||
outline: 2px solid #a855f7;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.topbar-theme-toggle {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
|
||||
Reference in new issue
Block a user