mirror of
https://github.com/kolbytn/mindcraft.git
synced 2025-09-01 11:53:03 +02:00
Merge branch 'develop' into stt
This commit is contained in:
commit
8ffc0f2d10
10 changed files with 1006 additions and 125 deletions
2
main.js
2
main.js
|
@ -63,7 +63,7 @@ if (process.env.LOG_ALL) {
|
|||
settings.log_all_prompts = process.env.LOG_ALL;
|
||||
}
|
||||
|
||||
Mindcraft.init(false, settings.mindserver_port);
|
||||
Mindcraft.init(false, settings.mindserver_port, settings.auto_open_ui);
|
||||
|
||||
for (let profile of settings.profiles) {
|
||||
const profile_json = JSON.parse(readFileSync(profile, 'utf8'));
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
"mineflayer-pvp": "^1.3.2",
|
||||
"node-canvas-webgl": "PrismarineJS/node-canvas-webgl",
|
||||
"npm": "^11.5.2",
|
||||
"open": "^10.2.0",
|
||||
"openai": "^4.4.0",
|
||||
"prismarine-item": "^1.15.0",
|
||||
"prismarine-viewer": "^1.32.0",
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
const settings = {
|
||||
"minecraft_version": "auto", // or specific version like "1.21.1"
|
||||
"host": "127.0.0.1", // or "localhost", "your.ip.address.here"
|
||||
"port": 55916,
|
||||
"port": 55916, // set to -1 to automatically scan for open ports
|
||||
"auth": "offline", // or "microsoft"
|
||||
|
||||
// the mindserver manages all agents and hosts the UI
|
||||
"mindserver_port": 8080,
|
||||
"auto_open_ui": true, // opens UI in browser on startup
|
||||
|
||||
"base_profile": "assistant", // survival, assistant, creative, or god_mode
|
||||
"profiles": [
|
||||
|
|
89
src/agent/library/full_state.js
Normal file
89
src/agent/library/full_state.js
Normal file
|
@ -0,0 +1,89 @@
|
|||
import {
|
||||
getPosition,
|
||||
getBiomeName,
|
||||
getNearbyPlayerNames,
|
||||
getInventoryCounts,
|
||||
getNearbyEntityTypes,
|
||||
getBlockAtPosition,
|
||||
getFirstBlockAboveHead
|
||||
} from "./world.js";
|
||||
import convoManager from '../conversation.js';
|
||||
|
||||
export function getFullState(agent) {
|
||||
const bot = agent.bot;
|
||||
|
||||
const pos = getPosition(bot);
|
||||
const position = {
|
||||
x: Number(pos.x.toFixed(2)),
|
||||
y: Number(pos.y.toFixed(2)),
|
||||
z: Number(pos.z.toFixed(2))
|
||||
};
|
||||
|
||||
let weather = 'Clear';
|
||||
if (bot.thunderState > 0) weather = 'Thunderstorm';
|
||||
else if (bot.rainState > 0) weather = 'Rain';
|
||||
|
||||
let timeLabel = 'Night';
|
||||
if (bot.time.timeOfDay < 6000) timeLabel = 'Morning';
|
||||
else if (bot.time.timeOfDay < 12000) timeLabel = 'Afternoon';
|
||||
|
||||
const below = getBlockAtPosition(bot, 0, -1, 0).name;
|
||||
const legs = getBlockAtPosition(bot, 0, 0, 0).name;
|
||||
const head = getBlockAtPosition(bot, 0, 1, 0).name;
|
||||
|
||||
let players = getNearbyPlayerNames(bot);
|
||||
let bots = convoManager.getInGameAgents().filter(b => b !== agent.name);
|
||||
players = players.filter(p => !bots.includes(p));
|
||||
|
||||
const helmet = bot.inventory.slots[5];
|
||||
const chestplate = bot.inventory.slots[6];
|
||||
const leggings = bot.inventory.slots[7];
|
||||
const boots = bot.inventory.slots[8];
|
||||
|
||||
const state = {
|
||||
name: agent.name,
|
||||
gameplay: {
|
||||
position,
|
||||
dimension: bot.game.dimension,
|
||||
gamemode: bot.game.gameMode,
|
||||
health: Math.round(bot.health),
|
||||
hunger: Math.round(bot.food),
|
||||
biome: getBiomeName(bot),
|
||||
weather,
|
||||
timeOfDay: bot.time.timeOfDay,
|
||||
timeLabel
|
||||
},
|
||||
action: {
|
||||
current: agent.isIdle() ? 'Idle' : agent.actions.currentActionLabel,
|
||||
isIdle: agent.isIdle()
|
||||
},
|
||||
surroundings: {
|
||||
below,
|
||||
legs,
|
||||
head,
|
||||
firstBlockAboveHead: getFirstBlockAboveHead(bot, null, 32)
|
||||
},
|
||||
inventory: {
|
||||
counts: getInventoryCounts(bot),
|
||||
stacksUsed: bot.inventory.items().length,
|
||||
totalSlots: bot.inventory.slots.length,
|
||||
equipment: {
|
||||
helmet: helmet ? helmet.name : null,
|
||||
chestplate: chestplate ? chestplate.name : null,
|
||||
leggings: leggings ? leggings.name : null,
|
||||
boots: boots ? boots.name : null,
|
||||
mainHand: bot.heldItem ? bot.heldItem.name : null
|
||||
}
|
||||
},
|
||||
nearby: {
|
||||
humanPlayers: players,
|
||||
botPlayers: bots,
|
||||
entityTypes: getNearbyEntityTypes(bot).filter(t => t !== 'player' && t !== 'item'),
|
||||
},
|
||||
modes: {
|
||||
summary: bot.modes.getMiniDocs()
|
||||
}
|
||||
};
|
||||
|
||||
return state;
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import { io } from 'socket.io-client';
|
||||
import convoManager from './conversation.js';
|
||||
import { setSettings } from './settings.js';
|
||||
import { getFullState } from './library/full_state.js';
|
||||
|
||||
// agent's individual connection to the mindserver
|
||||
// always connect to localhost
|
||||
|
@ -43,7 +44,7 @@ class MindServerProxy {
|
|||
convoManager.receiveFromBot(agentName, json);
|
||||
});
|
||||
|
||||
this.socket.on('agents-update', (agents) => {
|
||||
this.socket.on('agents-status', (agents) => {
|
||||
this.agents = agents;
|
||||
convoManager.updateAgents(agents);
|
||||
if (this.agent?.task) {
|
||||
|
@ -57,14 +58,24 @@ class MindServerProxy {
|
|||
this.agent.cleanKill();
|
||||
});
|
||||
|
||||
this.socket.on('send-message', (agentName, message) => {
|
||||
this.socket.on('send-message', (data) => {
|
||||
try {
|
||||
this.agent.respondFunc("NO USERNAME", message);
|
||||
this.agent.respondFunc(data.from, data.message);
|
||||
} catch (error) {
|
||||
console.error('Error: ', JSON.stringify(error, Object.getOwnPropertyNames(error)));
|
||||
}
|
||||
});
|
||||
|
||||
this.socket.on('get-full-state', (callback) => {
|
||||
try {
|
||||
const state = getFullState(this.agent);
|
||||
callback(state);
|
||||
} catch (error) {
|
||||
console.error('Error getting full state:', error);
|
||||
callback(null);
|
||||
}
|
||||
});
|
||||
|
||||
// Request settings and wait for response
|
||||
await new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
|
@ -77,6 +88,7 @@ class MindServerProxy {
|
|||
return reject(new Error(response.error));
|
||||
}
|
||||
setSettings(response.settings);
|
||||
this.socket.emit('connect-agent-process', name);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { createMindServer, registerAgent } from './mindserver.js';
|
||||
import { createMindServer, registerAgent, numStateListeners } from './mindserver.js';
|
||||
import { AgentProcess } from '../process/agent_process.js';
|
||||
import { getServer } from './mcserver.js';
|
||||
import open from 'open';
|
||||
|
||||
let mindserver;
|
||||
let connected = false;
|
||||
|
@ -8,7 +9,7 @@ let agent_processes = {};
|
|||
let agent_count = 0;
|
||||
let port = 8080;
|
||||
|
||||
export async function init(host_public=false, port=8080) {
|
||||
export async function init(host_public=false, port=8080, auto_open_ui=true) {
|
||||
if (connected) {
|
||||
console.error('Already initiliazed!');
|
||||
return;
|
||||
|
@ -16,28 +17,53 @@ export async function init(host_public=false, port=8080) {
|
|||
mindserver = createMindServer(host_public, port);
|
||||
port = port;
|
||||
connected = true;
|
||||
if (auto_open_ui) {
|
||||
setTimeout(() => {
|
||||
// check if browser listener is already open
|
||||
if (numStateListeners() === 0) {
|
||||
open('http://localhost:'+port);
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createAgent(settings) {
|
||||
if (!settings.profile.name) {
|
||||
console.error('Agent name is required in profile');
|
||||
return;
|
||||
return {
|
||||
success: false,
|
||||
error: 'Agent name is required in profile'
|
||||
};
|
||||
}
|
||||
settings = JSON.parse(JSON.stringify(settings));
|
||||
let agent_name = settings.profile.name;
|
||||
registerAgent(settings);
|
||||
const viewer_port = 3000 + agent_count;
|
||||
registerAgent(settings, viewer_port);
|
||||
let load_memory = settings.load_memory || false;
|
||||
let init_message = settings.init_message || null;
|
||||
|
||||
const server = await getServer(settings.host, settings.port, settings.minecraft_version);
|
||||
settings.host = server.host;
|
||||
settings.port = server.port;
|
||||
settings.minecraft_version = server.version;
|
||||
try {
|
||||
const server = await getServer(settings.host, settings.port, settings.minecraft_version);
|
||||
settings.host = server.host;
|
||||
settings.port = server.port;
|
||||
settings.minecraft_version = server.version;
|
||||
|
||||
const agentProcess = new AgentProcess(agent_name, port);
|
||||
agentProcess.start(load_memory, init_message, agent_count);
|
||||
agent_count++;
|
||||
agent_processes[settings.profile.name] = agentProcess;
|
||||
const agentProcess = new AgentProcess(agent_name, port);
|
||||
agentProcess.start(load_memory, init_message, agent_count);
|
||||
agent_count++;
|
||||
agent_processes[settings.profile.name] = agentProcess;
|
||||
} catch (error) {
|
||||
console.error(`Error creating agent ${agent_name}:`, error);
|
||||
destroyAgent(agent_name);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
error: null
|
||||
};
|
||||
}
|
||||
|
||||
export function getAgentProcess(agentName) {
|
||||
|
@ -59,6 +85,13 @@ export function stopAgent(agentName) {
|
|||
}
|
||||
}
|
||||
|
||||
export function destroyAgent(agentName) {
|
||||
if (agent_processes[agentName]) {
|
||||
agent_processes[agentName].stop();
|
||||
delete agent_processes[agentName];
|
||||
}
|
||||
}
|
||||
|
||||
export function shutdown() {
|
||||
console.log('Shutting down');
|
||||
for (let agentName in agent_processes) {
|
||||
|
|
|
@ -15,27 +15,32 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|||
let io;
|
||||
let server;
|
||||
const agent_connections = {};
|
||||
const agent_listeners = [];
|
||||
|
||||
const settings_spec = JSON.parse(readFileSync(path.join(__dirname, 'public/settings_spec.json'), 'utf8'));
|
||||
|
||||
class AgentConnection {
|
||||
constructor(settings) {
|
||||
constructor(settings, viewer_port) {
|
||||
this.socket = null;
|
||||
this.settings = settings;
|
||||
this.in_game = false;
|
||||
this.full_state = null;
|
||||
this.viewer_port = viewer_port;
|
||||
}
|
||||
setSettings(settings) {
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function registerAgent(settings) {
|
||||
let agentConnection = new AgentConnection(settings);
|
||||
export function registerAgent(settings, viewer_port) {
|
||||
let agentConnection = new AgentConnection(settings, viewer_port);
|
||||
agent_connections[settings.profile.name] = agentConnection;
|
||||
}
|
||||
|
||||
export function logoutAgent(agentName) {
|
||||
if (agent_connections[agentName]) {
|
||||
agent_connections[agentName].in_game = false;
|
||||
agentsUpdate();
|
||||
agentsStatusUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,9 +59,9 @@ export function createMindServer(host_public = false, port = 8080) {
|
|||
let curAgentName = null;
|
||||
console.log('Client connected');
|
||||
|
||||
agentsUpdate(socket);
|
||||
agentsStatusUpdate(socket);
|
||||
|
||||
socket.on('create-agent', (settings, callback) => {
|
||||
socket.on('create-agent', async (settings, callback) => {
|
||||
console.log('API create agent...');
|
||||
for (let key in settings_spec) {
|
||||
if (!(key in settings)) {
|
||||
|
@ -79,8 +84,14 @@ export function createMindServer(host_public = false, port = 8080) {
|
|||
callback({ success: false, error: 'Agent already exists' });
|
||||
return;
|
||||
}
|
||||
mindcraft.createAgent(settings);
|
||||
callback({ success: true });
|
||||
let returned = await mindcraft.createAgent(settings);
|
||||
callback({ success: returned.success, error: returned.error });
|
||||
let name = settings.profile.name;
|
||||
if (!returned.success && agent_connections[name]) {
|
||||
mindcraft.destroyAgent(name);
|
||||
delete agent_connections[name];
|
||||
}
|
||||
agentsStatusUpdate();
|
||||
}
|
||||
else {
|
||||
console.error('Agent name is required in profile');
|
||||
|
@ -96,12 +107,19 @@ export function createMindServer(host_public = false, port = 8080) {
|
|||
}
|
||||
});
|
||||
|
||||
socket.on('connect-agent-process', (agentName) => {
|
||||
if (agent_connections[agentName]) {
|
||||
agent_connections[agentName].socket = socket;
|
||||
agentsStatusUpdate();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('login-agent', (agentName) => {
|
||||
if (agent_connections[agentName]) {
|
||||
agent_connections[agentName].socket = socket;
|
||||
agent_connections[agentName].in_game = true;
|
||||
curAgentName = agentName;
|
||||
agentsUpdate();
|
||||
agentsStatusUpdate();
|
||||
}
|
||||
else {
|
||||
console.warn(`Unregistered agent ${agentName} tried to login`);
|
||||
|
@ -112,7 +130,11 @@ export function createMindServer(host_public = false, port = 8080) {
|
|||
if (agent_connections[curAgentName]) {
|
||||
console.log(`Agent ${curAgentName} disconnected`);
|
||||
agent_connections[curAgentName].in_game = false;
|
||||
agentsUpdate();
|
||||
agent_connections[curAgentName].socket = null;
|
||||
agentsStatusUpdate();
|
||||
}
|
||||
if (agent_listeners.includes(socket)) {
|
||||
removeListener(socket);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -125,6 +147,14 @@ export function createMindServer(host_public = false, port = 8080) {
|
|||
agent_connections[agentName].socket.emit('chat-message', curAgentName, json);
|
||||
});
|
||||
|
||||
socket.on('set-agent-settings', (agentName, settings) => {
|
||||
const agent = agent_connections[agentName];
|
||||
if (agent) {
|
||||
agent.setSettings(settings);
|
||||
agent.socket.emit('restart-agent');
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('restart-agent', (agentName) => {
|
||||
console.log(`Restarting agent: ${agentName}`);
|
||||
agent_connections[agentName].socket.emit('restart-agent');
|
||||
|
@ -138,6 +168,14 @@ export function createMindServer(host_public = false, port = 8080) {
|
|||
mindcraft.startAgent(agentName);
|
||||
});
|
||||
|
||||
socket.on('destroy-agent', (agentName) => {
|
||||
if (agent_connections[agentName]) {
|
||||
mindcraft.destroyAgent(agentName);
|
||||
delete agent_connections[agentName];
|
||||
}
|
||||
agentsStatusUpdate();
|
||||
});
|
||||
|
||||
socket.on('stop-all-agents', () => {
|
||||
console.log('Killing all agents');
|
||||
for (let agentName in agent_connections) {
|
||||
|
@ -158,14 +196,13 @@ export function createMindServer(host_public = false, port = 8080) {
|
|||
|
||||
});
|
||||
|
||||
socket.on('send-message', (agentName, message) => {
|
||||
socket.on('send-message', (agentName, data) => {
|
||||
if (!agent_connections[agentName]) {
|
||||
console.warn(`Agent ${agentName} not in game, cannot send message via MindServer.`);
|
||||
return
|
||||
}
|
||||
try {
|
||||
console.log(`Sending message to agent ${agentName}: ${message}`);
|
||||
agent_connections[agentName].socket.emit('send-message', agentName, message)
|
||||
agent_connections[agentName].socket.emit('send-message', data)
|
||||
} catch (error) {
|
||||
console.error('Error: ', error);
|
||||
}
|
||||
|
@ -174,6 +211,10 @@ export function createMindServer(host_public = false, port = 8080) {
|
|||
socket.on('bot-output', (agentName, message) => {
|
||||
io.emit('bot-output', agentName, message);
|
||||
});
|
||||
|
||||
socket.on('listen-to-agents', () => {
|
||||
addListener(socket);
|
||||
});
|
||||
});
|
||||
|
||||
let host = host_public ? '0.0.0.0' : 'localhost';
|
||||
|
@ -184,17 +225,59 @@ export function createMindServer(host_public = false, port = 8080) {
|
|||
return server;
|
||||
}
|
||||
|
||||
function agentsUpdate(socket) {
|
||||
function agentsStatusUpdate(socket) {
|
||||
if (!socket) {
|
||||
socket = io;
|
||||
}
|
||||
let agents = [];
|
||||
for (let agentName in agent_connections) {
|
||||
agents.push({name: agentName, in_game: agent_connections[agentName].in_game});
|
||||
const conn = agent_connections[agentName];
|
||||
agents.push({
|
||||
name: agentName,
|
||||
in_game: conn.in_game,
|
||||
viewerPort: conn.viewer_port,
|
||||
socket_connected: !!conn.socket
|
||||
});
|
||||
};
|
||||
socket.emit('agents-update', agents);
|
||||
socket.emit('agents-status', agents);
|
||||
}
|
||||
|
||||
|
||||
let listenerInterval = null;
|
||||
function addListener(listener_socket) {
|
||||
agent_listeners.push(listener_socket);
|
||||
if (agent_listeners.length === 1) {
|
||||
listenerInterval = setInterval(async () => {
|
||||
const states = {};
|
||||
for (let agentName in agent_connections) {
|
||||
let agent = agent_connections[agentName];
|
||||
if (agent.in_game) {
|
||||
try {
|
||||
const state = await new Promise((resolve) => {
|
||||
agent.socket.emit('get-full-state', (s) => resolve(s));
|
||||
});
|
||||
states[agentName] = state;
|
||||
} catch (e) {
|
||||
states[agentName] = { error: String(e) };
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let listener of agent_listeners) {
|
||||
listener.emit('state-update', states);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function removeListener(listener_socket) {
|
||||
agent_listeners.splice(agent_listeners.indexOf(listener_socket), 1);
|
||||
if (agent_listeners.length === 0) {
|
||||
clearInterval(listenerInterval);
|
||||
listenerInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Optional: export these if you need access to them from other files
|
||||
export const getIO = () => io;
|
||||
export const getServer = () => server;
|
||||
export const numStateListeners = () => agent_listeners.length;
|
|
@ -26,7 +26,7 @@
|
|||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
align-items: stretch;
|
||||
}
|
||||
.restart-btn, .start-btn, .stop-btn {
|
||||
color: white;
|
||||
|
@ -48,6 +48,17 @@
|
|||
.restart-btn:hover { background: #45a049; }
|
||||
.start-btn:hover { background: #1976D2; }
|
||||
.stop-btn:hover { background: #d32f2f; }
|
||||
.gear-btn {
|
||||
background: #505050;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 2px 6px;
|
||||
cursor: pointer;
|
||||
margin-left: 6px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.gear-btn:hover { background: #5a5a5a; }
|
||||
.status-icon {
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
|
@ -96,18 +107,26 @@
|
|||
.setting-wrapper input[type="checkbox"] {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
.agent-view-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
aspect-ratio: 4/3;
|
||||
overflow: hidden;
|
||||
}
|
||||
.agent-viewer {
|
||||
width: 200px;
|
||||
height: 150px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
margin-left: 10px;
|
||||
display: block;
|
||||
}
|
||||
.last-message {
|
||||
font-style: italic;
|
||||
color: #aaa;
|
||||
margin-top: 5px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
.start-btn:disabled {
|
||||
opacity: 0.4;
|
||||
|
@ -118,22 +137,243 @@
|
|||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.agent-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
align-items: start;
|
||||
}
|
||||
.agent-grid .cell {
|
||||
background: #3a3a3a;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.agent-grid .cell.title {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
.agent-inventory {
|
||||
margin-top: 8px;
|
||||
background: #2f2f2f;
|
||||
border-radius: 6px;
|
||||
padding: 8px;
|
||||
}
|
||||
.agent-inventory h3 { margin: 0 0 6px 0; font-size: 1em; }
|
||||
.inventory-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
.agent-details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
.controls-row {
|
||||
margin-top: 8px;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(100px, 1fr) repeat(5, auto);
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.msg-input {
|
||||
width: calc(100% - 8px);
|
||||
background: #262626;
|
||||
border: 1px solid #555;
|
||||
color: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
.neutral-btn {
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-left: 5px;
|
||||
background: #505050;
|
||||
}
|
||||
.neutral-btn:hover { background: #5a5a5a; }
|
||||
.neutral-btn:disabled {
|
||||
background: #383838;
|
||||
color: #666;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.msg-input:disabled {
|
||||
background: #1a1a1a;
|
||||
color: #666;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.page-footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #2d2d2d;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
box-shadow: 0 -2px 10px rgba(0,0,0,0.2);
|
||||
}
|
||||
body {
|
||||
padding-bottom: 80px; /* Make room for footer */
|
||||
}
|
||||
.agent-stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 8px;
|
||||
margin: 6px 0;
|
||||
font-size: 0.9em;
|
||||
color: #cccccc;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.agent-stats-row .stat {
|
||||
background: #3a3a3a;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.status-badge {
|
||||
font-size: 0.75em;
|
||||
margin-left: 8px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: #3a3a3a;
|
||||
color: #cccccc;
|
||||
text-transform: lowercase;
|
||||
}
|
||||
.status-badge.online { color: #4CAF50; }
|
||||
.status-badge.offline { color: #f44336; }
|
||||
.title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.title-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.title-spacer { flex: 1; }
|
||||
/* Modal styles */
|
||||
.modal-backdrop {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0,0,0,0.5);
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
.modal {
|
||||
background: #2d2d2d;
|
||||
border-radius: 8px;
|
||||
width: 80vw;
|
||||
height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
||||
}
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #3a3a3a;
|
||||
}
|
||||
.modal-close-btn {
|
||||
background: #f44336;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.modal-body {
|
||||
flex: 1 1 auto;
|
||||
overflow: auto;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid #3a3a3a;
|
||||
}
|
||||
.footer-left { color: #cccccc; font-style: italic; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Mindcraft</h1>
|
||||
<div class="title-row">
|
||||
<div class="title-left">
|
||||
<h1 style="margin: 0;">Mindcraft</h1>
|
||||
<span id="msStatus" class="status-badge offline">mindserver offline</span>
|
||||
</div>
|
||||
<div class="title-spacer"></div>
|
||||
</div>
|
||||
<div id="agents"></div>
|
||||
|
||||
<div id="createAgentSection" style="margin-top:20px;background:#2d2d2d;padding:20px;border-radius:8px;">
|
||||
<h2>Create Agent</h2>
|
||||
<div id="settingsForm"></div>
|
||||
<div id="profileStatus" style="margin-top:6px;font-style:italic;color:#cccccc;">Profile: Not uploaded</div>
|
||||
<div style="margin-top:10px;">
|
||||
<button id="uploadProfileBtn" class="start-btn">Upload Profile</button>
|
||||
<input type="file" id="profileFileInput" accept=".json,application/json" style="display:none">
|
||||
<button id="submitCreateAgentBtn" class="start-btn" disabled>Create Agent</button>
|
||||
<div class="page-footer">
|
||||
<div>
|
||||
<button id="openCreateAgentBtn" class="start-btn">New Agent</button>
|
||||
</div>
|
||||
<div style="display:flex; gap:12px;">
|
||||
<button class="stop-btn" onclick="disconnectAllAgents()">Disconnect All Agents</button>
|
||||
<button class="stop-btn" onclick="confirmShutdown()">Full Shutdown</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
<div id="createAgentModal" class="modal-backdrop">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 style="margin:0;">Create Agent</h2>
|
||||
<button id="closeCreateAgentBtn" class="modal-close-btn">Close</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="createAgentSection">
|
||||
<div id="profileStatus" style="margin:6px 0;">Profile: Not uploaded</div>
|
||||
<div id="settingsForm"></div>
|
||||
<div id="createError" style="color:#f44336;margin-top:10px;"></div>
|
||||
<input type="file" id="profileFileInput" accept=".json,application/json" style="display:none">
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="footer-left" id="footerStatus">Configure settings, then upload a profile and create the agent.</div>
|
||||
<div class="footer-actions">
|
||||
<button id="uploadProfileBtn" class="start-btn">Upload Profile</button>
|
||||
<button id="submitCreateAgentBtn" class="start-btn" disabled>Create Agent</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Agent Settings Modal -->
|
||||
<div id="agentSettingsModal" class="modal-backdrop">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 id="agentSettingsTitle" style="margin:0;">Agent Settings</h2>
|
||||
<button id="closeAgentSettingsBtn" class="modal-close-btn">Close</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div id="agentSettingsSection">
|
||||
<div id="agentSettingsForm"></div>
|
||||
<div id="agentSettingsError" style="color:#f44336;margin-top:10px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="footer-left" id="agentSettingsFooter">Modify settings then apply to restart the agent.</div>
|
||||
<div class="footer-actions">
|
||||
<button id="discardAgentSettingsBtn" class="stop-btn" style="background:#777;">Discard Changes</button>
|
||||
<button id="applyAgentSettingsBtn" class="start-btn" disabled>Apply & Restart</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="createError" style="color:#f44336;margin-top:10px;"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
@ -143,45 +383,86 @@
|
|||
let profileData = null;
|
||||
const agentSettings = {};
|
||||
const agentLastMessage = {};
|
||||
let currentAgents = [];
|
||||
|
||||
const statusEl = document.getElementById('msStatus');
|
||||
function updateStatus(connected) {
|
||||
if (!statusEl) return;
|
||||
if (connected) {
|
||||
statusEl.textContent = 'MindServer online';
|
||||
statusEl.classList.remove('offline');
|
||||
statusEl.classList.add('online');
|
||||
} else {
|
||||
statusEl.textContent = 'MindServer offline';
|
||||
statusEl.classList.remove('online');
|
||||
statusEl.classList.add('offline');
|
||||
}
|
||||
}
|
||||
function subscribeToState() {
|
||||
socket.emit('listen-to-agents');
|
||||
}
|
||||
// Initial status
|
||||
updateStatus(false);
|
||||
socket.on('connect', () => {
|
||||
updateStatus(true);
|
||||
subscribeToState();
|
||||
// Clear all cached settings on reconnect
|
||||
Object.keys(agentSettings).forEach(name => delete agentSettings[name]);
|
||||
});
|
||||
socket.on('disconnect', () => {
|
||||
updateStatus(false);
|
||||
});
|
||||
socket.on('connect_error', () => {
|
||||
updateStatus(false);
|
||||
});
|
||||
|
||||
fetch('/settings_spec.json')
|
||||
.then(r => r.json())
|
||||
.then(spec => {
|
||||
settingsSpec = spec;
|
||||
const form = document.getElementById('settingsForm');
|
||||
Object.keys(spec).forEach(key => {
|
||||
if (key === 'profile') return; // profile handled via upload
|
||||
const cfg = spec[key];
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'setting-wrapper';
|
||||
const label = document.createElement('label');
|
||||
label.textContent = key;
|
||||
label.title = cfg.description || '';
|
||||
let input;
|
||||
switch (cfg.type) {
|
||||
case 'boolean':
|
||||
input = document.createElement('input');
|
||||
input.type = 'checkbox';
|
||||
input.checked = cfg.default === true;
|
||||
break;
|
||||
case 'number':
|
||||
input = document.createElement('input');
|
||||
input.type = 'number';
|
||||
input.value = cfg.default;
|
||||
break;
|
||||
default:
|
||||
input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.value = typeof cfg.default === 'object' ? JSON.stringify(cfg.default) : cfg.default;
|
||||
}
|
||||
input.title = cfg.description || '';
|
||||
input.id = `setting-${key}`;
|
||||
wrapper.appendChild(label);
|
||||
wrapper.appendChild(input);
|
||||
form.appendChild(wrapper);
|
||||
});
|
||||
buildSettingsForm();
|
||||
});
|
||||
|
||||
function buildSettingsForm() {
|
||||
const form = document.getElementById('settingsForm');
|
||||
form.innerHTML = '';
|
||||
// ensure grid for multi-column layout
|
||||
form.style.display = 'grid';
|
||||
form.style.gridTemplateColumns = 'repeat(auto-fit, minmax(320px, 1fr))';
|
||||
form.style.gap = '8px';
|
||||
Object.keys(settingsSpec).forEach(key => {
|
||||
if (key === 'profile') return; // profile handled via upload
|
||||
const cfg = settingsSpec[key];
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'setting-wrapper';
|
||||
const label = document.createElement('label');
|
||||
label.textContent = key;
|
||||
label.title = cfg.description || '';
|
||||
let input;
|
||||
switch (cfg.type) {
|
||||
case 'boolean':
|
||||
input = document.createElement('input');
|
||||
input.type = 'checkbox';
|
||||
input.checked = cfg.default === true;
|
||||
break;
|
||||
case 'number':
|
||||
input = document.createElement('input');
|
||||
input.type = 'number';
|
||||
input.value = cfg.default;
|
||||
break;
|
||||
default:
|
||||
input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.value = typeof cfg.default === 'object' ? JSON.stringify(cfg.default) : cfg.default;
|
||||
}
|
||||
input.title = cfg.description || '';
|
||||
input.id = `setting-${key}`;
|
||||
wrapper.appendChild(label);
|
||||
wrapper.appendChild(input);
|
||||
form.appendChild(wrapper);
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById('uploadProfileBtn').addEventListener('click', () => {
|
||||
document.getElementById('profileFileInput').click();
|
||||
});
|
||||
|
@ -233,10 +514,22 @@
|
|||
document.getElementById('submitCreateAgentBtn').disabled = true;
|
||||
document.getElementById('profileStatus').textContent = 'Profile: Not uploaded';
|
||||
document.getElementById('createError').textContent = '';
|
||||
hideCreateAgentModal();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Modal open/close logic
|
||||
const modalBackdrop = document.getElementById('createAgentModal');
|
||||
document.getElementById('openCreateAgentBtn').addEventListener('click', () => {
|
||||
buildSettingsForm();
|
||||
modalBackdrop.style.display = 'flex';
|
||||
});
|
||||
function hideCreateAgentModal() {
|
||||
modalBackdrop.style.display = 'none';
|
||||
}
|
||||
document.getElementById('closeCreateAgentBtn').addEventListener('click', hideCreateAgentModal);
|
||||
|
||||
socket.on('bot-output', (agentName, message) => {
|
||||
agentLastMessage[agentName] = message;
|
||||
const messageDiv = document.getElementById(`lastMessage-${agentName}`);
|
||||
|
@ -245,6 +538,68 @@
|
|||
}
|
||||
});
|
||||
|
||||
// Subscribe to aggregated state updates (re-sent on each connect)
|
||||
socket.on('state-update', (states) => {
|
||||
window.lastStates = states;
|
||||
Object.keys(states || {}).forEach(name => {
|
||||
const st = states[name];
|
||||
const healthEl = document.getElementById(`health-${name}`);
|
||||
if (st && !st.error) {
|
||||
const gp = st.gameplay || {};
|
||||
if (healthEl && typeof gp.health === 'number') {
|
||||
const hMax = typeof gp.healthMax === 'number' ? gp.healthMax : 20;
|
||||
healthEl.textContent = `health: ${gp.health}/${hMax}`;
|
||||
}
|
||||
const posEl = document.getElementById(`pos-${name}`);
|
||||
const hunEl = document.getElementById(`hunger-${name}`);
|
||||
const bioEl = document.getElementById(`biome-${name}`);
|
||||
const modeEl = document.getElementById(`mode-${name}`);
|
||||
const itemsEl = document.getElementById(`items-${name}`);
|
||||
const equippedEl = document.getElementById(`equipped-${name}`);
|
||||
const invGrid = document.getElementById(`inventory-${name}`);
|
||||
const actionEl = document.getElementById(`action-${name}`);
|
||||
if (posEl && gp.position) {
|
||||
const p = gp.position;
|
||||
posEl.textContent = `x ${p.x}, y ${p.y}, z ${p.z}`;
|
||||
}
|
||||
if (hunEl && typeof gp.hunger === 'number') {
|
||||
const fMax = typeof gp.hungerMax === 'number' ? gp.hungerMax : 20;
|
||||
hunEl.textContent = `hunger: ${gp.hunger}/${fMax}`;
|
||||
}
|
||||
if (bioEl && gp.biome) bioEl.textContent = `biome: ${gp.biome}`;
|
||||
if (modeEl && gp.gamemode) modeEl.textContent = `gamemode: ${gp.gamemode}`;
|
||||
if (itemsEl && st.inventory) {
|
||||
const used = st.inventory.stacksUsed ?? 0;
|
||||
const total = st.inventory.totalSlots ?? 0;
|
||||
itemsEl.textContent = `inventory slots: ${used}/${total}`;
|
||||
}
|
||||
if (equippedEl && st.inventory?.equipment) {
|
||||
const e = st.inventory.equipment;
|
||||
equippedEl.textContent = `equipped: ${e.mainHand || 'none'}`;
|
||||
}
|
||||
const armorEl = document.getElementById(`armor-${name}`);
|
||||
if (armorEl && st.inventory?.equipment) {
|
||||
const e = st.inventory.equipment;
|
||||
const armor = [];
|
||||
if (e.helmet) armor.push(`head: ${e.helmet}`);
|
||||
if (e.chestplate) armor.push(`chest: ${e.chestplate}`);
|
||||
if (e.leggings) armor.push(`legs: ${e.leggings}`);
|
||||
if (e.boots) armor.push(`feet: ${e.boots}`);
|
||||
armorEl.textContent = `armor: ${armor.length ? armor.join(', ') : 'none'}`;
|
||||
}
|
||||
if (actionEl && st.action) {
|
||||
actionEl.textContent = `${st.action.current || 'Idle'}`;
|
||||
}
|
||||
if (invGrid && st.inventory?.counts) {
|
||||
const counts = st.inventory.counts;
|
||||
invGrid.innerHTML = Object.keys(counts).length ?
|
||||
Object.entries(counts).map(([k, v]) => `<div class="cell">${k}: ${v}</div>`).join('') :
|
||||
'<div class="cell">(empty)</div>';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function fetchAgentSettings(name) {
|
||||
return new Promise((resolve) => {
|
||||
if (agentSettings[name]) { resolve(agentSettings[name]); return; }
|
||||
|
@ -257,48 +612,347 @@
|
|||
});
|
||||
}
|
||||
|
||||
async function renderAgents(agents) {
|
||||
// fetch settings for any new agents
|
||||
await Promise.all(agents.map(a => fetchAgentSettings(a.name)));
|
||||
// Agent settings modal logic
|
||||
const agentSettingsModal = document.getElementById('agentSettingsModal');
|
||||
const agentSettingsForm = document.getElementById('agentSettingsForm');
|
||||
const applyBtn = document.getElementById('applyAgentSettingsBtn');
|
||||
const discardBtn = document.getElementById('discardAgentSettingsBtn');
|
||||
const closeAgentSettingsBtn = document.getElementById('closeAgentSettingsBtn');
|
||||
const agentSettingsTitle = document.getElementById('agentSettingsTitle');
|
||||
let currentAgentName = null;
|
||||
let originalAgentSettings = null;
|
||||
|
||||
agentsDiv.innerHTML = agents.length ?
|
||||
agents.map((agent, idx) => {
|
||||
const cfg = agentSettings[agent.name] || {};
|
||||
const showViewer = cfg.render_bot_view === true;
|
||||
const viewerHTML = showViewer ? `<div class="agent-view-container"><iframe class="agent-viewer" src="http://localhost:${3000 + idx}"></iframe></div>` : '';
|
||||
const lastMessage = agentLastMessage[agent.name] || '';
|
||||
return `
|
||||
<div class="agent">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;width:100%;">
|
||||
<span><span class="status-icon ${agent.in_game ? 'online' : 'offline'}">●</span>${agent.name}</span>
|
||||
<div style="display:flex;align-items:center;">
|
||||
${agent.in_game ? `
|
||||
<button class="stop-btn" onclick="stopAgent('${agent.name}')">Stop</button>
|
||||
<button class="restart-btn" onclick="restartAgent('${agent.name}')">Restart</button>
|
||||
<input type="text" id="messageInput-${agent.name}" placeholder="Enter message..." style="margin-left:4px;">
|
||||
<button class="start-btn" onclick="sendMessage('${agent.name}', document.getElementById('messageInput-${agent.name}').value)">Send</button>
|
||||
` : `
|
||||
<button class="start-btn" onclick="startAgent('${agent.name}')">Start</button>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
<div id="lastMessage-${agent.name}" class="last-message">${lastMessage}</div>
|
||||
${viewerHTML}
|
||||
</div>`;
|
||||
}).join('') +
|
||||
`<button class="stop-btn" onclick="killAllAgents()">Stop All</button>
|
||||
<button class="stop-btn" onclick="shutdown()">Shutdown</button>` :
|
||||
'<div class="agent">No agents connected</div>';
|
||||
function buildAgentSettingsForm(settings) {
|
||||
agentSettingsForm.innerHTML = '';
|
||||
agentSettingsForm.style.display = 'grid';
|
||||
agentSettingsForm.style.gridTemplateColumns = 'repeat(auto-fit, minmax(320px, 1fr))';
|
||||
agentSettingsForm.style.gap = '8px';
|
||||
Object.keys(settingsSpec).forEach(key => {
|
||||
if (key === 'profile') return; // profile not edited here
|
||||
const cfg = settingsSpec[key];
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'setting-wrapper';
|
||||
const label = document.createElement('label');
|
||||
label.textContent = key;
|
||||
label.title = cfg.description || '';
|
||||
let input;
|
||||
switch (cfg.type) {
|
||||
case 'boolean':
|
||||
input = document.createElement('input');
|
||||
input.type = 'checkbox';
|
||||
input.checked = Boolean(settings[key]);
|
||||
break;
|
||||
case 'number':
|
||||
input = document.createElement('input');
|
||||
input.type = 'number';
|
||||
input.value = settings[key] ?? cfg.default ?? 0;
|
||||
break;
|
||||
default:
|
||||
input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
const defVal = settings[key] ?? cfg.default ?? '';
|
||||
input.value = typeof defVal === 'object' ? JSON.stringify(defVal) : defVal;
|
||||
}
|
||||
input.id = `agent-setting-${key}`;
|
||||
input.addEventListener('input', onAgentSettingsChanged);
|
||||
if (input.type === 'checkbox') input.addEventListener('change', onAgentSettingsChanged);
|
||||
wrapper.appendChild(label);
|
||||
wrapper.appendChild(input);
|
||||
agentSettingsForm.appendChild(wrapper);
|
||||
});
|
||||
onAgentSettingsChanged();
|
||||
}
|
||||
|
||||
socket.on('agents-update', agents => { renderAgents(agents); });
|
||||
function openAgentSettings(name) {
|
||||
currentAgentName = name;
|
||||
agentSettingsTitle.textContent = `${name} Settings`;
|
||||
fetchAgentSettings(name).then(settings => {
|
||||
originalAgentSettings = JSON.parse(JSON.stringify(settings || {}));
|
||||
buildAgentSettingsForm(settings || {});
|
||||
agentSettingsModal.style.display = 'flex';
|
||||
});
|
||||
}
|
||||
window.openAgentSettings = openAgentSettings;
|
||||
|
||||
function getEditedAgentSettings() {
|
||||
const newSettings = { profile: (originalAgentSettings && originalAgentSettings.profile) || {} };
|
||||
Object.keys(settingsSpec).forEach(key => {
|
||||
if (key === 'profile') return;
|
||||
const cfg = settingsSpec[key];
|
||||
const input = document.getElementById(`agent-setting-${key}`);
|
||||
if (!input) return;
|
||||
let val;
|
||||
if (cfg.type === 'boolean') val = input.checked;
|
||||
else if (cfg.type === 'number') val = Number(input.value);
|
||||
else if (cfg.type === 'array' || cfg.type === 'object') {
|
||||
try { val = JSON.parse(input.value); }
|
||||
catch { val = input.value; }
|
||||
} else val = input.value;
|
||||
newSettings[key] = val;
|
||||
});
|
||||
return newSettings;
|
||||
}
|
||||
|
||||
function shallowEqual(a, b) {
|
||||
if (!a || !b) return false;
|
||||
const keys = Object.keys(settingsSpec).filter(k => k !== 'profile');
|
||||
for (const k of keys) {
|
||||
const va = a[k];
|
||||
const vb = b[k];
|
||||
if (typeof va === 'object' || typeof vb === 'object') {
|
||||
if (JSON.stringify(va) !== JSON.stringify(vb)) return false;
|
||||
} else if (va !== vb) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function onAgentSettingsChanged() {
|
||||
if (!originalAgentSettings) { applyBtn.disabled = true; return; }
|
||||
const edited = getEditedAgentSettings();
|
||||
applyBtn.disabled = shallowEqual(edited, originalAgentSettings);
|
||||
}
|
||||
|
||||
function closeAgentSettings() {
|
||||
agentSettingsModal.style.display = 'none';
|
||||
currentAgentName = null;
|
||||
originalAgentSettings = null;
|
||||
}
|
||||
|
||||
function updateAgentViewer(name) {
|
||||
const agentEl = document.getElementById(`agent-${name}`);
|
||||
if (!agentEl) return;
|
||||
|
||||
const settings = agentSettings[name];
|
||||
const viewerContainer = agentEl.querySelector('.agent-view-container');
|
||||
if (!viewerContainer) return;
|
||||
|
||||
const agentState = currentAgents.find(a => a.name === name);
|
||||
const shouldShow = agentState?.in_game && settings?.render_bot_view === true;
|
||||
viewerContainer.parentElement.style.display = shouldShow ? '' : 'none';
|
||||
}
|
||||
|
||||
discardBtn.addEventListener('click', () => {
|
||||
if (!currentAgentName || !originalAgentSettings) return;
|
||||
buildAgentSettingsForm(originalAgentSettings);
|
||||
});
|
||||
|
||||
applyBtn.addEventListener('click', () => {
|
||||
if (!currentAgentName) return;
|
||||
const edited = getEditedAgentSettings();
|
||||
socket.emit('set-agent-settings', currentAgentName, edited);
|
||||
// Update local settings immediately
|
||||
agentSettings[currentAgentName] = { ...edited, fetched: true };
|
||||
updateAgentViewer(currentAgentName);
|
||||
closeAgentSettings();
|
||||
});
|
||||
|
||||
closeAgentSettingsBtn.addEventListener('click', closeAgentSettings);
|
||||
|
||||
|
||||
function renderAgentCard(agent) {
|
||||
const cfg = agentSettings[agent.name] || {};
|
||||
const showViewer = agent.in_game && cfg.render_bot_view === true;
|
||||
const viewerPort = agent.viewerPort;
|
||||
const viewerHTML = showViewer ? `<div class="agent-view-container"><iframe class="agent-viewer" id="viewer-${agent.name}" src="http://localhost:${viewerPort}"></iframe></div>` : '';
|
||||
const lastMessage = agentLastMessage[agent.name] || '';
|
||||
return `
|
||||
<div class="agent" id="agent-${agent.name}">
|
||||
<div class="agent-grid">
|
||||
<div class="cell title" style="grid-column: 1 / -1; display:flex; align-items:center; justify-content:space-between;">
|
||||
<span><span class="status-icon ${agent.in_game ? 'online' : 'offline'}">●</span>${agent.name}${agent.socket_connected && !agent.in_game ? '<span style="margin-left:6px;color:#f0ad4e;">joining...</span>' : ''}
|
||||
<button class="gear-btn" title="Settings" onclick="openAgentSettings('${agent.name}')">⚙</button>
|
||||
<button class="gear-btn" title="Inventory" onclick="toggleDetails('${agent.name}')">Inventory</button>
|
||||
</span>
|
||||
</div>
|
||||
${showViewer ? `<div class="cell" style="grid-row: span 3; padding:0;">${viewerHTML}</div>` : ''}
|
||||
<div class="cell" id="action-${agent.name}">action: -</div>
|
||||
<div class="cell" id="mode-${agent.name}">gamemode: -</div>
|
||||
<div class="cell" id="health-${agent.name}">health: -</div>
|
||||
<div class="cell" id="hunger-${agent.name}">hunger: -</div>
|
||||
<div class="cell" id="pos-${agent.name}">pos: -</div>
|
||||
<div class="cell" id="biome-${agent.name}">biome: -</div>
|
||||
<div class="cell" id="items-${agent.name}">inventory slots: -</div>
|
||||
<div class="cell" id="equipped-${agent.name}">equipped: -</div>
|
||||
<div class="agent-inventory" id="inventorySection-${agent.name}" style="display:none; grid-column: 1 / -1;">
|
||||
<h3>Inventory</h3>
|
||||
<div class="cell" id="armor-${agent.name}" style="margin-bottom: 8px;">armor: -</div>
|
||||
<div class="inventory-grid" id="inventory-${agent.name}"></div>
|
||||
</div>
|
||||
<div id="lastMessage-${agent.name}" class="last-message" style="grid-column: 1 / -1;"><strong>Last Message:</strong> ${lastMessage}</div>
|
||||
</div>
|
||||
<div class="controls-row">
|
||||
<button class="start-btn" id="sendBtn-${agent.name}" disabled onclick="sendMessage('${agent.name}', document.getElementById('messageInput-${agent.name}').value)">Send</button>
|
||||
<input class="msg-input" type="text" id="messageInput-${agent.name}" placeholder="Enter message..."
|
||||
oninput="onMsgInputChange('${agent.name}')"
|
||||
onkeydown="if(event.key === 'Enter') document.getElementById('sendBtn-${agent.name}').click()"
|
||||
${!agent.in_game ? 'disabled' : ''}>
|
||||
<button class="neutral-btn" onclick="sendMessage('${agent.name}', '!stop')" ${!agent.in_game ? 'disabled' : ''}>Stop Action</button>
|
||||
<button class="neutral-btn" onclick="sendMessage('${agent.name}', '!stay(-1)')" ${!agent.in_game ? 'disabled' : ''}>Stay Still</button>
|
||||
<button class="neutral-btn" onclick="restartAgent('${agent.name}')" ${!agent.in_game ? 'disabled' : ''}>Restart</button>
|
||||
<button class="neutral-btn" ${agent.in_game ? `onclick=\"disconnectAgent('${agent.name}')\"` : (agent.socket_connected ? 'disabled' : `onclick=\"startAgent('${agent.name}')\"`)}>${agent.in_game ? 'Disconnect' : (agent.socket_connected ? 'Connecting...' : 'Connect')}</button>
|
||||
<button class="stop-btn" onclick="destroyAgent('${agent.name}')">Remove</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function renderAgents(agents) {
|
||||
if (!agents.length) {
|
||||
agentsDiv.innerHTML = '<div class="agent">No agents connected</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// If agentsDiv is empty, do a full render
|
||||
if (!agentsDiv.children.length) {
|
||||
agentsDiv.innerHTML = agents.map(agent => renderAgentCard(agent)).join('');
|
||||
// Update all viewers after initial render
|
||||
setTimeout(() => {
|
||||
agents.forEach(a => {
|
||||
if (a.in_game) updateAgentViewer(a.name);
|
||||
});
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Compare with current agents to find changes
|
||||
const prevAgents = currentAgents.reduce((acc, a) => ({ ...acc, [a.name]: a }), {});
|
||||
const changedAgents = agents.filter(a => {
|
||||
const prev = prevAgents[a.name];
|
||||
return !prev || prev.in_game !== a.in_game || prev.viewerPort !== a.viewerPort || prev.socket_connected !== a.socket_connected;
|
||||
});
|
||||
|
||||
// Update only changed agents
|
||||
changedAgents.forEach(agent => {
|
||||
const el = document.getElementById(`agent-${agent.name}`);
|
||||
if (el) {
|
||||
// Update existing card
|
||||
el.outerHTML = renderAgentCard(agent);
|
||||
if (agent.in_game) updateAgentViewer(agent.name);
|
||||
} else {
|
||||
// Add new card
|
||||
agentsDiv.insertAdjacentHTML('beforeend', renderAgentCard(agent));
|
||||
if (agent.in_game) updateAgentViewer(agent.name);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove cards for agents that no longer exist
|
||||
Array.from(agentsDiv.children).forEach(el => {
|
||||
const name = el.id.replace('agent-', '');
|
||||
if (!agents.find(a => a.name === name)) {
|
||||
el.remove();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
socket.on('agents-status', async (agents) => {
|
||||
// Fetch settings for all agents that don't have current settings
|
||||
const needSettings = agents.filter(a => !agentSettings[a.name]);
|
||||
if (needSettings.length > 0) {
|
||||
await Promise.all(needSettings.map(async (a) => {
|
||||
const settings = await fetchAgentSettings(a.name);
|
||||
if (settings) {
|
||||
agentSettings[a.name] = settings;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Compare with current agents to find changes
|
||||
const prevAgents = currentAgents.reduce((acc, a) => ({ ...acc, [a.name]: a }), {});
|
||||
const changedAgents = agents.filter(a => {
|
||||
const prev = prevAgents[a.name];
|
||||
return !prev || prev.in_game !== a.in_game || prev.viewerPort !== a.viewerPort || prev.socket_connected !== a.socket_connected;
|
||||
});
|
||||
|
||||
// Update current agents list
|
||||
currentAgents = agents;
|
||||
|
||||
// If agentsDiv is empty, do a full render
|
||||
if (!agentsDiv.children.length) {
|
||||
agentsDiv.innerHTML = agents.map(agent => renderAgentCard(agent)).join('');
|
||||
// Update all viewers after initial render
|
||||
setTimeout(() => {
|
||||
agents.forEach(a => {
|
||||
if (a.in_game) updateAgentViewer(a.name);
|
||||
});
|
||||
}, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update only changed agents
|
||||
changedAgents.forEach(agent => {
|
||||
const el = document.getElementById(`agent-${agent.name}`);
|
||||
if (el) {
|
||||
// Update existing card
|
||||
el.outerHTML = renderAgentCard(agent);
|
||||
if (agent.in_game) updateAgentViewer(agent.name);
|
||||
} else {
|
||||
// Add new card
|
||||
agentsDiv.insertAdjacentHTML('beforeend', renderAgentCard(agent));
|
||||
if (agent.in_game) updateAgentViewer(agent.name);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove cards for agents that no longer exist
|
||||
Array.from(agentsDiv.children).forEach(el => {
|
||||
const name = el.id.replace('agent-', '');
|
||||
if (!agents.find(a => a.name === name)) {
|
||||
el.remove();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function restartAgent(n) { socket.emit('restart-agent', n); }
|
||||
function startAgent(n) { socket.emit('start-agent', n); }
|
||||
function disconnectAgent(n) { socket.emit('stop-agent', n); }
|
||||
function startAgent(n) {
|
||||
const btn = document.querySelector(`button[onclick="startAgent('${n}')"]`);
|
||||
if (btn) {
|
||||
btn.textContent = 'Connecting...';
|
||||
btn.disabled = true;
|
||||
// Re-enable after 10s if still disabled (agent failed to connect)
|
||||
setTimeout(() => {
|
||||
const retryBtn = document.querySelector(`button[onclick=\\"startAgent('${n}')\\"]`);
|
||||
const agentState = (window.currentAgents || []).find(a => a.name === n);
|
||||
const stillWaiting = agentState ? (!agentState.in_game && !agentState.socket_connected) : true;
|
||||
if (retryBtn && stillWaiting) {
|
||||
retryBtn.disabled = false;
|
||||
retryBtn.textContent = 'Connect';
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
socket.emit('start-agent', n);
|
||||
}
|
||||
function stopAgent(n) { socket.emit('stop-agent', n); }
|
||||
function killAllAgents() { socket.emit('stop-all-agents'); }
|
||||
function shutdown() { socket.emit('shutdown'); }
|
||||
function sendMessage(n, m) { socket.emit('send-message', n, m); }
|
||||
function destroyAgent(n) { socket.emit('destroy-agent', n); }
|
||||
function disconnectAllAgents() {
|
||||
socket.emit('stop-all-agents');
|
||||
}
|
||||
function confirmShutdown() {
|
||||
if (confirm('Are you sure you want to perform a full shutdown?\nThis will stop all agents and close the server.')) {
|
||||
socket.emit('shutdown');
|
||||
}
|
||||
}
|
||||
function sendMessage(n, m) {
|
||||
if (!m || !m.trim()) return;
|
||||
socket.emit('send-message', n, { from: 'ADMIN', message: m });
|
||||
const input = document.getElementById(`messageInput-${n}`);
|
||||
const btn = document.getElementById(`sendBtn-${n}`);
|
||||
if (input) input.value = '';
|
||||
if (btn) btn.disabled = true;
|
||||
}
|
||||
function onMsgInputChange(name) {
|
||||
const input = document.getElementById(`messageInput-${name}`);
|
||||
const btn = document.getElementById(`sendBtn-${name}`);
|
||||
if (btn && input) {
|
||||
btn.disabled = !(input.value && input.value.trim().length > 0);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleDetails(name) {
|
||||
const invSection = document.getElementById(`inventorySection-${name}`);
|
||||
if (!invSection) return;
|
||||
const visible = invSection.style.display !== 'none';
|
||||
invSection.style.display = visible ? 'none' : '';
|
||||
}
|
||||
window.toggleDetails = toggleDetails;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
},
|
||||
"minecraft_version": {
|
||||
"type": "string",
|
||||
"description": "The version of Minecraft to use",
|
||||
"default": "1.21.1"
|
||||
"description": "The version of Minecraft to use. Set to 'auto' to automatically detect the version.",
|
||||
"default": "auto"
|
||||
},
|
||||
"host": {
|
||||
"type": "string",
|
||||
|
@ -16,18 +16,20 @@
|
|||
},
|
||||
"port": {
|
||||
"type": "number",
|
||||
"description": "The minecraft server port to connect to",
|
||||
"description": "The minecraft server port to connect to. -1 for auto-detect.",
|
||||
"default": 55916
|
||||
},
|
||||
"auth": {
|
||||
"type": "string",
|
||||
"description": "The authentication method to use",
|
||||
"default": "offline"
|
||||
"default": "offline",
|
||||
"options": ["offline", "microsoft"]
|
||||
},
|
||||
"base_profile": {
|
||||
"type": "string",
|
||||
"description": "Allowed values: survival, assistant, creative, god_mode. Each has fine tuned settings for different game modes.",
|
||||
"default": "survival"
|
||||
"default": "survival",
|
||||
"options": ["survival", "assistant", "creative", "god_mode"]
|
||||
},
|
||||
"load_memory": {
|
||||
"type": "boolean",
|
||||
|
@ -97,7 +99,13 @@
|
|||
"show_command_syntax": {
|
||||
"type": "string",
|
||||
"description": "Whether to show \"full\" command syntax, \"shortened\" command syntax, or \"none\"",
|
||||
"default": "full"
|
||||
"default": "full",
|
||||
"options": ["full", "shortened", "none"]
|
||||
},
|
||||
"chat_ingame": {
|
||||
"type": "boolean",
|
||||
"description": "Whether to show bot's chat messages in game chat",
|
||||
"default": true
|
||||
},
|
||||
"chat_bot_messages": {
|
||||
"type": "boolean",
|
||||
|
|
|
@ -55,7 +55,7 @@ export function selectAPI(profile) {
|
|||
else if (profile.model.includes('gemini'))
|
||||
profile.api = "google";
|
||||
else if (profile.model.includes('grok'))
|
||||
profile.api = 'grok';
|
||||
profile.api = 'xai';
|
||||
else if (profile.model.includes('mistral'))
|
||||
profile.api = 'mistral';
|
||||
else if (profile.model.includes('deepseek'))
|
||||
|
|
Loading…
Add table
Reference in a new issue