/
/
/
1import { useState, useCallback } from 'react';
2import { Play, Square, Settings, FolderOpen } from 'lucide-react';
3import type { RecordingControlsProps, RecordingConfig } from '../api/types';
4
5export function RecordingControls({
6 status,
7 isConnected,
8 onStartRecording,
9 onStopRecording,
10}: RecordingControlsProps) {
11 const [isSubmitting, setIsSubmitting] = useState(false);
12 const [showSettings, setShowSettings] = useState(false);
13 const [formData, setFormData] = useState({
14 outputPath: '/tmp/rosbags',
15 storageId: 'sqlite3',
16 serializationFormat: 'cdr',
17 maxBagfileSize: '',
18 maxBagfileDuration: '',
19 });
20
21 const handleStart = useCallback(async () => {
22 setIsSubmitting(true);
23 try {
24 const config: RecordingConfig = {
25 output_path: formData.outputPath || `/tmp/rosbags/recording_${Date.now()}`,
26 topics: [], // Will be populated by TopicSelector
27 storage_id: formData.storageId,
28 serialization_format: formData.serializationFormat,
29 max_bagfile_size: formData.maxBagfileSize ? parseInt(formData.maxBagfileSize) : 0,
30 max_bagfile_duration: formData.maxBagfileDuration ? parseFloat(formData.maxBagfileDuration) : 0,
31 };
32
33 await onStartRecording(config);
34 } catch (error) {
35 console.error('Failed to start recording:', error);
36 } finally {
37 setIsSubmitting(false);
38 }
39 }, [formData, onStartRecording]);
40
41 const handleStop = useCallback(async () => {
42 setIsSubmitting(true);
43 try {
44 await onStopRecording();
45 } catch (error) {
46 console.error('Failed to stop recording:', error);
47 } finally {
48 setIsSubmitting(false);
49 }
50 }, [onStopRecording]);
51
52 const handleInputChange = (field: string, value: string) => {
53 setFormData(prev => ({ ...prev, [field]: value }));
54 };
55
56 const canStart = isConnected && !status.is_recording && !isSubmitting;
57 const canStop = isConnected && status.is_recording && !isSubmitting;
58
59 return (
60 <div className="bg-white rounded-lg shadow-md p-6">
61 <div className="flex items-center justify-between mb-6">
62 <h2 className="text-xl font-semibold text-gray-800">Recording Controls</h2>
63 <button
64 onClick={() => setShowSettings(!showSettings)}
65 className="p-2 text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors"
66 title="Recording Settings"
67 >
68 <Settings size={20} />
69 </button>
70 </div>
71
72 {/* Settings Panel */}
73 {showSettings && (
74 <div className="mb-6 p-4 bg-gray-50 rounded-lg border">
75 <h3 className="text-sm font-medium text-gray-700 mb-3">Recording Settings</h3>
76
77 <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
78 <div>
79 <label className="block text-sm font-medium text-gray-700 mb-1">
80 Output Path
81 </label>
82 <div className="relative">
83 <input
84 type="text"
85 value={formData.outputPath}
86 onChange={(e) => handleInputChange('outputPath', e.target.value)}
87 className="w-full px-3 py-2 pr-10 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
88 placeholder="/tmp/rosbags"
89 />
90 <FolderOpen
91 size={16}
92 className="absolute right-3 top-3 text-gray-400"
93 />
94 </div>
95 </div>
96
97 <div>
98 <label className="block text-sm font-medium text-gray-700 mb-1">
99 Storage Format
100 </label>
101 <select
102 value={formData.storageId}
103 onChange={(e) => handleInputChange('storageId', e.target.value)}
104 className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
105 >
106 <option value="sqlite3">SQLite3</option>
107 <option value="mcap">MCAP</option>
108 </select>
109 </div>
110
111 <div>
112 <label className="block text-sm font-medium text-gray-700 mb-1">
113 Serialization Format
114 </label>
115 <select
116 value={formData.serializationFormat}
117 onChange={(e) => handleInputChange('serializationFormat', e.target.value)}
118 className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
119 >
120 <option value="cdr">CDR</option>
121 <option value="json">JSON</option>
122 </select>
123 </div>
124
125 <div>
126 <label className="block text-sm font-medium text-gray-700 mb-1">
127 Max File Size (MB)
128 </label>
129 <input
130 type="number"
131 value={formData.maxBagfileSize}
132 onChange={(e) => handleInputChange('maxBagfileSize', e.target.value)}
133 className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
134 placeholder="0 (unlimited)"
135 min="0"
136 />
137 </div>
138
139 <div className="md:col-span-2">
140 <label className="block text-sm font-medium text-gray-700 mb-1">
141 Max Duration (seconds)
142 </label>
143 <input
144 type="number"
145 value={formData.maxBagfileDuration}
146 onChange={(e) => handleInputChange('maxBagfileDuration', e.target.value)}
147 className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
148 placeholder="0 (unlimited)"
149 min="0"
150 step="0.1"
151 />
152 </div>
153 </div>
154 </div>
155 )}
156
157 {/* Control Buttons */}
158 <div className="flex space-x-4">
159 <button
160 onClick={handleStart}
161 disabled={!canStart}
162 className={`
163 flex items-center space-x-2 px-6 py-3 rounded-lg font-medium transition-all
164 ${canStart
165 ? 'bg-green-600 hover:bg-green-700 text-white shadow-md hover:shadow-lg'
166 : 'bg-gray-300 text-gray-500 cursor-not-allowed'
167 }
168 `}
169 >
170 <Play size={20} fill="currentColor" />
171 <span>{isSubmitting && !status.is_recording ? 'Starting...' : 'Start Recording'}</span>
172 </button>
173
174 <button
175 onClick={handleStop}
176 disabled={!canStop}
177 className={`
178 flex items-center space-x-2 px-6 py-3 rounded-lg font-medium transition-all
179 ${canStop
180 ? 'bg-red-600 hover:bg-red-700 text-white shadow-md hover:shadow-lg'
181 : 'bg-gray-300 text-gray-500 cursor-not-allowed'
182 }
183 `}
184 >
185 <Square size={20} fill="currentColor" />
186 <span>{isSubmitting && status.is_recording ? 'Stopping...' : 'Stop Recording'}</span>
187 </button>
188 </div>
189
190 {/* Current Recording Info */}
191 {status.is_recording && (
192 <div className="mt-4 p-3 bg-green-50 border border-green-200 rounded-lg">
193 <div className="text-sm text-green-700">
194 <div className="font-medium">Recording Active</div>
195 {status.current_bag_path && (
196 <div className="text-xs text-green-600 mt-1">
197 Path: {status.current_bag_path}
198 </div>
199 )}
200 </div>
201 </div>
202 )}
203
204 {/* Connection Status Warning */}
205 {!isConnected && (
206 <div className="mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
207 <div className="text-sm text-yellow-700">
208 <div className="font-medium">Connection Lost</div>
209 <div className="text-xs text-yellow-600 mt-1">
210 Attempting to reconnect...
211 </div>
212 </div>
213 </div>
214 )}
215 </div>
216 );
217}