feat: implement section 10 - hotkey label assignment for span annotations
This commit is contained in:
parent
4089aab77c
commit
b5e4d6573e
2 changed files with 70 additions and 12 deletions
|
|
@ -73,9 +73,9 @@
|
|||
|
||||
## 10. Hotkey Label Assignment
|
||||
|
||||
- [ ] 10.1 Add keydown listener for span label hotkeys (active only when span tool is active and span range is selected)
|
||||
- [ ] 10.2 On hotkey press: save span with mapped label (default confidence/outcome/notes), render rectangle, update sidebar — skip popover
|
||||
- [ ] 10.3 Ignore hotkeys when span tool is inactive or no span range selected
|
||||
- [x] 10.1 Add keydown listener for span label hotkeys (active only when span tool is active and span range is selected)
|
||||
- [x] 10.2 On hotkey press: save span with mapped label (default confidence/outcome/notes), render rectangle, update sidebar — skip popover
|
||||
- [x] 10.3 Ignore hotkeys when span tool is inactive or no span range selected
|
||||
|
||||
## 11. Export Endpoints
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ export default function SpanAnnotationManager({
|
|||
const [interactionState, setInteractionState] = useState<InteractionState>('idle');
|
||||
const [startCandle, setStartCandle] = useState<Candle | null>(null);
|
||||
const [endCandle, setEndCandle] = useState<Candle | null>(null);
|
||||
const [cursorCandle, setCursorCandle] = useState<Candle | null>(null);
|
||||
const [previewPrimitive, setPreviewPrimitive] = useState<SpanRectanglePrimitive | null>(null);
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
const [editingSpan, setEditingSpan] = useState<SpanAnnotation | null>(null);
|
||||
|
|
@ -255,18 +256,21 @@ export default function SpanAnnotationManager({
|
|||
if (!time) return;
|
||||
|
||||
const timestamp = typeof time === 'string' ? Date.parse(time) / 1000 : (time as number);
|
||||
const cursorCandle = findNearestCandle(timestamp);
|
||||
const nearestCandle = findNearestCandle(timestamp);
|
||||
|
||||
if (!cursorCandle) return;
|
||||
if (!nearestCandle) return;
|
||||
|
||||
// Update cursor candle for hotkey usage
|
||||
setCursorCandle(nearestCandle);
|
||||
|
||||
// Calculate price range for preview
|
||||
const { max_high, min_low } = calculatePriceRange(startCandle, cursorCandle);
|
||||
const { max_high, min_low } = calculatePriceRange(startCandle, nearestCandle);
|
||||
|
||||
// Swap if end < start
|
||||
const [start_time, end_time] =
|
||||
startCandle.time <= cursorCandle.time
|
||||
? [startCandle.time, cursorCandle.time]
|
||||
: [cursorCandle.time, startCandle.time];
|
||||
startCandle.time <= nearestCandle.time
|
||||
? [startCandle.time, nearestCandle.time]
|
||||
: [nearestCandle.time, startCandle.time];
|
||||
|
||||
const previewData: SpanData = {
|
||||
id: -1, // Preview ID
|
||||
|
|
@ -434,13 +438,14 @@ export default function SpanAnnotationManager({
|
|||
setEditingSpan(null);
|
||||
};
|
||||
|
||||
// Handle keyboard shortcuts for deletion and editing
|
||||
// Handle keyboard shortcuts for deletion, editing, and hotkey label assignment
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const handleKeyDown = async (e: KeyboardEvent) => {
|
||||
// Delete/Backspace: delete selected span
|
||||
if ((e.key === 'Delete' || e.key === 'Backspace') && selectedSpanId !== null) {
|
||||
e.preventDefault();
|
||||
handleDeleteSpan(selectedSpanId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Enter: open edit popover for selected span
|
||||
|
|
@ -450,12 +455,65 @@ export default function SpanAnnotationManager({
|
|||
setEditingSpan(span);
|
||||
setPopoverOpen(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Hotkey label assignment: only when span tool is active and a span range is selected (after first click)
|
||||
if (activeTool === 'span' && interactionState === 'first-click-done' && startCandle) {
|
||||
// Check if key matches any label hotkey
|
||||
const matchingLabel = spanLabelTypes.find((lt) => lt.hotkey === e.key && lt.is_active === 1);
|
||||
|
||||
if (matchingLabel && activeChartId) {
|
||||
e.preventDefault();
|
||||
|
||||
// Use cursor candle if available, otherwise use start candle
|
||||
const endCandleForHotkey = cursorCandle || startCandle;
|
||||
|
||||
// Swap if end < start
|
||||
const [start_time, end_time] =
|
||||
startCandle.time <= endCandleForHotkey.time
|
||||
? [startCandle.time, endCandleForHotkey.time]
|
||||
: [endCandleForHotkey.time, startCandle.time];
|
||||
|
||||
// Create span with hotkey label (default confidence/outcome/notes)
|
||||
try {
|
||||
const response = await fetch('/api/span-annotations', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
chart_id: activeChartId,
|
||||
start_time,
|
||||
end_time,
|
||||
label: matchingLabel.name,
|
||||
confidence: 3, // Default confidence
|
||||
outcome: null,
|
||||
notes: null,
|
||||
color: matchingLabel.color,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
onSpanAnnotationsChange();
|
||||
|
||||
// Clean up preview and reset state
|
||||
if (previewPrimitive && series) {
|
||||
series.detachPrimitive(previewPrimitive);
|
||||
setPreviewPrimitive(null);
|
||||
}
|
||||
setInteractionState('idle');
|
||||
setStartCandle(null);
|
||||
setEndCandle(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating span with hotkey:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [selectedSpanId, spanAnnotations]);
|
||||
}, [selectedSpanId, spanAnnotations, activeTool, interactionState, startCandle, cursorCandle, spanLabelTypes, activeChartId, previewPrimitive, series, onSpanAnnotationsChange]);
|
||||
|
||||
// Handle double-click to edit span
|
||||
useEffect(() => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue