'use strict';
const express = require('express');
const { CJbot } = require('../../model/minecraft');
function createRouter() {
const router = express.Router();
router.get('/api/ai/status', (req, res) => {
try {
const result = {};
for (const [name, bot] of Object.entries(CJbot.bots)) {
const ai = bot.plunginsLoaded['Ai'];
if (!ai) continue;
const config = ai.__getConfig();
result[name] = {
connected: bot.isReady,
provider: config.provider || 'unknown',
model: config.model || 'unknown',
interval: ai.intervalLength,
promptName: ai.promptName || 'unknown',
active: !!ai._active,
};
}
res.json({ bots: result });
} catch (error) {
console.error('API Error /api/ai/status:', error);
res.status(500).json({ error: error.message });
}
});
// Get list of all bots (for UI)
router.get('/api/ai/bots', (req, res) => {
try {
const bots = [];
for (const [name, bot] of Object.entries(CJbot.bots)) {
const ai = bot.plunginsLoaded['Ai'];
bots.push({
name: name,
hasAI: !!ai,
hasMemory: !!(ai && ai.memoryDB)
});
}
res.json({ bots });
} catch (error) {
console.error('API Error /api/ai/bots:', error);
res.status(500).json({ error: error.message });
}
});
// Get all players with memories (shared across all bots)
router.get('/api/ai/memories/players', async (req, res) => {
try {
// Get players from any bot's memoryDB (they're shared now)
let players = [];
for (const [name, bot] of Object.entries(CJbot.bots)) {
const ai = bot.plunginsLoaded['Ai'];
if (ai && ai.memoryDB) {
players = await ai.memoryDB.getAllKnownPlayers();
break;
}
}
res.json({ players });
} catch (error) {
console.error('API Error /api/ai/memories/players:', error);
res.status(500).json({ error: error.message });
}
});
// Get memories for a specific player
router.get('/api/ai/memories/:botName/:playerName', async (req, res) => {
try {
const { botName, playerName } = req.params;
const bot = CJbot.bots[botName];
if (!bot) {
return res.status(404).json({ error: 'Bot not found' });
}
const ai = bot.plunginsLoaded['Ai'];
if (!ai || !ai.memoryDB) {
return res.status(404).json({ error: 'AI or memory DB not loaded' });
}
const memories = await ai.memoryDB.getAllPlayerMemories(playerName);
res.json({ bot: botName, player: playerName, memories });
} catch (error) {
console.error('API Error /api/ai/memories/:bot/:player:', error);
res.status(500).json({ error: error.message });
}
});
// Set or update a player memory
router.post('/api/ai/memories/:botName/:playerName', async (req, res) => {
try {
const { botName, playerName } = req.params;
const { key, value } = req.body;
if (!key || !value) {
return res.status(400).json({ error: 'key and value are required' });
}
const bot = CJbot.bots[botName];
if (!bot) {
return res.status(404).json({ error: 'Bot not found' });
}
const ai = bot.plunginsLoaded['Ai'];
if (!ai || !ai.memoryDB) {
return res.status(404).json({ error: 'AI or memory DB not loaded' });
}
await ai.memoryDB.setPlayerMemory(playerName, key, value);
res.json({ success: true, bot: botName, player: playerName, key, value });
} catch (error) {
console.error('API Error /api/ai/memories/:bot/:player POST:', error);
res.status(500).json({ error: error.message });
}
});
// Delete a player memory
router.delete('/api/ai/memories/:botName/:playerName/:key', async (req, res) => {
try {
const { botName, playerName, key } = req.params;
const bot = CJbot.bots[botName];
if (!bot) {
return res.status(404).json({ error: 'Bot not found' });
}
const ai = bot.plunginsLoaded['Ai'];
if (!ai || !ai.memoryDB) {
return res.status(404).json({ error: 'AI or memory DB not loaded' });
}
await ai.memoryDB.deletePlayerMemory(playerName, key);
res.json({ success: true, bot: botName, player: playerName, key });
} catch (error) {
console.error('API Error /api/ai/memories/:bot/:player/:key DELETE:', error);
res.status(500).json({ error: error.message });
}
});
// Get bot directives
router.get('/api/ai/directives/:botName', async (req, res) => {
try {
const { botName } = req.params;
const bot = CJbot.bots[botName];
if (!bot) {
return res.status(404).json({ error: 'Bot not found' });
}
const ai = bot.plunginsLoaded['Ai'];
if (!ai || !ai.memoryDB) {
return res.status(404).json({ error: 'AI or memory DB not loaded' });
}
const directives = await ai.memoryDB.getAllDirectives();
res.json({ bot: botName, directives });
} catch (error) {
console.error('API Error /api/ai/directives/:bot:', error);
res.status(500).json({ error: error.message });
}
});
// Set or update a directive
router.post('/api/ai/directives/:botName', async (req, res) => {
try {
const { botName } = req.params;
const { key, value } = req.body;
if (!key || !value) {
return res.status(400).json({ error: 'key and value are required' });
}
const bot = CJbot.bots[botName];
if (!bot) {
return res.status(404).json({ error: 'Bot not found' });
}
const ai = bot.plunginsLoaded['Ai'];
if (!ai || !ai.memoryDB) {
return res.status(404).json({ error: 'AI or memory DB not loaded' });
}
await ai.memoryDB.setDirective(key, value);
res.json({ success: true, bot: botName, key, value });
} catch (error) {
console.error('API Error /api/ai/directives/:bot POST:', error);
res.status(500).json({ error: error.message });
}
});
// Get general memories
router.get('/api/ai/general-memories/:botName', async (req, res) => {
try {
const { botName } = req.params;
const bot = CJbot.bots[botName];
if (!bot) {
return res.status(404).json({ error: 'Bot not found' });
}
const ai = bot.plunginsLoaded['Ai'];
if (!ai || !ai.memoryDB) {
return res.status(404).json({ error: 'AI or memory DB not loaded' });
}
const memories = await ai.memoryDB.getAllGeneralMemories();
res.json({ bot: botName, memories });
} catch (error) {
console.error('API Error /api/ai/general-memories/:bot:', error);
res.status(500).json({ error: error.message });
}
});
// Set or update a general memory
router.post('/api/ai/general-memories/:botName', async (req, res) => {
try {
const { botName } = req.params;
const { key, value } = req.body;
if (!key || !value) {
return res.status(400).json({ error: 'key and value are required' });
}
const bot = CJbot.bots[botName];
if (!bot) {
return res.status(404).json({ error: 'Bot not found' });
}
const ai = bot.plunginsLoaded['Ai'];
if (!ai || !ai.memoryDB) {
return res.status(404).json({ error: 'AI or memory DB not loaded' });
}
await ai.memoryDB.setGeneralMemory(key, value);
res.json({ success: true, bot: botName, key, value });
} catch (error) {
console.error('API Error /api/ai/general-memories/:bot POST:', error);
res.status(500).json({ error: error.message });
}
});
return router;
}
const webUI = {
tabId: 'ai',
tabLabel: 'AI',
tabOrder: 30,
html: `
AI Status
Memories
Directives
`,
css: `
.ai-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(340px,1fr));gap:16px}
.ai-card{background:#111827;border:1px solid #374151;border-radius:8px;padding:16px;transition:border-color .2s}
.ai-card:hover{border-color:#a78bfa}
.ai-card h3{font-size:1em;color:#a78bfa;margin-bottom:12px;display:flex;align-items:center;gap:8px}
.ai-info{font-size:.85em;color:#9ca3af;margin:4px 0}
.ai-info .ai-label{color:#6b7280;display:inline-block;min-width:80px}
.ai-info .ai-value{color:#e5e7eb}
.ai-status-badge{padding:2px 8px;border-radius:10px;font-size:.75em;font-weight:600}
.ai-status-badge.active{background:#059669;color:#fff}
.ai-status-badge.inactive{background:#6b7280;color:#fff}
.ai-empty{padding:40px 20px;color:#6b7280;text-align:center;font-size:.9em}
.ai-tabs{display:flex;gap:8px;margin-bottom:16px;border-bottom:1px solid #374151;padding-bottom:8px}
.ai-tab-btn{background:transparent;border:1px solid #374151;color:#9ca3af;padding:8px 16px;border-radius:6px;cursor:pointer;transition:all .2s}
.ai-tab-btn:hover{border-color:#a78bfa;color:#e5e7eb}
.ai-tab-btn.active{background:#a78bfa;border-color:#a78bfa;color:#111827}
.ai-tab-content{display:none}
.ai-tab-content.active{display:block}
.memory-card{background:#111827;border:1px solid #374151;border-radius:8px;padding:16px;margin-bottom:12px}
.memory-card h4{color:#a78bfa;margin:0 0 12px 0;font-size:.95em}
.memory-entry{display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid #1f2937}
.memory-entry:last-child{border-bottom:none}
.memory-key{color:#e5e7eb;font-weight:500}
.memory-value{color:#9ca3af;max-width:60%;overflow:hidden;text-overflow:ellipsis}
.memory-actions{display:flex;gap:8px}
.btn-sm{padding:4px 8px;font-size:.75em;border-radius:4px;border:1px solid #374151;background:#1f2937;color:#9ca3af;cursor:pointer}
.btn-sm:hover{border-color:#a78bfa;color:#e5e7eb}
.btn-danger{border-color:#dc2626;color:#fca5a5}
.btn-danger:hover{background:#dc2626;color:#fff}
.btn-success{border-color:#059669;color:#6ee7b7}
.btn-success:hover{background:#059669;color:#fff}
.memory-form{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}
.memory-form input{flex:1;min-width:150px;padding:8px;border-radius:4px;border:1px solid #374151;background:#1f2937;color:#e5e7eb}
.memory-form input:focus{outline:none;border-color:#a78bfa}
.player-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px}
.player-card{background:#1f2937;border:1px solid #374151;border-radius:6px;padding:12px;cursor:pointer;transition:border-color .2s}
.player-card:hover{border-color:#a78bfa}
.player-card.selected{border-color:#a78bfa;background:#2d1f4e}
.form-group{margin-bottom:12px}
.form-group label{display:block;color:#6b7280;font-size:.85em;margin-bottom:4px}
.form-group input,.form-group textarea{width:100%;padding:8px;border-radius:4px;border:1px solid #374151;background:#1f2937;color:#e5e7eb;box-sizing:border-box}
.form-group textarea{min-height:80px;resize:vertical}
.bot-select{margin-bottom:16px;padding:8px;border-radius:6px;border:1px solid #374151;background:#1f2937;color:#e5e7eb;min-width:200px}
`,
onTabActive: 'onAiTabActive',
js: `
let aiInterval=null;
let currentAiTab='status';
let selectedBot=null;
let selectedPlayer=null;
function onAiTabActive() {
// Initialize tab content structure if needed
var container = document.getElementById('ai-tab-content');
if (container && !container.querySelector('#ai-status')) {
container.innerHTML = '
';
}
loadAiStatus();
if (!aiInterval) aiInterval = setInterval(() => { if (currentTab === 'ai') loadAiStatus(); }, 10000);
}
function switchAiTab(tab) {
currentAiTab = tab;
var btns = document.querySelectorAll('.ai-tab-btn');
for (var i = 0; i < btns.length; i++) {
btns[i].classList.remove('active');
}
var activeBtn = document.querySelector('.ai-tab-btn[data-tab="'+tab+'"]');
if (activeBtn) activeBtn.classList.add('active');
var contents = document.querySelectorAll('.ai-tab-content');
for (var i = 0; i < contents.length; i++) {
contents[i].classList.remove('active');
}
var target = document.getElementById('ai-'+tab);
if (target) target.classList.add('active');
if (tab === 'memories') loadAiPlayers();
if (tab === 'directives') loadAiDirectives();
}
async function loadAiStatus() {
try {
const r = await fetch('/api/ai/status');
if (!r.ok) { document.getElementById('ai-status').innerHTML='Failed to load AI status
'; return; }
const d = await r.json();
renderAiStatus(d.bots || {});
} catch(e) {
document.getElementById('ai-status').innerHTML='Failed to load AI status: ' + escHtml(e.message) + '
';
}
}
function renderAiStatus(bots) {
const names = Object.keys(bots);
if (names.length === 0) {
document.getElementById('ai-status').innerHTML='No bots with AI loaded
';
return;
}
let html = '' + names.map(name => {
const ai = bots[name];
const badge = ai.active
? '
Active '
: '
Inactive ';
return '
' +
'
' + escHtml(name) + ' ' + badge + '' +
'
Provider: ' + escHtml(ai.provider) + '
' +
'
Model: ' + escHtml(ai.model) + '
' +
'
Interval: ' + ai.interval + 's
' +
'
Prompt: ' + escHtml(ai.promptName) + '
' +
'
';
}).join('') + '
';
document.getElementById('ai-status').innerHTML = html;
}
async function loadAiPlayers() {
try {
// First get list of all bots (for selecting which bot to edit with)
const botsR = await fetch('/api/ai/bots');
if (!botsR.ok) throw new Error('Failed to load bots');
const botsD = await botsR.json();
// Then get players with memories (shared across all bots)
const r = await fetch('/api/ai/memories/players');
if (!r.ok) throw new Error('Failed to load players');
const d = await r.json();
renderAiPlayers(d.players || [], botsD.bots || []);
} catch(e) {
document.getElementById('ai-memories').innerHTML='Failed to load players: ' + escHtml(e.message) + '
';
}
}
function renderAiPlayers(players, allBots) {
// players is now a flat array (shared memories)
// allBots is array of {name, hasAI, hasMemory}
if (!allBots || allBots.length === 0) {
document.getElementById('ai-memories').innerHTML='No bots available
';
return;
}
let html = 'Select Bot (for editing): ';
html += '-- Select -- ';
allBots.forEach(bot => {
html += ''+escHtml(bot.name) + (bot.hasAI ? '' : ' (no AI)')+' ';
});
html += '
';
// Player list (shared, no bot filtering)
if (players && players.length > 0) {
html += '' + players.map(player => {
const selected = selectedPlayer === player ? 'selected' : '';
return '
'+escHtml(player)+'
';
}).join('') + '
';
} else {
html += 'No players with stored memories
';
}
if (selectedPlayer) {
html += '
';
setTimeout(() => loadPlayerMemories(selectedBot, selectedPlayer), 0);
}
document.getElementById('ai-memories').innerHTML = html;
}
function onBotSelect(botName) {
selectedBot = botName;
}
function selectPlayer(playerName) {
selectedPlayer = playerName;
// Re-render to show selection highlight
loadAiPlayers();
}
async function loadPlayerMemories(botName, playerName) {
// Memories are shared, but we need a bot selected to edit
if (!botName) {
const area = document.getElementById('playerMemoriesArea');
if (area) {
area.innerHTML='Select a bot above to view/edit memories
';
}
return;
}
try {
const r = await fetch('/api/ai/memories/'+encodeURIComponent(botName)+'/'+encodeURIComponent(playerName));
if (!r.ok) throw new Error('Failed to load memories');
const d = await r.json();
renderPlayerMemories(d.memories || {}, botName, playerName);
} catch(e) {
const area = document.getElementById('playerMemoriesArea');
if (area) {
area.innerHTML='Failed to load memories: ' + escHtml(e.message) + '
';
}
}
}
function renderPlayerMemories(memories, botName, playerName) {
const area = document.getElementById('playerMemoriesArea');
if (!area) return; // Element doesn't exist yet
const keys = Object.keys(memories);
if (keys.length === 0) {
area.innerHTML='No memories stored for '+escHtml(playerName)+'
';
return;
}
let html = 'Memories for '+escHtml(playerName)+' (shared) ';
keys.forEach(key => {
html += '
'+
'
'+escHtml(key)+' '+
'
'+escHtml(memories[key])+' '+
'
';
});
html += '
';
html += '';
document.getElementById('playerMemoriesArea').innerHTML = html;
}
async function deleteMemory(botName, playerName, key) {
if (!confirm('Delete memory "'+key+'" for '+playerName+'?')) return;
try {
const r = await fetch('/api/ai/memories/'+encodeURIComponent(botName)+'/'+encodeURIComponent(playerName)+'/'+encodeURIComponent(key), { method: 'DELETE' });
if (!r.ok) throw new Error('Failed to delete');
loadPlayerMemories(botName, playerName);
} catch(e) {
alert('Failed to delete: ' + e.message);
}
}
async function addMemory(botName, playerName) {
const key = document.getElementById('memoryKey').value;
const value = document.getElementById('memoryValue').value;
if (!key || !value) { alert('Key and value required'); return; }
try {
const r = await fetch('/api/ai/memories/'+encodeURIComponent(botName)+'/'+encodeURIComponent(playerName), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key, value })
});
if (!r.ok) throw new Error('Failed to add');
document.getElementById('memoryKey').value = '';
document.getElementById('memoryValue').value = '';
loadPlayerMemories(botName, playerName);
} catch(e) {
alert('Failed to add: ' + e.message);
}
}
async function loadAiDirectives() {
try {
const r = await fetch('/api/ai/bots');
if (!r.ok) throw new Error('Failed to load bots');
const d = await r.json();
const bots = d.bots || [];
let html = 'Select Bot: ';
html += '-- Select -- ';
bots.forEach(bot => {
html += ''+escHtml(bot.name) + (bot.hasAI ? '' : ' (no AI)')+' ';
});
html += '
';
document.getElementById('ai-directives').innerHTML = html;
} catch(e) {
document.getElementById('ai-directives').innerHTML='Failed to load bots: ' + escHtml(e.message) + '
';
}
}
async function loadBotDirectives(botName) {
if (!botName) { document.getElementById('directivesArea').innerHTML = ''; return; }
try {
const r = await fetch('/api/ai/directives/'+encodeURIComponent(botName));
if (!r.ok) throw new Error('Failed to load directives');
const d = await r.json();
renderBotDirectives(d.directives || {}, botName);
} catch(e) {
document.getElementById('directivesArea').innerHTML='Failed to load: ' + escHtml(e.message) + '
';
}
}
function renderBotDirectives(directives, botName) {
const keys = Object.keys(directives);
let html = 'Directives for '+escHtml(botName)+' ';
if (keys.length === 0) {
html += '
No directives set
';
} else {
keys.forEach(key => {
html += '
'+
''+escHtml(key)+' '+
''+escHtml(directives[key])+' '+
'
';
});
}
html += '
';
html += 'Add Directive '+
'Key '+
'
'+
'Value '+
'
Add '+
'
';
document.getElementById('directivesArea').innerHTML = html;
}
async function addDirective(botName) {
const key = document.getElementById('directiveKey').value;
const value = document.getElementById('directiveValue').value;
if (!key || !value) { alert('Key and value required'); return; }
try {
const r = await fetch('/api/ai/directives/'+encodeURIComponent(botName), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ key, value })
});
if (!r.ok) throw new Error('Failed to add');
document.getElementById('directiveKey').value = '';
document.getElementById('directiveValue').value = '';
loadBotDirectives(botName);
} catch(e) {
alert('Failed to add: ' + e.message);
}
}
`,
};
module.exports = { createRouter, webUI };