diff --git a/src/app/page.tsx b/src/app/page.tsx index b19ea81..5f43754 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -10,6 +10,8 @@ import SpanAnnotationList from '@/components/SpanAnnotationList'; import TalibPatternPanel from '@/components/TalibPatternPanel'; import TrainingPanel from '@/components/TrainingPanel'; import { ThemeToggle } from '@/components/ThemeToggle'; +import KeyboardShortcutsModal from '@/components/KeyboardShortcutsModal'; +import { useTheme } from 'next-themes'; import { Settings, Tag, Layers } from 'lucide-react'; import type { PredictionState, PredictionSpan, ModelInfoResponse, Disagreement, DisagreementType, PredictionSummary } from '@/types/predictions'; @@ -159,7 +161,9 @@ interface SpanLabelType { } export default function Home() { + const { theme, setTheme } = useTheme(); const [settingsOpen, setSettingsOpen] = useState(false); + const [shortcutsOpen, setShortcutsOpen] = useState(false); const [activeTool, setActiveTool] = useState(null); const [selectedColor, setSelectedColor] = useState('#3b82f6'); const [selectedLabelId, setSelectedLabelId] = useState(null); @@ -698,9 +702,14 @@ export default function Home() { } }, [predictionState.visible, predictionState.spans, spanAnnotations]); - // Keyboard handler for Delete/Backspace key + // Keyboard handler for Delete/Backspace key and tool shortcuts useEffect(() => { const handleKeyDown = async (e: KeyboardEvent) => { + // Ignore shortcuts when typing in an input/textarea/select + const tag = (e.target as HTMLElement).tagName; + if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return; + + // Delete selected marker annotation if ((e.key === 'Delete' || e.key === 'Backspace') && selectedLabelId !== null) { try { const response = await fetch(`/api/annotations/${selectedLabelId}`, { @@ -713,17 +722,41 @@ export default function Home() { } catch (error) { console.error('Failed to delete label:', error); } + return; + } + + // Tool shortcuts (only when not in a modal) + switch (e.key.toLowerCase()) { + case 'r': + setActiveTool((prev) => (prev === 'rectangle' ? null : 'rectangle')); + break; + case 's': + setActiveTool((prev) => (prev === 'span' ? null : 'span')); + break; + case 'l': + setActiveTool((prev) => (prev === 'line' ? null : 'line')); + break; + case 'd': + setActiveTool((prev) => (prev === 'delete' ? null : 'delete')); + break; + case 't': + setTheme(theme === 'dark' ? 'light' : 'dark'); + break; + case '?': + setShortcutsOpen((prev) => !prev); + break; } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); - }, [selectedLabelId]); + }, [selectedLabelId, theme, setTheme]); const activeChart = charts.find((c) => c.id === activeChartId); return (
+ setShortcutsOpen(false)} /> {/* Sidebar */}
{/* Sidebar Header */} diff --git a/src/components/KeyboardShortcutsModal.tsx b/src/components/KeyboardShortcutsModal.tsx new file mode 100644 index 0000000..ba52379 --- /dev/null +++ b/src/components/KeyboardShortcutsModal.tsx @@ -0,0 +1,78 @@ +'use client'; + +import { useEffect } from 'react'; +import { X } from 'lucide-react'; + +interface KeyboardShortcutsModalProps { + open: boolean; + onClose: () => void; +} + +const shortcuts = [ + { key: 'R', description: 'Rectangle tool' }, + { key: 'S', description: 'Span tool' }, + { key: 'L', description: 'Line tool' }, + { key: 'D', description: 'Delete tool' }, + { key: 'T', description: 'Toggle theme (light/dark)' }, + { key: 'Esc', description: 'Deselect tool / cancel action' }, + { key: 'Del / ⌫', description: 'Delete selected annotation' }, + { key: 'Enter', description: 'Edit selected span' }, + { key: '1–6', description: 'Quick-assign span label (during span creation)' }, + { key: '?', description: 'Show this help' }, +]; + +export default function KeyboardShortcutsModal({ open, onClose }: KeyboardShortcutsModalProps) { + useEffect(() => { + if (!open) return; + const handleKey = (e: KeyboardEvent) => { + if (e.key === 'Escape' || e.key === '?') { + onClose(); + } + }; + window.addEventListener('keydown', handleKey); + return () => window.removeEventListener('keydown', handleKey); + }, [open, onClose]); + + if (!open) return null; + + return ( +
+
e.stopPropagation()} + > +
+

Keyboard Shortcuts

+ +
+ + + + {shortcuts.map(({ key, description }) => ( + + + + + ))} + +
+ + {key} + + {description}
+ +

+ Press ? or Esc to close +

+
+
+ ); +}