improve ui and default settings

This commit is contained in:
MaxRobinsonTheGreat 2025-06-16 16:32:40 -05:00
parent 0eb16cc3ec
commit 00127506b1
7 changed files with 341 additions and 161 deletions

View file

@ -28,7 +28,7 @@ const settings = {
"only_chat_with": [], // users that the bots listen to and send general messages to. if empty it will chat publicly
"speak": false, // allows all bots to speak through system text-to-speech. works on windows, mac, on linux you need to `apt install espeak`
"language": "en", // translate to/from this language. Supports these language names: https://cloud.google.com/translate/docs/languages
"render_bot_views": false, // show bot's view in browser at localhost:3000, 3001...
"render_bot_view": false, // show bot's view in browser at localhost:3000, 3001...
"allow_insecure_coding": false, // allows newAction command and model can write/run code on your computer. enable at own risk
"allow_vision": false, // allows vision model to interpret screenshots as inputs

View file

@ -23,10 +23,10 @@ export class Agent {
this.count_id = count_id;
// Initialize components with more detailed error handling
console.log(`Initializing agent ${this.name}...`);
this.actions = new ActionManager(this);
this.prompter = new Prompter(this, settings.profile);
this.name = this.prompter.getName();
console.log(`Initializing agent ${this.name}...`);
this.history = new History(this);
this.coder = new Coder(this);
this.npc = new NPCContoller(this);

View file

@ -1,25 +0,0 @@
{
"minecraft_version": "1.21.1",
"host": "127.0.0.1",
"port": 55916,
"auth": "offline",
"base_profile": "survival",
"load_memory": false,
"init_message": "Respond with hello world and your name",
"only_chat_with": [],
"speak": false,
"language": "en",
"allow_vision": false,
"blocked_actions" : ["!checkBlueprint", "!checkBlueprintLevel", "!getBlueprint", "!getBlueprintLevel"] ,
"relevant_docs_count": 5,
"max_messages": 15,
"num_examples": 2,
"max_commands": -1,
"narrate_behavior": true,
"log_all_prompts": false,
"verbose_commands": true,
"chat_bot_messages": true,
"render_bot_view": false,
"allow_insecure_coding": false,
"code_timeout_mins": -1
}

View file

@ -16,7 +16,7 @@ let io;
let server;
const agent_connections = {};
const default_settings = JSON.parse(readFileSync(path.join(__dirname, 'default_settings.json'), 'utf8'));
const settings_spec = JSON.parse(readFileSync(path.join(__dirname, 'public/settings_spec.json'), 'utf8'));
class AgentConnection {
constructor(settings) {
@ -58,7 +58,22 @@ export function createMindServer(host_public = false, port = 8080) {
socket.on('create-agent', (settings, callback) => {
console.log('API create agent...');
settings = { ...default_settings, ...settings };
for (let key in settings_spec) {
if (!(key in settings)) {
if (settings_spec[key].required) {
callback({ success: false, error: `Setting ${key} is required` });
return;
}
else {
settings[key] = settings_spec[key].default;
}
}
}
for (let key in settings) {
if (!(key in settings_spec)) {
delete settings[key];
}
}
if (settings.profile?.name) {
if (settings.profile.name in agent_connections) {
callback({ success: false, error: 'Agent already exists' });

View file

@ -58,97 +58,229 @@
.status-icon.offline {
color: #f44336;
}
#settingsForm {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 8px;
margin-top: 10px;
}
.setting-wrapper {
display: flex;
align-items: center;
gap: 6px;
background: #3a3a3a;
padding: 6px 8px;
border-radius: 4px;
width: 100%;
box-sizing: border-box;
min-width: 0;
}
.setting-wrapper label {
flex: 0 0 50%;
font-size: 0.9em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.setting-wrapper input[type="text"],
.setting-wrapper input[type="number"] {
flex: 1 1 0;
background: #262626;
border: 1px solid #555;
color: #e0e0e0;
border-radius: 4px;
padding: 4px 6px;
max-width: 100%;
min-width: 0;
}
.setting-wrapper input[type="checkbox"] {
transform: scale(1.2);
}
.agent-viewer {
width: 200px;
height: 150px;
border: none;
margin-left: 10px;
}
.start-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.agent-view-container {
margin-top: 6px;
display: flex;
justify-content: flex-start;
}
</style>
</head>
<body>
<h1>Mindcraft</h1>
<div id="agents"></div>
<div style="margin-top: 20px;">
<button id="createAgentBtn" class="start-btn">Create Agent</button>
<input type="file" id="agentConfigFile" accept=".json,application/json" style="display: none">
<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>
<div id="createError" style="color:#f44336;margin-top:10px;"></div>
</div>
<script>
const socket = io();
const agentsDiv = document.getElementById('agents');
let settingsSpec = {};
let profileData = null;
const agentSettings = {};
document.getElementById('createAgentBtn').addEventListener('click', () => {
document.getElementById('agentConfigFile').click();
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);
});
});
document.getElementById('uploadProfileBtn').addEventListener('click', () => {
document.getElementById('profileFileInput').click();
});
document.getElementById('agentConfigFile').addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) {
return;
}
document.getElementById('profileFileInput').addEventListener('change', e => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
reader.onload = ev => {
try {
const profile = JSON.parse(e.target.result);
socket.emit('create-agent', {profile}, (response) => {
if (response.success) {
alert('Agent created successfully!');
} else {
alert(`Error creating agent: ${response.error}`);
}
});
} catch (error) {
alert('Error parsing JSON file: ' + error.message);
profileData = JSON.parse(ev.target.result);
document.getElementById('submitCreateAgentBtn').disabled = false;
document.getElementById('profileStatus').textContent = `Profile: ${profileData.name || 'Uploaded'}`;
document.getElementById('createError').textContent = '';
} catch (err) {
document.getElementById('createError').textContent = 'Invalid profile JSON: ' + err.message;
profileData = null;
document.getElementById('submitCreateAgentBtn').disabled = true;
document.getElementById('profileStatus').textContent = 'Profile: Not uploaded';
}
};
reader.readAsText(file);
// Reset file input to allow re-uploading the same file
event.target.value = '';
e.target.value = '';
});
socket.on('agents-update', (agents) => {
agentsDiv.innerHTML = agents.length ?
agents.map(agent => `
document.getElementById('submitCreateAgentBtn').addEventListener('click', () => {
if (!profileData) return;
const settings = { profile: profileData };
Object.keys(settingsSpec).forEach(key => {
if (key === 'profile') return;
const input = document.getElementById(`setting-${key}`);
if (!input) return;
const type = settingsSpec[key].type;
let val;
if (type === 'boolean') val = input.checked;
else if (type === 'number') val = Number(input.value);
else if (type === 'array' || type === 'object') {
try { val = JSON.parse(input.value); }
catch { val = input.value; }
} else val = input.value;
settings[key] = val;
});
socket.emit('create-agent', settings, res => {
if (!res.success) {
document.getElementById('createError').textContent = res.error || 'Unknown error';
} else {
// reset on success
profileData = null;
document.getElementById('submitCreateAgentBtn').disabled = true;
document.getElementById('profileStatus').textContent = 'Profile: Not uploaded';
document.getElementById('createError').textContent = '';
}
});
});
function fetchAgentSettings(name) {
return new Promise((resolve) => {
if (agentSettings[name]) { resolve(agentSettings[name]); return; }
socket.emit('get-settings', name, res => {
if (res.settings) {
agentSettings[name] = res.settings;
resolve(res.settings);
} else resolve(null);
});
});
}
async function renderAgents(agents) {
// fetch settings for any new agents
await Promise.all(agents.map(a => fetchAgentSettings(a.name)));
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>` : '';
return `
<div class="agent">
<span>
<span class="status-icon ${agent.in_game ? 'online' : 'offline'}"></span>
${agent.name}
</span>
<div>
${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" placeholder="Enter a message or command..."></input><button class="start-btn" onclick="sendMessage('${agent.name}', document.getElementById('messageInput').value)">Send</button>
` : `
<button class="start-btn" onclick="startAgent('${agent.name}')">Start</button>
`}
<div style="display:flex;justify-content:space-between;align-items:center;">
<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>
`).join('') +
${viewerHTML}
</div>`;
}).join('') +
`<button class="stop-btn" onclick="killAllAgents()">Stop All</button>
<button class="stop-btn" onclick="shutdown()">Shutdown</button>` :
<button class="stop-btn" onclick="shutdown()">Shutdown</button>` :
'<div class="agent">No agents connected</div>';
});
function restartAgent(agentName) {
socket.emit('restart-agent', agentName);
}
function startAgent(agentName) {
socket.emit('start-agent', agentName);
}
socket.on('agents-update', agents => { renderAgents(agents); });
function stopAgent(agentName) {
socket.emit('stop-agent', agentName);
}
function killAllAgents() {
socket.emit('stop-all-agents');
}
function shutdown() {
socket.emit('shutdown');
}
function sendMessage(agentName, message) {
socket.emit('send-message', agentName, message)
}
function restartAgent(n) { socket.emit('restart-agent', n); }
function startAgent(n) { 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); }
</script>
</body>
</html>

View file

@ -0,0 +1,127 @@
{
"profile": {
"type": "object",
"required": true,
"description": "The profile object to use, including name, prompts, and examples"
},
"minecraft_version": {
"type": "string",
"description": "The version of Minecraft to use",
"default": "1.21.1"
},
"host": {
"type": "string",
"description": "The minecraft server host address to connect to",
"default": "127.0.0.1"
},
"port": {
"type": "number",
"description": "The minecraft server port to connect to",
"default": 55916
},
"auth": {
"type": "string",
"description": "The authentication method to use",
"default": "offline"
},
"base_profile": {
"type": "string",
"description": "Allowed values: survival, creative, god_mode. Each has fine tuned settings for different game modes.",
"default": "survival"
},
"load_memory": {
"type": "boolean",
"description": "Whether to load bot's previous memory",
"default": false
},
"init_message": {
"type": "string",
"description": "The initial message to send to the bot",
"default": "Respond with hello world and your name"
},
"only_chat_with": {
"type": "array",
"description": "List of agents to only chat with. If empty, the bot will chat publicly",
"default": []
},
"speak": {
"type": "boolean",
"description": "Whether to enable text-to-speech reading on the host machine",
"default": false
},
"language": {
"type": "string",
"description": "The language to automatically translate to and from using google translate",
"default": "en"
},
"allow_vision": {
"type": "boolean",
"description": "Whether to allow vision capabilities",
"default": false
},
"blocked_actions": {
"type": "array",
"description": "List of actions that are blocked",
"default": ["!checkBlueprint", "!checkBlueprintLevel", "!getBlueprint", "!getBlueprintLevel"]
},
"relevant_docs_count": {
"type": "number",
"description": "Number of relevant function documents to include in the prompt for LLM code writing",
"default": 5
},
"max_messages": {
"type": "number",
"description": "Maximum number of recent messages to keep in context for LLM",
"default": 15
},
"num_examples": {
"type": "number",
"description": "Number of examples to select to help prompt better LLM responses",
"default": 2
},
"max_commands": {
"type": "number",
"description": "Maximum number of commands allowed in consecutive responses. -1 for no limit",
"default": -1
},
"narrate_behavior": {
"type": "boolean",
"description": "Whether to openly chat automatic behavior like 'Picking up item!'",
"default": true
},
"log_all_prompts": {
"type": "boolean",
"description": "Whether to log all prompts to file. Can be very verbose.",
"default": false
},
"verbose_commands": {
"type": "boolean",
"description": "Whether to show full command syntax in bot responses. If false will use a shortened syntax.",
"default": true
},
"chat_bot_messages": {
"type": "boolean",
"description": "Whether to publicly chat messages to and from other bots",
"default": true
},
"render_bot_view": {
"type": "boolean",
"description": "Whether to render bot view for user observation. Does not give bot vision.",
"default": false
},
"allow_insecure_coding": {
"type": "boolean",
"description": "Whether to allow newAction command that let's LLM write/run code on host computer. Despite sandboxxing, it is potentially insecure.",
"default": false
},
"code_timeout_mins": {
"type": "number",
"description": "Number of minutes to allow code execution. -1 for no timeout",
"default": -1
},
"task": {
"type": "object",
"description": "The task object to give the agent on start. If null, the agent will not have a task.",
"default": null
}
}

View file

@ -1,69 +0,0 @@
<html>
<head>
<title>Viewer</title>
<style>
body {
margin: 0;
padding: 0;
}
#container {
display: flex;
flex-wrap: wrap;
width: 100%;
height: 100vh;
}
.iframe-wrapper {
border: none;
}
</style>
</head>
<body>
<div id="container">
<iframe data-port="3000" src="http://localhost:3000" class="iframe-wrapper"></iframe>
<iframe data-port="3001" src="http://localhost:3001" class="iframe-wrapper"></iframe>
<iframe data-port="3002" src="http://localhost:3002" class="iframe-wrapper"></iframe>
<iframe data-port="3003" src="http://localhost:3003" class="iframe-wrapper"></iframe>
</div>
<script>
function updateLayout() {
let width = window.innerWidth;
let height = window.innerHeight;
let iframes = document.querySelectorAll('.iframe-wrapper');
if (width > height) {
iframes.forEach(function(iframe) {
iframe.style.width = '50%';
iframe.style.height = '50%';
});
} else {
iframes.forEach(function(iframe) {
iframe.style.width = '100%';
iframe.style.height = '25%';
});
}
}
window.addEventListener('resize', updateLayout);
window.addEventListener('load', updateLayout);
let iframes = document.querySelectorAll('.iframe-wrapper');
iframes.forEach(function(iframe) {
let port = iframe.getAttribute('data-port');
let loaded = false;
function checkServer() {
fetch('http://localhost:' + port, { method: 'HEAD' })
.then(function(response) {
if (response.ok && !loaded) {
iframe.src = 'http://localhost:' + port;
}
})
.catch(function(error) {});
}
iframe.onload = function() {
loaded = true;
};
iframe.onerror = function() {
loaded = false;
};
setInterval(checkServer, 3000);
});
</script>
</body>
</html>