'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: `
`, 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 = '
'; // 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 += '

Add Memory

'+ ''+ ''+ ''+ '
'; 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 = '
'; 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

'+ ''+ '
'+ ''+ '
'+ '
'; 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 };