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

597 lines
24 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 = Array.isArray(turtle.inventory)
? turtle.inventory.length
: (turtle.inventory ? Object.keys(turtle.inventory).length : 0);
const displayName = turtle.label || `Turtle ${turtle.turtleID}`;
const modeColors = {
mining: '#55ff55',
exploring: '#55ffff',
returning: '#ffaa00',
goHome: '#ffaa00',
idle: '#aaaaaa',
manual: '#ff55ff',
refueling: '#ff5555',
farming: '#55ff55',
dumpInventory: '#aa00aa',
dumping: '#aa00aa',
moving: '#55ffff',
scan: '#5555ff',
extraction: '#ffaa00',
building: '#00aaaa',
autocraft: '#ff55ff',
unknown: '#555555'
};
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 setTurtleState = useTurtleStore((state) => state.setTurtleState);
const renameTurtle = useTurtleStore((state) => state.renameTurtle);
const equipLeft = useTurtleStore((state) => state.equipLeft);
const equipRight = useTurtleStore((state) => state.equipRight);
const sortInventory = useTurtleStore((state) => state.sortInventory);
const selectSlot = useTurtleStore((state) => state.selectSlot);
const dropItems = useTurtleStore((state) => state.dropItems);
const suckItems = useTurtleStore((state) => state.suckItems);
const connectToInventory = useTurtleStore((state) => state.connectToInventory);
const updateTurtleConfig = useTurtleStore((state) => state.updateTurtleConfig);
const exploreTurtle = useTurtleStore((state) => state.exploreTurtle);
const gpsLocateTurtle = useTurtleStore((state) => state.gpsLocateTurtle);
const moveForward = useTurtleStore((state) => state.moveForward);
const moveBack = useTurtleStore((state) => state.moveBack);
const moveUp = useTurtleStore((state) => state.moveUp);
const moveDown = useTurtleStore((state) => state.moveDown);
const turnLeft = useTurtleStore((state) => state.turnLeft);
const turnRight = useTurtleStore((state) => state.turnRight);
const digBlock = useTurtleStore((state) => state.digBlock);
const digBlockUp = useTurtleStore((state) => state.digBlockUp);
const digBlockDown = useTurtleStore((state) => state.digBlockDown);
const placeBlock = useTurtleStore((state) => state.placeBlock);
const [renameValue, setRenameValue] = useState('');
const [showConfig, setShowConfig] = useState(false);
const [configValues, setConfigValues] = useState({ maxDistance: 200, autoRefuel: true });
if (!turtle) {
return (
<div className="turtle-details empty">
<p>Select a turtle to view details and control it</p>
</div>
);
}
const handleStateChange = (stateName, data = {}) => {
setTurtleState(turtle.turtleID, stateName, data);
};
const handleRename = async () => {
if (renameValue.trim()) {
await renameTurtle(turtle.turtleID, renameValue.trim());
setRenameValue('');
}
};
const handleSlotClick = async (slotIndex) => {
await selectSlot(turtle.turtleID, slotIndex + 1);
};
const handleConfigSave = async () => {
await updateTurtleConfig(turtle.turtleID, configValues);
setShowConfig(false);
};
const activeState = turtle.state || turtle.mode || 'idle';
const displayName = turtle.label || `Turtle ${turtle.turtleID}`;
return (
<div className="turtle-details">
<h2>{displayName} <span style={{color: '#aaaaaa', fontSize: '0.8em'}}>#{turtle.turtleID}</span></h2>
{/* Rename + Config bar */}
<div className="detail-section" style={{ display: 'flex', gap: '8px', alignItems: 'center', flexWrap: 'wrap' }}>
<input
type="text"
value={renameValue}
onChange={(e) => setRenameValue(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleRename()}
placeholder="Rename turtle..."
className="rename-input"
style={{ flex: 1, minWidth: '120px', padding: '4px 8px', border: '2px solid #333', background: '#1a1a1a', color: '#e0e0e0' }}
/>
<button onClick={handleRename} className="command-btn" style={{ padding: '4px 12px' }}>📝 Rename</button>
<button onClick={() => setShowConfig(!showConfig)} className="command-btn" style={{ padding: '4px 12px' }}> Config</button>
<button onClick={() => gpsLocateTurtle(turtle.turtleID)} className="command-btn" style={{ padding: '4px 12px' }}>📡 GPS</button>
<button onClick={() => exploreTurtle(turtle.turtleID)} className="command-btn" style={{ padding: '4px 12px' }}>🔎 Inspect</button>
</div>
{/* Config Modal */}
{showConfig && (
<div className="detail-section" style={{ background: '#2c2c2c', padding: '12px', border: '3px solid #1a1a1a' }}>
<h3> Configuration</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px', marginTop: '8px' }}>
<label style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>Max Distance:</span>
<input type="number" value={configValues.maxDistance} onChange={(e) => setConfigValues({...configValues, maxDistance: parseInt(e.target.value)})} style={{ width: '80px', padding: '2px 6px', border: '2px solid #333', background: '#1a1a1a', color: '#e0e0e0' }} />
</label>
<label style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span>Auto Refuel:</span>
<input type="checkbox" checked={configValues.autoRefuel} onChange={(e) => setConfigValues({...configValues, autoRefuel: e.target.checked})} />
</label>
<div style={{ display: 'flex', gap: '8px', justifyContent: 'flex-end' }}>
<button onClick={() => setShowConfig(false)} className="command-btn" style={{ padding: '4px 12px' }}>Cancel</button>
<button onClick={handleConfigSave} className="command-btn explore" style={{ padding: '4px 12px' }}>💾 Save</button>
</div>
</div>
</div>
)}
<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: '#ff5555' }}>
<span className="label">Error:</span>
<span className="value">{turtle.error}</span>
</div>
)}
{turtle.warning && (
<div className="status-item" style={{ color: '#ffaa00' }}>
<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, info]) => (
<div key={side} className="status-item">
<span className="label" style={{ textTransform: 'capitalize' }}>{side}:</span>
<span className="value" style={{ color: '#ff55ff' }}>
{typeof info === 'string' ? info : (info?.types?.join(', ') || 'unknown')}
</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')}
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')}
title="Autonomous exploration"
style={activeState === 'exploring' ? { outline: '2px solid #fff' } : {}}
>
🔍 Explore
</button>
<button
className={`command-btn mine ${activeState === 'mining' ? 'active' : ''}`}
onClick={() => handleStateChange('mining')}
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')}
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')}
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>Movement</h3>
<div className="movement-controls">
<div className="movement-row">
<button onClick={() => moveForward(turtle.turtleID)} title="Move forward"></button>
</div>
<div className="movement-row">
<button onClick={() => turnLeft(turtle.turtleID)} title="Turn left"></button>
<button onClick={() => moveBack(turtle.turtleID)} title="Move backward"></button>
<button onClick={() => turnRight(turtle.turtleID)} title="Turn right"></button>
</div>
<div className="movement-row vertical">
<button onClick={() => moveUp(turtle.turtleID)} title="Move up"> Up</button>
<button onClick={() => moveDown(turtle.turtleID)} 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={() => digBlock(turtle.turtleID)}
title="Dig block in front"
>
Dig
</button>
<button
className="action-btn digup"
onClick={() => digBlockUp(turtle.turtleID)}
title="Dig block above"
>
Dig Up
</button>
<button
className="action-btn digdown"
onClick={() => digBlockDown(turtle.turtleID)}
title="Dig block below"
>
Dig Down
</button>
<button
className="action-btn place"
onClick={() => placeBlock(turtle.turtleID)}
title="Place block from inventory"
>
🧱 Place
</button>
</div>
</div>
{/* Equipment & Inventory Actions */}
<div className="detail-section">
<h3>Equipment & Inventory</h3>
<div className="action-grid">
<button
className="action-btn"
onClick={() => equipLeft(turtle.turtleID)}
title="Equip selected item on left side"
>
🛡 Equip L
</button>
<button
className="action-btn"
onClick={() => equipRight(turtle.turtleID)}
title="Equip selected item on right side"
>
🗡 Equip R
</button>
<button
className="action-btn"
onClick={() => sortInventory(turtle.turtleID)}
title="Sort and compact inventory"
>
📋 Sort
</button>
<button
className="action-btn"
onClick={() => dropItems(turtle.turtleID, 'front')}
title="Drop items forward"
>
📤 Drop
</button>
<button
className="action-btn"
onClick={() => dropItems(turtle.turtleID, 'up')}
title="Drop items upward"
>
Drop Up
</button>
<button
className="action-btn"
onClick={() => dropItems(turtle.turtleID, 'down')}
title="Drop items downward"
>
Drop Down
</button>
<button
className="action-btn"
onClick={() => suckItems(turtle.turtleID, 'front')}
title="Suck items from front"
>
📥 Suck
</button>
<button
className="action-btn"
onClick={() => {
const side = prompt('Enter peripheral side (front/top/bottom/left/right):');
if (side) connectToInventory(turtle.turtleID, side);
}}
title="Read adjacent inventory peripheral contents"
>
📦 Read Inv
</button>
</div>
</div>
{turtle.inventory && turtle.inventory.length > 0 && (
<div className="detail-section">
<h3>Inventory ({turtle.inventoryCount || turtle.inventory.length}/16) Slot: {turtle.selectedSlot || 1}</h3>
<div className="inventory-grid">
{Array.from({ length: 16 }, (_, slotIndex) => {
const item = turtle.inventory[slotIndex];
const isSelected = (turtle.selectedSlot || 1) === (slotIndex + 1);
return (
<div
key={slotIndex}
className={`inventory-slot ${item ? 'filled' : 'empty'} ${isSelected ? 'selected-slot' : ''}`}
title={item ? `${item.name.replace('minecraft:', '').replace(/_/g, ' ')} (${item.count}) — Click to select` : `Slot ${slotIndex + 1} — Click to select`}
onClick={() => handleSlotClick(slotIndex)}
style={isSelected ? { outline: '2px solid #55ffff', outlineOffset: '-2px' } : {}}
>
{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>
);
}