Skip to content

Commit adfb326

Browse files
alireza787bclaude
andcommitted
fix(dashboard): pre-select active model, clickable labels, keyboard shortcuts
- ModelQuickControl: dropdown pre-selects the currently active model - ModelQuickControl: label count is clickable — opens labels dialog - DashboardPage: M key toggles smart mode, R key toggles recording - LiveFeedPage: R key toggles recording - Keyboard handlers skip events when focused on input/select/textarea Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5b8ce00 commit adfb326

File tree

3 files changed

+108
-9
lines changed

3 files changed

+108
-9
lines changed

dashboard/src/components/ModelQuickControl.js

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111
* Design: Matches RecordingQuickControl / OSDToggle pattern.
1212
*/
1313

14-
import React, { useState } from 'react';
14+
import React, { useState, useEffect } from 'react';
1515
import {
1616
Box, Chip, Select, MenuItem, FormControl, InputLabel, IconButton,
1717
Typography, Tooltip, CircularProgress, Button,
18+
Dialog, DialogTitle, DialogContent, DialogActions,
1819
} from '@mui/material';
1920
import SmartToyIcon from '@mui/icons-material/SmartToy';
2021
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
@@ -23,14 +24,16 @@ import OpenInNewIcon from '@mui/icons-material/OpenInNew';
2324
import MemoryIcon from '@mui/icons-material/Memory';
2425
import KeyboardIcon from '@mui/icons-material/Keyboard';
2526
import { Link } from 'react-router-dom';
26-
import { useActiveModel, useModels, useSwitchModel } from '../hooks/useModels';
27+
import { useActiveModel, useModels, useSwitchModel, useModelLabels } from '../hooks/useModels';
2728

