Files
remoteturtle/client/src/components/ControlPanel.jsx

511 lines
19 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useCallback } from 'react';
import { useTurtleStore } from '../store/turtleStore';
import './ControlPanel.css';
function TurtleCard({ turtle, isSelected, onSelect }) {
const activeState = turtle.state || turtle.mode || 'idle';
const fuel = turtle.fuel === 'unlimited' ? '∞' : (turtle.fuel || '?');
const inventoryCount = turtle.inventory?.length || 0;
const displayName = turtle.label || `Turtle ${turtle.turtleID}`;
const modeColors = {
mining: '#4ade80',
exploring: '#60a5fa',
returning: '#f59e0b',
goHome: '#f59e0b',
idle: '#9ca3af',
manual: '#a78bfa',
refueling: '#ef4444',
farming: '#22c55e',
dumpInventory: '#a855f7',
dumping: '#a855f7',
moving: '#06b6d4',
scan: '#8b5cf6',
extraction: '#f97316',
building: '#14b8a6',
autocraft: '#ec4899',
unknown: '#6b7280'
};
return (
<div
className={`turtle-card ${isSelected ? 'selected' : ''}`}
onClick={onSelect}
style={{ borderColor: modeColors[activeState] || modeColors.unknown }}
>
<div className="turtle-header">
<h3>{displayName}</h3>
<span className="mode-badge" style={{ background: modeColors[activeState] || modeColors.unknown }}>
{activeState}
</span>
</div>
<div className="turtle-stats">
<div className="stat">
<span className="label">Position:</span>
<span className="value">
{turtle.position
? `${turtle.position.x}, ${turtle.position.y}, ${turtle.position.z}`
: 'No GPS'
}
</span>
</div>
<div className="stat">
<span className="label">Fuel:</span>
<span className="value">{fuel}</span>
</div>
<div className="stat">
<span className="label">Inventory:</span>
<span className="value">{inventoryCount} items</span>
</div>
{turtle.position && turtle.homePosition && (
<div className="stat">
<span className="label">Home Distance:</span>
<span className="value">
{Math.abs(turtle.position.x - turtle.homePosition.x) +
Math.abs(turtle.position.y - turtle.homePosition.y) +
Math.abs(turtle.position.z - turtle.homePosition.z)} blocks
</span>
</div>
)}
</div>
</div>
);
}
function TurtleDetails({ turtle }) {
const sendCommand = useTurtleStore((state) => state.sendCommand);
const setTurtleState = useTurtleStore((state) => state.setTurtleState);
if (!turtle) {
return (
<div className="turtle-details empty">
<p>Select a turtle to view details and control it</p>
</div>
);
}
const handleCommand = (command, param = null) => {
sendCommand(turtle.turtleID, command, param);
};
const handleStateChange = (stateName, data = {}) => {
setTurtleState(turtle.turtleID, stateName, data);
};
const activeState = turtle.state || turtle.mode || 'idle';
return (
<div className="turtle-details">
<h2>Turtle {turtle.turtleID} Control</h2>
<div className="detail-section">
<h3>Status</h3>
<div className="status-grid">
<div className="status-item">
<span className="label">State:</span>
<span className="value">{activeState}</span>
</div>
{turtle.stateDescription && (
<div className="status-item">
<span className="label">Activity:</span>
<span className="value">{turtle.stateDescription}</span>
</div>
)}
<div className="status-item">
<span className="label">Fuel:</span>
<span className="value">
{turtle.fuel === 'unlimited' ? 'Unlimited' : turtle.fuel}
</span>
</div>
<div className="status-item">
<span className="label">Position:</span>
<span className="value">
{turtle.position
? `X: ${turtle.position.x}, Y: ${turtle.position.y}, Z: ${turtle.position.z}`
: 'No GPS'
}
</span>
</div>
<div className="status-item">
<span className="label">Home:</span>
<span className="value">
{turtle.homePosition
? `X: ${turtle.homePosition.x}, Y: ${turtle.homePosition.y}, Z: ${turtle.homePosition.z}`
: 'Not set'
}
</span>
</div>
{turtle.error && (
<div className="status-item" style={{ color: '#ef4444' }}>
<span className="label">Error:</span>
<span className="value">{turtle.error}</span>
</div>
)}
{turtle.warning && (
<div className="status-item" style={{ color: '#f59e0b' }}>
<span className="label">Warning:</span>
<span className="value">{turtle.warning}</span>
</div>
)}
</div>
</div>
{/* Peripherals section */}
{turtle.peripherals && Object.keys(turtle.peripherals).length > 0 && (
<div className="detail-section">
<h3>Peripherals</h3>
<div className="status-grid">
{Object.entries(turtle.peripherals).map(([side, type]) => (
<div key={side} className="status-item">
<span className="label" style={{ textTransform: 'capitalize' }}>{side}:</span>
<span className="value" style={{ color: '#a78bfa' }}>{type}</span>
</div>
))}
</div>
</div>
)}
<div className="detail-section">
<h3>State Machine</h3>
<div className="command-grid">
<button
className={`command-btn ${activeState === 'idle' ? 'active' : ''}`}
onClick={() => { handleStateChange('idle'); handleCommand('stop'); }}
title="Stop and go idle"
style={activeState === 'idle' ? { outline: '2px solid #fff' } : {}}
>
Idle
</button>
<button
className={`command-btn explore ${activeState === 'exploring' ? 'active' : ''}`}
onClick={() => { handleStateChange('exploring'); handleCommand('explore'); }}
title="Autonomous exploration"
style={activeState === 'exploring' ? { outline: '2px solid #fff' } : {}}
>
🔍 Explore
</button>
<button
className={`command-btn mine ${activeState === 'mining' ? 'active' : ''}`}
onClick={() => { handleStateChange('mining'); handleCommand('explore'); }}
title="Mining with ore priority"
style={activeState === 'mining' ? { outline: '2px solid #fff' } : {}}
>
Mine
</button>
<button
className={`command-btn ${activeState === 'farming' ? 'active' : ''}`}
onClick={() => handleStateChange('farming')}
title="Automated farming"
style={activeState === 'farming' ? { outline: '2px solid #fff' } : {}}
>
🌾 Farm
</button>
<button
className={`command-btn return ${activeState === 'goHome' || activeState === 'returning' ? 'active' : ''}`}
onClick={() => { handleStateChange('goHome'); handleCommand('returnHome'); }}
title="Navigate home"
style={activeState === 'goHome' || activeState === 'returning' ? { outline: '2px solid #fff' } : {}}
>
🏠 Go Home
</button>
<button
className={`command-btn refuel ${activeState === 'refueling' ? 'active' : ''}`}
onClick={() => { handleStateChange('refueling'); handleCommand('refuel'); }}
title="Auto-refuel from inventory"
style={activeState === 'refueling' ? { outline: '2px solid #fff' } : {}}
>
Refuel
</button>
<button
className={`command-btn ${activeState === 'dumpInventory' || activeState === 'dumping' ? 'active' : ''}`}
onClick={() => handleStateChange('dumpInventory')}
title="Dump inventory into nearby container"
style={activeState === 'dumpInventory' || activeState === 'dumping' ? { outline: '2px solid #fff' } : {}}
>
📦 Dump
</button>
<button
className={`command-btn ${activeState === 'moving' ? 'active' : ''}`}
onClick={() => handleStateChange('moving')}
title="Navigate to target"
style={activeState === 'moving' ? { outline: '2px solid #fff' } : {}}
>
🧭 Move To
</button>
<button
className={`command-btn ${activeState === 'scan' ? 'active' : ''}`}
onClick={() => handleStateChange('scan')}
title="Scan surroundings with peripheral scanner"
style={activeState === 'scan' ? { outline: '2px solid #fff' } : {}}
>
📡 Scan
</button>
<button
className={`command-btn ${activeState === 'extraction' ? 'active' : ''}`}
onClick={() => {
const area = prompt('Enter extraction area (startX,startY,startZ,endX,endY,endZ):');
if (area) {
const [sx,sy,sz,ex,ey,ez] = area.split(',').map(Number);
if (!isNaN(sx) && !isNaN(sy) && !isNaN(sz) && !isNaN(ex) && !isNaN(ey) && !isNaN(ez)) {
const points = [];
for (let x = sx; x <= ex; x++) for (let y = sy; y <= ey; y++) for (let z = sz; z <= ez; z++) {
points.push({x, y, z});
}
handleStateChange('extraction', { area: points });
}
}
}}
title="Smart ore extraction in an area"
style={activeState === 'extraction' ? { outline: '2px solid #fff' } : {}}
>
💎 Extract
</button>
<button
className={`command-btn ${activeState === 'building' ? 'active' : ''}`}
onClick={() => {
const input = prompt('Enter blueprint JSON (array of {x,y,z,name} blocks):');
if (input) {
try {
const blocks = JSON.parse(input);
handleStateChange('building', { blocks });
} catch (e) {
alert('Invalid JSON blueprint');
}
}
}}
title="Build from blueprint"
style={activeState === 'building' ? { outline: '2px solid #fff' } : {}}
>
🏗 Build
</button>
<button
className={`command-btn ${activeState === 'autocraft' ? 'active' : ''}`}
onClick={() => {
const input = prompt('Enter recipe JSON (array of 16 slot items, null for empty):');
if (input) {
try {
const recipe = JSON.parse(input);
handleStateChange('autocraft', { recipe });
} catch (e) {
alert('Invalid JSON recipe');
}
}
}}
title="Automated crafting with workbench"
style={activeState === 'autocraft' ? { outline: '2px solid #fff' } : {}}
>
🔨 Autocraft
</button>
</div>
</div>
<div className="detail-section">
<h3>Legacy Commands</h3>
<div className="command-grid">
<button
className="command-btn explore"
onClick={() => handleCommand('explore')}
title="Start autonomous exploration and mining"
>
🔍 Explore
</button>
<button
className="command-btn mine"
onClick={() => handleCommand('mine')}
title="Start mining mode"
>
Mine
</button>
<button
className="command-btn return"
onClick={() => handleCommand('returnHome')}
title="Navigate back to home position"
>
🏠 Return Home
</button>
<button
className="command-btn stop"
onClick={() => handleCommand('stop')}
title="Stop all autonomous actions"
>
Stop
</button>
<button
className="command-btn manual"
onClick={() => handleCommand('manual')}
title="Switch to manual control mode"
>
🎮 Manual Mode
</button>
<button
className="command-btn setHome"
onClick={() => handleCommand('setHome')}
title="Set current position as home"
>
📍 Set Home
</button>
<button
className="command-btn refuel"
onClick={() => handleCommand('refuel')}
title="Consume fuel from inventory"
>
Refuel
</button>
<button
className="command-btn status"
onClick={() => handleCommand('status')}
title="Request status update"
>
📊 Status
</button>
</div>
</div>
<div className="detail-section">
<h3>Movement</h3>
<div className="movement-controls">
<div className="movement-row">
<button onClick={() => handleCommand('forward')} title="Move forward"></button>
</div>
<div className="movement-row">
<button onClick={() => handleCommand('turnLeft')} title="Turn left"></button>
<button onClick={() => handleCommand('back')} title="Move backward"></button>
<button onClick={() => handleCommand('turnRight')} title="Turn right"></button>
</div>
<div className="movement-row vertical">
<button onClick={() => handleCommand('up')} title="Move up"> Up</button>
<button onClick={() => handleCommand('down')} title="Move down"> Down</button>
</div>
</div>
</div>
<div className="detail-section">
<h3>Actions</h3>
<div className="action-grid">
<button
className="action-btn dig"
onClick={() => handleCommand('dig')}
title="Dig block in front"
>
Dig
</button>
<button
className="action-btn digup"
onClick={() => handleCommand('digUp')}
title="Dig block above"
>
Dig Up
</button>
<button
className="action-btn digdown"
onClick={() => handleCommand('digDown')}
title="Dig block below"
>
Dig Down
</button>
<button
className="action-btn place"
onClick={() => handleCommand('place')}
title="Place block from inventory"
>
🧱 Place
</button>
</div>
</div>
{turtle.inventory && turtle.inventory.length > 0 && (
<div className="detail-section">
<h3>Inventory ({turtle.inventoryCount || turtle.inventory.length}/16)</h3>
<div className="inventory-grid">
{Array.from({ length: 16 }, (_, slotIndex) => {
const item = turtle.inventory[slotIndex];
return (
<div
key={slotIndex}
className={`inventory-slot ${item ? 'filled' : 'empty'}`}
title={item ? `${item.name.replace('minecraft:', '').replace(/_/g, ' ')} (${item.count})` : 'Empty'}
>
{item ? (
<>
<div className="slot-item">
<span className="item-icon">
{item.name.includes('diamond') ? '💎' :
item.name.includes('gold') ? '<27>' :
item.name.includes('iron') ? '⚪' :
item.name.includes('coal') ? '⚫' :
item.name.includes('emerald') ? '🟢' :
item.name.includes('redstone') ? '🔴' :
item.name.includes('lapis') ? '🔵' :
item.name.includes('stone') ? '🗿' :
item.name.includes('dirt') ? '🟤' :
item.name.includes('wood') || item.name.includes('log') ? '🪵' :
item.name.includes('cobble') ? '🪨' : '<27>📦'}
</span>
<span className="item-count">{item.count}</span>
</div>
<div className="slot-name">
{item.name.replace('minecraft:', '').replace(/_/g, ' ').split(' ').slice(0, 2).join(' ')}
</div>
</>
) : (
<span className="slot-number">{slotIndex + 1}</span>
)}
</div>
);
})}
</div>
</div>
)}
</div>
);
}
export default function ControlPanel() {
const turtles = useTurtleStore((state) => state.getTurtleArray());
const selectedTurtleId = useTurtleStore((state) => state.selectedTurtleId);
const selectTurtle = useTurtleStore((state) => state.selectTurtle);
const selectedTurtle = useTurtleStore((state) => state.getSelectedTurtle());
const connected = useTurtleStore((state) => state.connected);
return (
<div className="control-panel">
<div className="panel-header">
<h1>🐢 Turtle Control Center</h1>
<div className={`connection-status ${connected ? 'connected' : 'disconnected'}`}>
<span className="status-dot"></span>
{connected ? 'Connected' : 'Disconnected'}
</div>
</div>
<div className="panel-content">
<div className="turtle-list">
<h2>Turtles ({turtles.length})</h2>
{turtles.length === 0 ? (
<div className="empty-state">
<p>No turtles connected</p>
<p className="hint">Waiting for turtles to come online...</p>
</div>
) : (
<div className="turtle-cards">
{turtles.map((turtle) => (
<TurtleCard
key={turtle.turtleID}
turtle={turtle}
isSelected={selectedTurtleId === turtle.turtleID}
onSelect={() => selectTurtle(turtle.turtleID)}
/>
))}
</div>
)}
</div>
<TurtleDetails turtle={selectedTurtle} />
</div>
</div>
);
}