import React, { useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { AlertCircle, Map, MapPin, Settings, Zap, CheckCircle, XCircle, Loader2 } from 'lucide-react';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { mcpService } from '@/services/mcpService';
import { MapVisualization } from '@/components/MapVisualization';
interface DreameRobot {
robot_id: string;
robot_type: 'dreame';
connected: boolean;
battery?: number;
status?: string;
position?: { x: number; y: number };
map_available?: boolean;
}
interface DreameControlsProps {
robot: DreameRobot;
onCommand: (robotId: string, command: any) => void;
}
export function DreameControls({ robot, onCommand }: DreameControlsProps) {
const [suctionLevel, setSuctionLevel] = useState('2');
const [waterVolume, setWaterVolume] = useState('2');
const [mopHumidity, setMopHumidity] = useState('2');
const [roomId, setRoomId] = useState('1');
const [zoneCoords, setZoneCoords] = useState('0,0,200,200');
const [spotCoords, setSpotCoords] = useState('150,150');
const [cleaningSequence, setCleaningSequence] = useState('1,2,3');
// LIDAR map data for display
const [mapData, setMapData] = useState<{
map_id?: string;
dreame_map?: { map_data?: unknown; scale_factor?: number; origin_x?: number; origin_y?: number };
obstacles?: Array<{ x: number; y: number; size?: number }>;
rooms?: Array<{ id: string; name: string; coordinates: number[] }>;
} | null>(null);
// Loading states for different operations
const [loadingStates, setLoadingStates] = useState<Record<string, boolean>>({});
const [lastResult, setLastResult] = useState<{success: boolean, message: string} | null>(null);
const setLoading = (action: string, loading: boolean) => {
setLoadingStates(prev => ({ ...prev, [action]: loading }));
};
const sendCommand = async (action: string, params: any = {}) => {
setLoading(action, true);
setLastResult(null);
try {
const result = await mcpService.dreameCommand(robot.robot_id, action, params);
if (result.success) {
setLastResult({ success: true, message: result.message || `Command ${action} executed successfully` });
// Also call the original onCommand for any additional handling
onCommand(robot.robot_id, {
action,
robot_id: robot.robot_id,
...params,
result
});
} else {
setLastResult({ success: false, message: result.error || `Command ${action} failed` });
}
} catch (error) {
setLastResult({
success: false,
message: `Network error: ${error instanceof Error ? error.message : String(error)}`
});
} finally {
setLoading(action, false);
}
};
return (
<Card className="w-full">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Zap className="w-5 h-5 text-blue-500" />
Dreame D20 Pro Controls
</CardTitle>
<CardDescription>
Advanced vacuum robot with LIDAR mapping and zone cleaning
</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="basic" className="w-full">
<TabsList className="grid w-full grid-cols-5">
<TabsTrigger value="basic">Basic</TabsTrigger>
<TabsTrigger value="map">Map</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
<TabsTrigger value="zones">Zones</TabsTrigger>
<TabsTrigger value="advanced">Advanced</TabsTrigger>
</TabsList>
<TabsContent value="basic" className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<Button
onClick={() => sendCommand('start_cleaning')}
className="bg-green-600 hover:bg-green-700"
disabled={!robot.connected || loadingStates['start_cleaning']}
>
{loadingStates['start_cleaning'] ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
'π§Ή '
)}
{loadingStates['start_cleaning'] ? 'Starting...' : 'Start Cleaning'}
</Button>
<Button
onClick={() => sendCommand('stop_cleaning')}
variant="destructive"
disabled={!robot.connected || loadingStates['stop_cleaning']}
>
{loadingStates['stop_cleaning'] ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
'βΉοΈ '
)}
{loadingStates['stop_cleaning'] ? 'Stopping...' : 'Stop Cleaning'}
</Button>
<Button
onClick={() => sendCommand('return_to_dock')}
className="bg-blue-600 hover:bg-blue-700"
disabled={!robot.connected || loadingStates['return_to_dock']}
>
{loadingStates['return_to_dock'] ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
'π '
)}
{loadingStates['return_to_dock'] ? 'Returning...' : 'Return to Dock'}
</Button>
<Button
onClick={() => sendCommand('get_status')}
variant="outline"
disabled={!robot.connected || loadingStates['get_status']}
>
{loadingStates['get_status'] ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
'π '
)}
{loadingStates['get_status'] ? 'Getting...' : 'Get Status'}
</Button>
</div>
</TabsContent>
<TabsContent value="map" className="space-y-4">
<div className="flex items-center justify-between gap-4">
<p className="text-sm text-gray-600">
Fetch and display LIDAR map from the robot. Requires robot to have completed at least one mapping run.
</p>
<Button
onClick={async () => {
setLoadingStates(prev => ({ ...prev, get_map: true }));
setLastResult(null);
try {
const result = await mcpService.dreameMap(robot.robot_id, false, false);
if (result.success && result.data) {
const wrappedMap = {
map_id: `dreame_${robot.robot_id}_${Date.now()}`,
dreame_map: {
map_data: result.data,
scale_factor: (result as { scale_factor?: number }).scale_factor ?? 0.001,
origin_x: 0,
origin_y: 0
},
obstacles: [],
rooms: []
};
setMapData(wrappedMap);
setLastResult({
success: true,
message: `LIDAR map loaded (${result.data?.map_format ?? 'SLAM'} format)`
});
onCommand(robot.robot_id, {
action: 'map_loaded',
robot_id: robot.robot_id,
map_data: wrappedMap
});
} else {
setLastResult({
success: false,
message: result.error || 'No map data returned'
});
}
} catch (e) {
setLastResult({ success: false, message: String(e) });
} finally {
setLoadingStates(prev => ({ ...prev, get_map: false }));
}
}}
className="bg-purple-600 hover:bg-purple-700 shrink-0"
disabled={!robot.connected || loadingStates['get_map']}
>
{loadingStates['get_map'] ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Map className="w-4 h-4 mr-2" />
)}
{loadingStates['get_map'] ? 'Loading...' : 'Fetch LIDAR Map'}
</Button>
</div>
{mapData ? (
<MapVisualization
robots={[{
robot_id: robot.robot_id,
robot_type: 'dreame',
name: 'Dreame D20 Pro',
position: robot.position ?? { x: 0, y: 0 },
rotation: { yaw: 0 },
connected: robot.connected,
status: robot.status
}]}
mapData={mapData}
onRobotCommand={(id, cmd) => onCommand(id, cmd)}
onLoadDreameMap={async (id) => {
const result = await mcpService.dreameMap(id, false, false);
if (result.success && result.data) {
setMapData({
map_id: `dreame_${id}_${Date.now()}`,
dreame_map: { map_data: result.data, scale_factor: 0.001, origin_x: 0, origin_y: 0 },
obstacles: [],
rooms: []
});
}
}}
/>
) : (
<Alert>
<Map className="h-4 w-4" />
<AlertDescription>
No LIDAR map loaded. Click "Fetch LIDAR Map" to acquire map data from the robot.
</AlertDescription>
</Alert>
)}
</TabsContent>
<TabsContent value="settings" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label htmlFor="suction">Suction Level</Label>
<Select value={suctionLevel} onValueChange={setSuctionLevel}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">Quiet (1)</SelectItem>
<SelectItem value="2">Standard (2)</SelectItem>
<SelectItem value="3">Turbo (3)</SelectItem>
<SelectItem value="4">Max (4)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="water">Water Volume</Label>
<Select value={waterVolume} onValueChange={setWaterVolume}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">Low (1)</SelectItem>
<SelectItem value="2">Medium (2)</SelectItem>
<SelectItem value="3">High (3)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="humidity">Mop Humidity</Label>
<Select value={mopHumidity} onValueChange={setMopHumidity}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="1">Low (1)</SelectItem>
<SelectItem value="2">Medium (2)</SelectItem>
<SelectItem value="3">High (3)</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<Button
onClick={async () => {
setLoadingStates(prev => ({ ...prev, apply_settings: true }))
setLastResult(null)
try {
const result1 = await mcpService.dreameCommand(robot.robot_id, 'set_suction_level', { suction_level: parseInt(suctionLevel) })
const result2 = await mcpService.dreameCommand(robot.robot_id, 'set_water_volume', { water_volume: parseInt(waterVolume) })
const result3 = await mcpService.dreameCommand(robot.robot_id, 'set_mop_humidity', { mop_humidity: parseInt(mopHumidity) })
const ok = result1.success && result2.success && result3.success
setLastResult({ success: ok, message: ok ? 'Settings applied' : (result3.error || 'Failed to apply settings') })
} catch (e) {
setLastResult({ success: false, message: String(e) })
} finally {
setLoadingStates(prev => ({ ...prev, apply_settings: false }))
}
}}
className="w-full bg-orange-600 hover:bg-orange-700"
disabled={!robot.connected || loadingStates['apply_settings']}
>
{loadingStates['apply_settings'] ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Settings className="w-4 h-4 mr-2" />}
Apply Settings
</Button>
</TabsContent>
<TabsContent value="zones" className="space-y-4">
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="room-id">Room ID for Room Cleaning</Label>
<Input
id="room-id"
type="number"
min="1"
max="10"
value={roomId}
onChange={(e) => setRoomId(e.target.value)}
placeholder="1"
/>
<Button
onClick={() => sendCommand('clean_room', { room_id: parseInt(roomId) })}
className="w-full bg-indigo-600 hover:bg-indigo-700"
disabled={!robot.connected}
>
π Clean Room {roomId}
</Button>
</div>
<div className="space-y-2">
<Label htmlFor="zone-coords">Zone Coordinates (x1,y1,x2,y2)</Label>
<Input
id="zone-coords"
value={zoneCoords}
onChange={(e) => setZoneCoords(e.target.value)}
placeholder="0,0,200,200"
/>
<Button
onClick={() => {
const coords = zoneCoords.split(',').map(c => parseInt(c.trim()));
sendCommand('clean_zone', { zones: [coords] });
}}
className="w-full bg-teal-600 hover:bg-teal-700"
disabled={!robot.connected}
>
π Clean Zone
</Button>
</div>
<div className="space-y-2">
<Label htmlFor="spot-coords">Spot Coordinates (x,y)</Label>
<Input
id="spot-coords"
value={spotCoords}
onChange={(e) => setSpotCoords(e.target.value)}
placeholder="150,150"
/>
<Button
onClick={() => {
const coords = spotCoords.split(',').map(c => parseInt(c.trim()));
sendCommand('clean_spot', { spot_x: coords[0], spot_y: coords[1] });
}}
className="w-full bg-pink-600 hover:bg-pink-700"
disabled={!robot.connected}
>
π― Clean Spot
</Button>
</div>
</div>
</TabsContent>
<TabsContent value="advanced" className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Button
onClick={() => sendCommand('start_mapping')}
className="bg-cyan-600 hover:bg-cyan-700"
disabled={!robot.connected}
>
πΊοΈ Start Mapping
</Button>
<Button
onClick={() => sendCommand('start_fast_mapping')}
className="bg-cyan-700 hover:bg-cyan-800"
disabled={!robot.connected}
>
β‘ Fast Mapping
</Button>
</div>
<div className="space-y-2">
<Label htmlFor="sequence">Cleaning Sequence (room IDs)</Label>
<Input
id="sequence"
value={cleaningSequence}
onChange={(e) => setCleaningSequence(e.target.value)}
placeholder="1,2,3"
/>
<Button
onClick={() => {
const sequence = cleaningSequence.split(',').map(s => parseInt(s.trim()));
sendCommand('set_cleaning_sequence', { cleaning_sequence: sequence });
}}
className="w-full bg-violet-600 hover:bg-violet-700"
disabled={!robot.connected}
>
π Set Sequence
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Button
onClick={() => sendCommand('get_cleaning_history')}
variant="outline"
disabled={!robot.connected}
>
π Get History
</Button>
<Button
onClick={() => sendCommand('clear_error')}
variant="outline"
disabled={!robot.connected}
>
π§ Clear Error
</Button>
</div>
</TabsContent>
</Tabs>
{!robot.connected && (
<Alert className="mt-4">
<AlertCircle className="h-4 w-4" />
<AlertDescription>
Dreame robot is not connected. Please check the robotics MCP server and robot configuration.
</AlertDescription>
</Alert>
)}
{lastResult && (
<Alert className={`mt-4 ${lastResult.success ? 'border-green-500 bg-green-50' : 'border-red-500 bg-red-50'}`}>
{lastResult.success ? (
<CheckCircle className="h-4 w-4 text-green-600" />
) : (
<XCircle className="h-4 w-4 text-red-600" />
)}
<AlertDescription className={lastResult.success ? 'text-green-800' : 'text-red-800'}>
{lastResult.message}
</AlertDescription>
</Alert>
)}
</CardContent>
</Card>
);
}