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:
asus committed 2026-10-07 11:55:00 +07:00
1 parent f5be7880b0
commit fd608848bf
16 files changed
+290 -20

No files matched your search

+1
View File
@@ -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}`),
+17
View File
@@ -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 {
+9 -1
View File
@@ -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>
+35 -3
View File
@@ -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>
)}
+17 -5
View File
@@ -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}
+14
View File
@@ -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);