Skip to content

Commit e2f78f9

Browse files
authored
Merge pull request #48 from hriday330/improvement/add-keyboard-navigation-for-annotations-view
Improvement/add keyboard navigation & autoplay
2 parents 57e272c + 1757331 commit e2f78f9

7 files changed

Lines changed: 126 additions & 21 deletions

File tree

dataset_explorer/app/DashboardContent.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { Button } from "@components/ui/button";
2121

2222
import type { User } from "@supabase/supabase-js";
2323
import type { Dataset, Label, BoundingBox, ImageThumbnail } from "@lib/types";
24+
import { useLabelKeyboardNavigation } from "@components/General/hooks/useLabelKeyboardNavigation";
2425

2526
// TODO - make page size configurable
2627
const PAGE_SIZE = 12;
@@ -69,6 +70,10 @@ export function DashboardContent({
6970
const { labels, createLabel, updateLabel, reorderLabels, deleteLabel } =
7071
useLabelClasses(datasetId);
7172

73+
useLabelKeyboardNavigation(labels, selectedLabelId, (newId) => {
74+
setSelectedLabelId(newId);
75+
});
76+
7277
const {
7378
data: analytics,
7479
loading,

dataset_explorer/components/General/DatasetPicker.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import { useState } from "react";
4-
import { Check, ChevronDown, FolderOpen, Plus } from "lucide-react";
4+
import { Check, ChevronDown, File, FolderOpen, Plus } from "lucide-react";
55

66
import { cn } from "@components/ui/utils";
77
import { useDataset } from "@contexts/DatasetContext";
@@ -73,10 +73,7 @@ export function DatasetPicker() {
7373
No datasets found.
7474
</CommandEmpty>
7575

76-
<CommandGroup
77-
heading="Datasets"
78-
className="max-h-[220px] overflow-y-auto"
79-
>
76+
<CommandGroup className="max-h-[220px] overflow-y-auto">
8077
{datasets.map((d) => (
8178
<CommandItem
8279
key={d.id}
@@ -112,7 +109,7 @@ export function DatasetPicker() {
112109
onSelect={() => router.push("/datasets")}
113110
className="flex items-center gap-2 px-3 py-2 text-[#D4D4D4] hover:bg-[#1A1A1A]"
114111
>
115-
<Plus className="w-4 h-4" />
112+
<File className="w-4 h-4" />
116113
<span>Manage your dataset</span>
117114
</CommandItem>
118115
</CommandGroup>
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useEffect, useRef } from "react";
2+
3+
export function useAutoplayFrames(
4+
isPlaying: boolean,
5+
intervalMs: number,
6+
onNextFrame: () => void,
7+
disableDrawing: (disabled: boolean) => void,
8+
) {
9+
const intervalRef = useRef<NodeJS.Timeout | null>(null);
10+
11+
useEffect(() => {
12+
if (!isPlaying) {
13+
if (intervalRef.current) {
14+
clearInterval(intervalRef.current);
15+
intervalRef.current = null;
16+
}
17+
disableDrawing(false);
18+
return;
19+
}
20+
disableDrawing(true);
21+
22+
intervalRef.current = setInterval(() => {
23+
onNextFrame();
24+
}, intervalMs);
25+
26+
return () => {
27+
if (intervalRef.current) {
28+
clearInterval(intervalRef.current);
29+
intervalRef.current = null;
30+
}
31+
disableDrawing(false);
32+
};
33+
}, [isPlaying, intervalMs, onNextFrame, disableDrawing]);
34+
}

dataset_explorer/components/General/ImageViewer/hooks/useSelectFrame.tsx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,32 @@ export function useSelectFrame(
77
) {
88
const [frameInput, setFrameInput] = useState(frameNumber.toString());
99

10-
// Keep input in sync with external frame changes
1110
useEffect(() => {
1211
setFrameInput(frameNumber.toString());
1312
}, [frameNumber]);
1413

14+
useEffect(() => {
15+
const handleKey = (e: KeyboardEvent) => {
16+
const active = document.activeElement as HTMLElement | null;
17+
if (
18+
active &&
19+
(["INPUT", "TEXTAREA", "SELECT"].includes(active.tagName) ||
20+
active.isContentEditable)
21+
) {
22+
return;
23+
}
24+
25+
if (e.key === "ArrowLeft" && frameNumber > 1) {
26+
onGoToFrame(frameNumber - 1);
27+
} else if (e.key === "ArrowRight" && frameNumber < totalFrames) {
28+
onGoToFrame(frameNumber + 1);
29+
}
30+
};
31+
32+
window.addEventListener("keydown", handleKey);
33+
return () => window.removeEventListener("keydown", handleKey);
34+
}, [frameNumber, totalFrames, onGoToFrame]);
35+
1536
const isValidFrame = useCallback(
1637
(value: string) => {
1738
const num = Number(value);

dataset_explorer/components/General/ImageViewer/index.tsx

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useState, useRef, useEffect } from "react";
66
import type { BoundingBox, Label } from "@lib/types";
77
import { useBoundingBoxes } from "./hooks/useBoundingBoxes";
88
import { useSelectFrame } from "./hooks/useSelectFrame";
9+
import { useAutoplayFrames } from "./hooks/useAutoplayFrames";
910

1011
interface Frame {
1112
id: string;
@@ -42,7 +43,7 @@ export function ImageViewer({
4243
}: ImageViewerProps) {
4344
const [isPlaying, setIsPlaying] = useState(false);
4445
const containerRef = useRef<HTMLDivElement>(null);
45-
46+
const [drawingDisabled, setDrawingDisabled] = useState(false);
4647
const {
4748
getBoundingBoxLabelColor,
4849
getBoundingBoxLabelName,
@@ -70,21 +71,18 @@ export function ImageViewer({
7071
isValidFrame,
7172
} = useSelectFrame(frameNumber, totalFrames, onGoToFrame);
7273

73-
// TODO - work on autoplay logic
74+
7475
useEffect(() => {
7576
// Sync displayed number when frame changes externally
7677
setFrameInput(frameNumber.toString());
7778
}, [frameNumber]);
7879

79-
// Auto-play frames
80-
useEffect(() => {
81-
if (isPlaying) {
82-
const interval = setInterval(() => {
83-
onNextFrame();
84-
}, 2000);
85-
return () => clearInterval(interval);
86-
}
87-
}, [isPlaying, onNextFrame]);
80+
useAutoplayFrames(
81+
isPlaying,
82+
500,
83+
onNextFrame,
84+
setDrawingDisabled,
85+
);
8886

8987
return (
9088
<div className="flex-1 flex flex-col items-center justify-center p-6 gap-4">
@@ -174,7 +172,7 @@ export function ImageViewer({
174172
))}
175173

176174
{/* Current Drawing Box */}
177-
{currentBox && (
175+
{!drawingDisabled && currentBox && (
178176
<div
179177
className="absolute border-2 border-dashed rounded pointer-events-none"
180178
style={{

dataset_explorer/components/General/Sidebar.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,6 @@ export function Sidebar({
3333
const shortcuts = [
3434
{ keys: ["←", "→"], description: "Navigate frames" },
3535
{ keys: ["↑", "↓"], description: "Select labels" },
36-
{ keys: ["1-9"], description: "Quick label" },
37-
{ keys: ["Space"], description: "Play/Pause" },
3836
];
3937

4038
const toggleLabel = (id: string) => {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { useEffect, useCallback } from "react";
2+
import { Label } from "@lib/types";
3+
4+
export function useLabelKeyboardNavigation(
5+
labels: Label[],
6+
selectedLabelId: string | null,
7+
onSelectLabel: (id: string) => void,
8+
) {
9+
const currentIndex = labels.findIndex((l) => l.id === selectedLabelId);
10+
11+
const selectPrev = useCallback(() => {
12+
if (labels.length === 0) return;
13+
14+
const nextIndex = currentIndex <= 0 ? labels.length - 1 : currentIndex - 1;
15+
16+
onSelectLabel(labels[nextIndex].id);
17+
}, [labels, currentIndex, onSelectLabel]);
18+
19+
const selectNext = useCallback(() => {
20+
if (labels.length === 0) return;
21+
22+
const nextIndex = currentIndex >= labels.length - 1 ? 0 : currentIndex + 1;
23+
24+
onSelectLabel(labels[nextIndex].id);
25+
}, [labels, currentIndex, onSelectLabel]);
26+
27+
useEffect(() => {
28+
const handleKey = (e: KeyboardEvent) => {
29+
if (e.repeat) return;
30+
// block when typing in input/text areas
31+
const active = document.activeElement as HTMLElement | null;
32+
if (
33+
active &&
34+
(["INPUT", "TEXTAREA", "SELECT"].includes(active.tagName) ||
35+
active.isContentEditable)
36+
) {
37+
return;
38+
}
39+
40+
if (e.key === "ArrowUp") {
41+
e.preventDefault();
42+
selectPrev();
43+
} else if (e.key === "ArrowDown") {
44+
e.preventDefault();
45+
selectNext();
46+
}
47+
};
48+
49+
window.addEventListener("keydown", handleKey);
50+
return () => window.removeEventListener("keydown", handleKey);
51+
}, [selectPrev, selectNext]);
52+
}

0 commit comments

Comments
 (0)