/
/
/
1import { useState, useEffect, useCallback } from 'react';
2import { apiClient, ApiClient } from '../api/client';
3// import { useWebSocket } from './useWebSocket'; // Disabled for minimal testing
4import type {
5 RecordingStatus,
6 RecordingConfig,
7 RecordingFile,
8 // WebSocketMessage, // Disabled for minimal testing
9 AppState,
10 SelectionState
11} from '../api/types';
12import { ConnectionStatus } from '../api/types';
13
14interface UseRecordingReturn extends AppState {
15 // Actions
16 startRecording: (config: RecordingConfig) => Promise<void>;
17 stopRecording: () => Promise<void>;
18 refreshTopics: () => Promise<void>;
19 refreshRecordings: () => Promise<void>;
20 deleteRecording: (filename: string) => Promise<void>;
21
22 // Batch Actions
23 batchDownloadRecordings: (filenames: string[]) => Promise<void>;
24 batchDeleteRecordings: (filenames: string[]) => Promise<void>;
25 downloadSingleRecording: (filename: string) => Promise<void>;
26
27 // Selection State
28 selectionState: SelectionState;
29 toggleSelection: (filename: string) => void;
30 toggleSelectAll: () => void;
31 clearSelection: () => void;
32
33 // State
34 isLoading: boolean;
35 error: string | null;
36}
37
38export function useRecording(): UseRecordingReturn {
39 const [status, setStatus] = useState<RecordingStatus>({
40 is_recording: false,
41 current_bag_path: '',
42 recorded_messages: 0,
43 recording_duration: 0,
44 active_topics: [],
45 });
46
47 const [topics, setTopics] = useState<string[]>([]);
48 const [recordings, setRecordings] = useState<RecordingFile[]>([]);
49 const [isLoading, setIsLoading] = useState(false);
50 const [error, setError] = useState<string | null>(null);
51
52 // Selection State
53 const [selectionState, setSelectionState] = useState<SelectionState>({
54 selectedFilenames: [],
55 isAllSelected: false,
56 });
57
58 // const lastUpdateRef = useRef<number>(0); // Disabled for minimal testing
59 // const updateThrottleMs = 100; // Throttle updates to 10fps // Disabled for minimal testing
60
61 // WebSocket connection for real-time updates (disabled for minimal testing)
62 // const { connectionStatus } = useWebSocket({
63 // onMessage: handleWebSocketMessage,
64 // onError: (wsError) => {
65 // console.error('WebSocket error:', wsError);
66 // setError(`Connection error: ${wsError.message}`);
67 // },
68 // });
69
70 // Use HTTP polling as fallback instead of WebSocket
71 const [httpConnectionStatus, setHttpConnectionStatus] = useState<ConnectionStatus>(ConnectionStatus.DISCONNECTED);
72 const connectionStatus = httpConnectionStatus;
73
74 // Handle WebSocket messages (disabled for minimal testing)
75 // function handleWebSocketMessage(message: WebSocketMessage) {
76 // const now = Date.now();
77 //
78 // // Throttle status updates to prevent excessive re-renders
79 // if (message.type === 'status_update' && now - lastUpdateRef.current < updateThrottleMs) {
80 // return;
81 // }
82 // lastUpdateRef.current = now;
83 //
84 // switch (message.type) {
85 // case 'status_update':
86 // if (message.data) {
87 // setStatus(message.data);
88 // setError(null);
89 // }
90 // break;
91 //
92 // case 'recording_started':
93 // if (message.data) {
94 // setStatus(prev => ({ ...prev, is_recording: true }));
95 // setError(null);
96 // }
97 // break;
98 //
99 // case 'recording_stopped':
100 // setStatus(prev => ({ ...prev, is_recording: false }));
101 // refreshRecordings(); // Refresh recordings list
102 // break;
103 //
104 // case 'error_message':
105 // setError(message.data || 'Unknown error occurred');
106 // break;
107 //
108 // case 'topic_list_update':
109 // if (message.data?.topics) {
110 // setTopics(message.data.topics);
111 // }
112 // break;
113 //
114 // default:
115 // // Handle other message types or ignore
116 // break;
117 // }
118 // }
119
120 // Selection Management Functions
121 const toggleSelection = useCallback((filename: string) => {
122 setSelectionState(prev => {
123 const isSelected = prev.selectedFilenames.includes(filename);
124 const newSelectedFilenames = isSelected
125 ? prev.selectedFilenames.filter(f => f !== filename)
126 : [...prev.selectedFilenames, filename];
127
128 const isAllSelected = newSelectedFilenames.length === recordings.length && recordings.length > 0;
129
130 return {
131 selectedFilenames: newSelectedFilenames,
132 isAllSelected,
133 };
134 });
135 }, [recordings.length]);
136
137 const toggleSelectAll = useCallback(() => {
138 setSelectionState(prev => {
139 const newIsAllSelected = !prev.isAllSelected;
140 return {
141 selectedFilenames: newIsAllSelected ? recordings.map(r => r.name) : [],
142 isAllSelected: newIsAllSelected,
143 };
144 });
145 }, [recordings]);
146
147 const clearSelection = useCallback(() => {
148 setSelectionState({
149 selectedFilenames: [],
150 isAllSelected: false,
151 });
152 }, []);
153
154 // Update selection state when recordings change
155 useEffect(() => {
156 setSelectionState(prev => {
157 const availableFilenames = recordings.map(r => r.name);
158 const filteredSelected = prev.selectedFilenames.filter(filename =>
159 availableFilenames.includes(filename)
160 );
161 const isAllSelected = filteredSelected.length === recordings.length && recordings.length > 0;
162
163 return {
164 selectedFilenames: filteredSelected,
165 isAllSelected,
166 };
167 });
168 }, [recordings]);
169
170 // API Actions
171 const startRecording = useCallback(async (config: RecordingConfig) => {
172 try {
173 setIsLoading(true);
174 setError(null);
175
176 const response = await apiClient.startRecording(config);
177
178 if (!response.success) {
179 throw new Error(response.message || 'Failed to start recording');
180 }
181
182 // Status will be updated via WebSocket
183 } catch (err) {
184 const errorMessage = err instanceof Error ? err.message : 'Failed to start recording';
185 setError(errorMessage);
186 throw err;
187 } finally {
188 setIsLoading(false);
189 }
190 }, []);
191
192 const stopRecording = useCallback(async () => {
193 try {
194 setIsLoading(true);
195 setError(null);
196
197 const response = await apiClient.stopRecording();
198
199 if (!response.success) {
200 throw new Error(response.message || 'Failed to stop recording');
201 }
202
203 // Status will be updated via WebSocket
204 } catch (err) {
205 const errorMessage = err instanceof Error ? err.message : 'Failed to stop recording';
206 setError(errorMessage);
207 throw err;
208 } finally {
209 setIsLoading(false);
210 }
211 }, []);
212
213 const refreshTopics = useCallback(async () => {
214 try {
215 setError(null);
216 const topicsList = await apiClient.getTopics();
217 setTopics(topicsList);
218 } catch (err) {
219 const errorMessage = err instanceof Error ? err.message : 'Failed to fetch topics';
220 setError(errorMessage);
221 console.error('Failed to fetch topics:', err);
222 }
223 }, []);
224
225 const refreshRecordings = useCallback(async () => {
226 try {
227 setError(null);
228 const response = await apiClient.getRecordings();
229 setRecordings(response.recordings);
230 } catch (err) {
231 const errorMessage = err instanceof Error ? err.message : 'Failed to fetch recordings';
232 setError(errorMessage);
233 console.error('Failed to fetch recordings:', err);
234 }
235 }, []);
236
237 const deleteRecording = useCallback(async (filename: string) => {
238 try {
239 setIsLoading(true);
240 setError(null);
241
242 const response = await apiClient.deleteRecording(filename);
243
244 if (!response.success) {
245 throw new Error(response.message || 'Failed to delete recording');
246 }
247
248 // Refresh recordings list
249 await refreshRecordings();
250 } catch (err) {
251 const errorMessage = err instanceof Error ? err.message : 'Failed to delete recording';
252 setError(errorMessage);
253 throw err;
254 } finally {
255 setIsLoading(false);
256 }
257 }, [refreshRecordings]);
258
259 // Batch Operations
260 const batchDownloadRecordings = useCallback(async (filenames: string[]) => {
261 try {
262 setIsLoading(true);
263 setError(null);
264
265 if (filenames.length === 0) {
266 throw new Error('No files selected for download');
267 }
268
269 if (filenames.length === 1) {
270 // Use single file download for better performance
271 await downloadSingleRecording(filenames[0]);
272 return;
273 }
274
275 const blob = await apiClient.batchDownloadRecordings(filenames);
276
277 // Generate filename for batch download
278 const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
279 const filename = `batch_recordings_${timestamp}.tar.gz`;
280
281 // Trigger download from blob
282 ApiClient.triggerDownload(blob, filename);
283 } catch (err) {
284 const errorMessage = err instanceof Error ? err.message : 'Failed to download recordings';
285 setError(errorMessage);
286 throw err;
287 } finally {
288 setIsLoading(false);
289 }
290 }, []);
291
292 const batchDeleteRecordings = useCallback(async (filenames: string[]) => {
293 try {
294 setIsLoading(true);
295 setError(null);
296
297 if (filenames.length === 0) {
298 throw new Error('No files selected for deletion');
299 }
300
301 const response = await apiClient.batchDeleteRecordings(filenames);
302
303 if (!response.success) {
304 throw new Error(response.message || 'Failed to delete recordings');
305 }
306
307 // Clear selection and refresh recordings list
308 clearSelection();
309 await refreshRecordings();
310
311 if (response.failed_deletions && response.failed_deletions.length > 0) {
312 setError(`Failed to delete some files: ${response.failed_deletions.join(', ')}`);
313 }
314 } catch (err) {
315 const errorMessage = err instanceof Error ? err.message : 'Failed to delete recordings';
316 setError(errorMessage);
317 throw err;
318 } finally {
319 setIsLoading(false);
320 }
321 }, [clearSelection, refreshRecordings]);
322
323 const downloadSingleRecording = useCallback(async (filename: string) => {
324 try {
325 setIsLoading(true);
326 setError(null);
327
328 const blob = await apiClient.downloadRecording(filename);
329 ApiClient.triggerDownload(blob, `${filename}.tar.gz`);
330 } catch (err) {
331 const errorMessage = err instanceof Error ? err.message : 'Failed to download recording';
332 setError(errorMessage);
333 throw err;
334 } finally {
335 setIsLoading(false);
336 }
337 }, []);
338
339 // Check API connection status
340 const checkConnectionStatus = useCallback(async () => {
341 try {
342 const healthCheck = await apiClient.healthCheck();
343 setHttpConnectionStatus(healthCheck ? ConnectionStatus.CONNECTED : ConnectionStatus.ERROR);
344 return healthCheck;
345 } catch (err) {
346 setHttpConnectionStatus(ConnectionStatus.ERROR);
347 return false;
348 }
349 }, []);
350
351 // Initial data loading
352 useEffect(() => {
353 const loadInitialData = async () => {
354 setIsLoading(true);
355 try {
356 // Check API connection
357 const isConnected = await checkConnectionStatus();
358
359 if (isConnected) {
360 // Load initial status
361 const initialStatus = await apiClient.getStatus();
362 setStatus(initialStatus);
363
364 // Load topics and recordings
365 await Promise.all([
366 refreshTopics(),
367 refreshRecordings(),
368 ]);
369
370 setError(null);
371 } else {
372 setError('Cannot connect to backend API');
373 }
374 } catch (err) {
375 const errorMessage = err instanceof Error ? err.message : 'Failed to load initial data';
376 setError(errorMessage);
377 console.error('Failed to load initial data:', err);
378 } finally {
379 setIsLoading(false);
380 }
381 };
382
383 loadInitialData();
384 }, [checkConnectionStatus, refreshTopics, refreshRecordings]);
385
386 // Periodic data refresh for HTTP polling
387 useEffect(() => {
388 const interval = setInterval(async () => {
389 try {
390 // Check connection and update status
391 const isConnected = await checkConnectionStatus();
392 if (isConnected) {
393 const currentStatus = await apiClient.getStatus();
394 setStatus(currentStatus);
395 }
396 } catch (err) {
397 console.error('Failed to refresh status:', err);
398 setHttpConnectionStatus(ConnectionStatus.ERROR);
399 }
400 }, 3000); // Refresh every 3 seconds
401
402 return () => clearInterval(interval);
403 }, [checkConnectionStatus]);
404
405 return {
406 // State
407 status,
408 topics,
409 recordings,
410 connectionStatus,
411 lastError: error || undefined,
412 isLoading,
413 error,
414
415 // Actions
416 startRecording,
417 stopRecording,
418 refreshTopics,
419 refreshRecordings,
420 deleteRecording,
421
422 // Batch Actions
423 batchDownloadRecordings,
424 batchDeleteRecordings,
425 downloadSingleRecording,
426
427 // Selection State
428 selectionState,
429 toggleSelection,
430 toggleSelectAll,
431 clearSelection,
432 };
433}