2829
const ModelQuickControl = () => {
2930
const { activeModel, runtime, loading: activeLoading } = useActiveModel(5000);
3031
const { models, loading: modelsLoading } = useModels(15000);
3132
const { switchModel, switching } = useSwitchModel();
33+
const { fetchLabels, loading: labelsLoading } = useModelLabels();
3234
const [selectedModelPath, setSelectedModelPath] = useState('');
3335
const [selectedDevice, setSelectedDevice] = useState('auto');
36+
const [labelsDialog, setLabelsDialog] = useState({ open: false, labels: [], modelName: '' });
3437

3538
// activeModel is the full active_model_summary object from /api/models/active
3639
const modelName = runtime?.model_name || activeModel?.model_name || 'None';
@@ -44,10 +47,26 @@ const ModelQuickControl = () => {
4447

4548
const modelList = models ? Object.entries(models) : [];
4649

50+
// Pre-select the active model in the dropdown when it changes
51+
const activeModelPath = activeModel?.model_path || '';
52+
useEffect(() => {
53+
if (activeModelPath) {
54+
setSelectedModelPath(activeModelPath);
55+
}
56+
}, [activeModelPath]);
57+
4758
const handleSwitch = async () => {
4859
if (!selectedModelPath) return;
4960
await switchModel(selectedModelPath, selectedDevice);
50-
setSelectedModelPath('');
61+
};
62+
63+
const handleViewLabels = async () => {
64+
const modelId = activeModel?.model_id;
65+
if (!modelId) return;
66+
const result = await fetchLabels(modelId);
67+
if (result.success) {
68+
setLabelsDialog({ open: true, labels: result.labels, modelName: modelName });
69+
}
5170
};
5271

5372
if (activeLoading) {
@@ -110,9 +129,20 @@ const ModelQuickControl = () => {
110129
<Typography variant="caption" color="text.secondary" sx={{ fontSize: 10 }}>
111130
Task: <b>{task}</b>
112131
</Typography>
113-
<Typography variant="caption" color="text.secondary" sx={{ fontSize: 10 }}>
114-
Classes: <b>{numLabels}</b>
115-
</Typography>
132+
<Tooltip title={hasModel ? 'Click to view class labels' : ''}>
133+
<Typography
134+
variant="caption"
135+
color={hasModel ? 'primary' : 'text.secondary'}
136+
onClick={hasModel ? handleViewLabels : undefined}
137+
sx={{
138+
fontSize: 10,
139+
cursor: hasModel ? 'pointer' : 'default',
140+
'&:hover': hasModel ? { textDecoration: 'underline' } : {},
141+
}}
142+
>
143+
Classes: <b>{labelsLoading ? '...' : numLabels}</b>
144+
</Typography>
145+
</Tooltip>
116146
</Box>
117147

118148
{/* Quick Switch Row */}
@@ -184,6 +214,37 @@ const ModelQuickControl = () => {
184214
</Typography>
185215
</Tooltip>
186216
</Box>
217+
218+
{/* Labels Dialog */}
219+
<Dialog
220+
open={labelsDialog.open}
221+
onClose={() => setLabelsDialog({ open: false, labels: [], modelName: '' })}
222+
maxWidth="sm"
223+
fullWidth
224+
>
225+
<DialogTitle>Labels: {labelsDialog.modelName}</DialogTitle>
226+
<DialogContent dividers>
227+
{labelsDialog.labels.length === 0 ? (
228+
<Typography color="text.secondary">No labels available.</Typography>
229+
) : (
230+
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
231+
{labelsDialog.labels.map((item, idx) => (
232+
<Chip
233+
key={idx}
234+
label={typeof item === 'object' ? `${item.class_id}: ${item.label}` : `${idx}: ${item}`}
235+
size="small"
236+
variant="outlined"
237+
/>
238+
))}
239+
</Box>
240+
)}
241+
</DialogContent>
242+
<DialogActions>
243+
<Button onClick={() => setLabelsDialog({ open: false, labels: [], modelName: '' })}>
244+
Close
245+
</Button>
246+
</DialogActions>
247+
</Dialog>
187248
</Box>
188249
);
189250
};

dashboard/src/pages/DashboardPage.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// dashboard/src/pages/DashboardPage.js
2-
import React, { useState, useEffect } from 'react';
2+
import React, { useState, useEffect, useCallback } from 'react';
33
import {
44
Container, Typography, CircularProgress, Box, Grid, Snackbar, Alert,
55
FormControl, InputLabel, Select, MenuItem, Card, CardContent,
@@ -183,6 +183,27 @@ const DashboardPage = () => {
183183

184184
const handleSnackbarClose = () => setSnackbarOpen(false);
185185

186+
// Keyboard shortcuts: M = toggle smart mode, R = toggle recording
187+
const handleKeyboardShortcut = useCallback((e) => {
188+
const tag = e.target.tagName;
189+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || e.target.isContentEditable) return;
190+
191+
const key = e.key.toLowerCase();
192+
if (key === 'm') {
193+
handleToggleSmartMode();
194+
} else if (key === 'r') {
195+
fetch(endpoints.recordingToggle, {
196+
method: 'POST',
197+
headers: { 'Content-Type': 'application/json' },
198+
}).catch((err) => console.error('Recording toggle failed:', err));
199+
}
200+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
201+
202+
useEffect(() => {
203+
window.addEventListener('keydown', handleKeyboardShortcut);
204+
return () => window.removeEventListener('keydown', handleKeyboardShortcut);
205+
}, [handleKeyboardShortcut]);
206+
186207
useEffect(() => {
187208
if (streamingProtocol === 'websocket' || streamingProtocol === 'webrtc' || streamingProtocol === 'auto') {
188209
setLoading(false);

dashboard/src/pages/LiveFeedPage.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// dashboard/src/pages/LiveFeedPage.js
2-
import React, { useState, useEffect } from 'react';
2+
import React, { useState, useEffect, useCallback } from 'react';
33
import {
44
Container,
55
Typography,
@@ -22,7 +22,7 @@ import GStreamerQGCPanel from '../components/GStreamerQGCPanel';
2222
import StreamingStats from '../components/StreamingStats';
2323
import RecordingQuickControl from '../components/RecordingQuickControl';
2424
import RecordingIndicator from '../components/RecordingIndicator';
25-
import { videoFeed } from '../services/apiEndpoints';
25+
import { videoFeed, endpoints } from '../services/apiEndpoints';
2626

2727
const LiveFeedPage = () => {
2828
const [loading, setLoading] = useState(true);
@@ -81,6 +81,23 @@ const LiveFeedPage = () => {
8181

8282
const connectionState = getConnectionState();
8383

84+
// Keyboard shortcut: R = toggle recording
85+
const handleKeyboardShortcut = useCallback((e) => {
86+
const tag = e.target.tagName;
87+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || e.target.isContentEditable) return;
88+
if (e.key.toLowerCase() === 'r') {
89+
fetch(endpoints.recordingToggle, {
90+
method: 'POST',
91+
headers: { 'Content-Type': 'application/json' },
92+
}).catch((err) => console.error('Recording toggle failed:', err));
93+
}
94+
}, []);
95+
96+
useEffect(() => {
97+
window.addEventListener('keydown', handleKeyboardShortcut);
98+
return () => window.removeEventListener('keydown', handleKeyboardShortcut);
99+
}, [handleKeyboardShortcut]);
100+
84101
useEffect(() => {
85102
if (streamingProtocol === 'websocket' || streamingProtocol === 'webrtc' || streamingProtocol === 'auto') {
86103
// WebSocket/WebRTC/Auto manage their own connection state

0 commit comments

Comments
 (0)