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. Hotkey Label Assignment
|
||||||
|
|
||||||
- [ ] 10.1 Add keydown listener for span label hotkeys (active only when span tool is active and span range is selected)
|
- [x] 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
|
- [x] 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.3 Ignore hotkeys when span tool is inactive or no span range selected
|
||||||
|
|
||||||
## 11. Export Endpoints
|
## 11. Export Endpoints
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ export default function SpanAnnotationManager({
|
||||||
const [interactionState, setInteractionState] = useState<InteractionState>('idle');
|
const [interactionState, setInteractionState] = useState<InteractionState>('idle');
|
||||||
const [startCandle, setStartCandle] = useState<Candle | null>(null);
|
const [startCandle, setStartCandle] = useState<Candle | null>(null);
|
||||||
const [endCandle, setEndCandle] = 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 [previewPrimitive, setPreviewPrimitive] = useState<SpanRectanglePrimitive | null>(null);
|
||||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||||
const [editingSpan, setEditingSpan] = useState<SpanAnnotation | null>(null);
|
const [editingSpan, setEditingSpan] = useState<SpanAnnotation | null>(null);
|
||||||
|
|
@ -255,18 +256,21 @@ export default function SpanAnnotationManager({
|
||||||
if (!time) return;
|
if (!time) return;
|
||||||
|
|
||||||
const timestamp = typeof time === 'string' ? Date.parse(time) / 1000 : (time as number);
|
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
|
// 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
|
// Swap if end < start
|
||||||
const [start_time, end_time] =
|
const [start_time, end_time] =
|
||||||
startCandle.time <= cursorCandle.time
|
startCandle.time <= nearestCandle.time
|
||||||
? [startCandle.time, cursorCandle.time]
|
? [startCandle.time, nearestCandle.time]
|
||||||
: [cursorCandle.time, startCandle.time];
|
: [nearestCandle.time, startCandle.time];
|
||||||
|
|
||||||
const previewData: SpanData = {
|
const previewData: SpanData = {
|
||||||
id: -1, // Preview ID
|
id: -1, // Preview ID
|
||||||
|
|
@ -434,13 +438,14 @@ export default function SpanAnnotationManager({
|
||||||
setEditingSpan(null);
|
setEditingSpan(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle keyboard shortcuts for deletion and editing
|
// Handle keyboard shortcuts for deletion, editing, and hotkey label assignment
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = async (e: KeyboardEvent) => {
|
||||||
// Delete/Backspace: delete selected span
|
// Delete/Backspace: delete selected span
|
||||||
if ((e.key === 'Delete' || e.key === 'Backspace') && selectedSpanId !== null) {
|
if ((e.key === 'Delete' || e.key === 'Backspace') && selectedSpanId !== null) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleDeleteSpan(selectedSpanId);
|
handleDeleteSpan(selectedSpanId);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enter: open edit popover for selected span
|
// Enter: open edit popover for selected span
|
||||||
|
|
@ -450,12 +455,65 @@ export default function SpanAnnotationManager({
|
||||||
setEditingSpan(span);
|
setEditingSpan(span);
|
||||||
setPopoverOpen(true);
|
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);
|
window.addEventListener('keydown', handleKeyDown);
|
||||||
return () => window.removeEventListener('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
|
// Handle double-click to edit span
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue