forked from wmantly/mc-bot-town
640 lines
27 KiB
JavaScript
640 lines
27 KiB
JavaScript
'use strict';
|
|
|
|
const express = require('express');
|
|
const settings = require('./manager');
|
|
const Database = require('../storage/database');
|
|
|
|
function createRouter() {
|
|
const router = express.Router();
|
|
|
|
function dbAvailable() {
|
|
return Database && Database.db;
|
|
}
|
|
|
|
// ---- Global settings ----
|
|
|
|
router.get('/api/settings', async (req, res) => {
|
|
try {
|
|
const all = settings.getAll();
|
|
const registry = settings.getRegistry();
|
|
const result = registry.map(r => ({
|
|
...r,
|
|
value: all[r.key],
|
|
}));
|
|
res.json({ settings: result });
|
|
} catch (error) {
|
|
console.error('API Error /api/settings:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
router.get('/api/settings/:category', async (req, res) => {
|
|
try {
|
|
const values = settings.getAllByCategory(req.params.category);
|
|
const registry = settings.getRegistry().filter(r => r.category === req.params.category);
|
|
const result = registry.map(r => ({
|
|
...r,
|
|
value: values[r.key],
|
|
}));
|
|
res.json({ settings: result });
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
router.put('/api/settings/:key', async (req, res) => {
|
|
if (!dbAvailable()) return res.status(503).json({ error: 'Database not initialized' });
|
|
try {
|
|
const { key } = req.params;
|
|
const { value } = req.body;
|
|
if (value === undefined) {
|
|
return res.status(400).json({ error: 'Missing value' });
|
|
}
|
|
const newValue = await settings.set(key, value);
|
|
res.json({ key, value: newValue });
|
|
} catch (error) {
|
|
console.error('API Error /api/settings/:key:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// ---- Bot settings ----
|
|
|
|
router.get('/api/bot-settings', async (req, res) => {
|
|
try {
|
|
const names = settings.getBotNames();
|
|
const botReg = settings.getBotSettingsRegistry();
|
|
const bots = names.map(name => {
|
|
const s = settings.getBotSettings(name);
|
|
const flat = { name };
|
|
// Add metadata from registry for each key
|
|
for (const br of botReg) {
|
|
flat[br.key] = { value: s ? s[br.key] : null, type: br.type, label: br.label, description: br.description };
|
|
}
|
|
return flat;
|
|
});
|
|
res.json({ bots, registry: botReg });
|
|
} catch (error) {
|
|
console.error('API Error /api/bot-settings:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
router.get('/api/bot-settings/:botName', async (req, res) => {
|
|
try {
|
|
const s = settings.getBotSettings(req.params.botName);
|
|
if (!s) return res.status(404).json({ error: `Unknown bot: ${req.params.botName}` });
|
|
const botReg = settings.getBotSettingsRegistry();
|
|
const result = { name: req.params.botName };
|
|
for (const br of botReg) {
|
|
result[br.key] = { value: s[br.key], type: br.type, label: br.label, description: br.description };
|
|
}
|
|
res.json(result);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
router.put('/api/bot-settings/:botName/:key', async (req, res) => {
|
|
if (!dbAvailable()) return res.status(503).json({ error: 'Database not initialized' });
|
|
try {
|
|
const { botName, key } = req.params;
|
|
const { value } = req.body;
|
|
if (value === undefined) return res.status(400).json({ error: 'Missing value' });
|
|
const newValue = await settings.setBotSetting(botName, key, value);
|
|
res.json({ botName, key, value: newValue });
|
|
} catch (error) {
|
|
console.error('API Error /api/bot-settings/:botName/:key:', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
return router;
|
|
}
|
|
|
|
const webUI = {
|
|
tabId: 'settings',
|
|
tabLabel: 'Settings',
|
|
tabOrder: 35,
|
|
html: `
|
|
<div id="settingsArea">
|
|
<div class="settings-layout">
|
|
<div class="settings-sidebar" id="settingsSidebar"></div>
|
|
<div class="settings-main" id="settingsMain">
|
|
<div class="settings-empty">Select a category to view settings</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`,
|
|
css: `
|
|
.settings-layout{display:flex;gap:16px;min-height:400px}
|
|
.settings-sidebar{width:180px;flex-shrink:0;display:flex;flex-direction:column;gap:4px}
|
|
.settings-sidebar-btn{background:transparent;border:1px solid #374151;color:#9ca3af;padding:10px 14px;border-radius:6px;cursor:pointer;text-align:left;font-size:.9em;transition:all .2s}
|
|
.settings-sidebar-btn:hover{border-color:#60a5fa;color:#e5e7eb}
|
|
.settings-sidebar-btn.active{background:#1e40af;border-color:#60a5fa;color:#fff}
|
|
.settings-main{flex:1;min-width:0}
|
|
.settings-empty{padding:40px 20px;color:#6b7280;text-align:center;font-size:.9em}
|
|
.settings-grid{display:flex;flex-direction:column;gap:12px}
|
|
.settings-card{background:#111827;border:1px solid #374151;border-radius:8px;padding:14px 16px;transition:border-color .2s}
|
|
.settings-card:hover{border-color:#60a5fa}
|
|
.settings-card-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:8px}
|
|
.settings-card-label{font-weight:600;color:#e5e7eb;font-size:.95em}
|
|
.settings-card-desc{color:#6b7280;font-size:.8em;margin-bottom:10px}
|
|
.settings-card-key{color:#4b5563;font-size:.75em;font-family:monospace}
|
|
.settings-card-body{display:flex;gap:8px;align-items:center}
|
|
.settings-card-body input[type="text"],
|
|
.settings-card-body input[type="number"],
|
|
.settings-card-body input[type="password"]{flex:1;padding:8px 10px;border-radius:6px;border:1px solid #374151;background:#1f2937;color:#e5e7eb;font-size:.9em}
|
|
.settings-card-body input:focus{outline:none;border-color:#60a5fa}
|
|
.settings-card-body textarea{flex:1;padding:8px 10px;border-radius:6px;border:1px solid #374151;background:#1f2937;color:#e5e7eb;font-size:.85em;min-height:80px;resize:vertical;font-family:monospace}
|
|
.settings-card-body textarea:focus{outline:none;border-color:#60a5fa}
|
|
.settings-card-body input[type="checkbox"]{width:18px;height:18px;accent-color:#60a5fa}
|
|
.settings-card-body select{flex:1;padding:8px 10px;border-radius:6px;border:1px solid #374151;background:#1f2937;color:#e5e7eb;font-size:.9em}
|
|
.settings-card-body select:focus{outline:none;border-color:#60a5fa}
|
|
.settings-value-display{flex:1;padding:8px 10px;color:#9ca3af;font-size:.9em;font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
.settings-btn-save{padding:8px 16px;border-radius:6px;border:1px solid #059669;background:#065f46;color:#6ee7b7;cursor:pointer;font-size:.85em;white-space:nowrap;transition:all .2s}
|
|
.settings-btn-save:hover{background:#059669;color:#fff}
|
|
.settings-btn-save.saved{background:#059669;color:#fff}
|
|
.settings-toast{position:fixed;bottom:20px;right:20px;background:#059669;color:#fff;padding:12px 20px;border-radius:8px;font-size:.9em;z-index:9999;opacity:0;transform:translateY(10px);transition:all .3s}
|
|
.settings-toast.show{opacity:1;transform:translateY(0)}
|
|
.settings-toast.error{background:#dc2626}
|
|
.prompt-editor{margin-top:12px;border:1px solid #374151;border-radius:8px;overflow:hidden}
|
|
.prompt-editor-layout{display:flex;min-height:300px}
|
|
.prompt-editor-sidebar{width:180px;flex-shrink:0;background:#0f1729;border-right:1px solid #374151;display:flex;flex-direction:column}
|
|
.prompt-editor-sidebar-header{padding:10px 12px;border-bottom:1px solid #374151;display:flex;justify-content:space-between;align-items:center}
|
|
.prompt-editor-sidebar-title{color:#9ca3af;font-size:.75em;text-transform:uppercase;letter-spacing:.5px}
|
|
.prompt-editor-sidebar-list{flex:1;overflow-y:auto;padding:4px}
|
|
.prompt-editor-prompt-item{padding:8px 10px;border-radius:4px;cursor:pointer;color:#9ca3af;font-size:.85em;transition:all .15s;display:flex;justify-content:space-between;align-items:center}
|
|
.prompt-editor-prompt-item:hover{background:#1e293b;color:#e5e7eb}
|
|
.prompt-editor-prompt-item.active{background:#1e40af;color:#fff}
|
|
.prompt-editor-prompt-item .prompt-delete-x{opacity:0;color:#ef4444;font-weight:bold;font-size:1.1em;padding:0 4px;transition:opacity .15s}
|
|
.prompt-editor-prompt-item:hover .prompt-delete-x{opacity:.7}
|
|
.prompt-editor-prompt-item .prompt-delete-x:hover{opacity:1}
|
|
.prompt-editor-content{flex:1;display:flex;flex-direction:column;padding:12px}
|
|
.prompt-editor-content-label{color:#9ca3af;font-size:.8em;margin-bottom:6px}
|
|
.prompt-editor-content textarea{flex:1;min-height:250px;padding:10px;border-radius:6px;border:1px solid #374151;background:#1f2937;color:#e5e7eb;font-size:.85em;font-family:monospace;resize:vertical;line-height:1.5}
|
|
.prompt-editor-content textarea:focus{outline:none;border-color:#60a5fa}
|
|
.prompt-editor-actions{display:flex;gap:8px;margin-top:8px;align-items:center}
|
|
.prompt-editor-vars{font-size:.75em;color:#6b7280;margin-top:6px}
|
|
.prompt-editor-vars code{color:#93c5fd;font-size:.85em}
|
|
.prompt-editor-add-btn{padding:4px 8px;border-radius:4px;border:1px solid #374151;background:transparent;color:#9ca3af;cursor:pointer;font-size:.8em;transition:all .15s}
|
|
.prompt-editor-add-btn:hover{border-color:#60a5fa;color:#e5e7eb}
|
|
.bot-list{display:flex;flex-direction:column;gap:8px}
|
|
.bot-card{background:#111827;border:1px solid #374151;border-radius:8px;overflow:hidden;transition:border-color .2s}
|
|
.bot-card.expanded{border-color:#60a5fa}
|
|
.bot-card-header{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;cursor:pointer;user-select:none;transition:background .15s}
|
|
.bot-card-header:hover{background:#1e293b}
|
|
.bot-card-name{font-weight:600;color:#e5e7eb;font-size:1em}
|
|
.bot-card-summary{font-size:.8em;color:#6b7280;display:flex;gap:12px;flex-wrap:wrap}
|
|
.bot-card-summary span{white-space:nowrap}
|
|
.bot-card-summary .on{color:#6ee7b7}
|
|
.bot-card-summary .off{color:#ef4444}
|
|
.bot-card-arrow{color:#6b7280;transition:transform .2s;font-size:1.2em}
|
|
.bot-card.expanded .bot-card-arrow{transform:rotate(180deg)}
|
|
.bot-card-body{display:none;padding:0 16px 14px;border-top:1px solid #1f2937}
|
|
.bot-card.expanded .bot-card-body{display:block}
|
|
.bot-field{margin-top:10px}
|
|
.bot-field-label{font-size:.8em;color:#9ca3af;margin-bottom:4px}
|
|
.bot-field-row{display:flex;gap:8px;align-items:center}
|
|
.bot-field-row input[type="text"],
|
|
.bot-field-row input[type="number"],
|
|
.bot-field-row input[type="password"]{flex:1;padding:6px 10px;border-radius:6px;border:1px solid #374151;background:#1f2937;color:#e5e7eb;font-size:.85em}
|
|
.bot-field-row input:focus{outline:none;border-color:#60a5fa}
|
|
.bot-field-row textarea{flex:1;padding:6px 10px;border-radius:6px;border:1px solid #374151;background:#1f2937;color:#e5e7eb;font-size:.8em;font-family:monospace;min-height:50px;resize:vertical}
|
|
.bot-field-row textarea:focus{outline:none;border-color:#60a5fa}
|
|
.bot-field-row input[type="checkbox"]{width:16px;height:16px;accent-color:#60a5fa}
|
|
.bot-field-row .mini-save{padding:6px 12px;border-radius:6px;border:1px solid #059669;background:#065f46;color:#6ee7b7;cursor:pointer;font-size:.8em;white-space:nowrap;transition:all .2s}
|
|
.bot-field-row .mini-save:hover{background:#059669;color:#fff}
|
|
.bot-field-row .mini-save.saved{background:#059669;color:#fff}
|
|
`,
|
|
onTabActive: 'onSettingsTabActive',
|
|
js: `
|
|
let settingsData = [];
|
|
let activeCategory = null;
|
|
let botData = [];
|
|
let botRegistry = [];
|
|
|
|
function onSettingsTabActive() {
|
|
loadSettingsCategories();
|
|
}
|
|
|
|
async function loadSettingsCategories() {
|
|
try {
|
|
const r = await fetch('/api/settings');
|
|
if (!r.ok) throw new Error('Failed to load settings');
|
|
const d = await r.json();
|
|
settingsData = d.settings || [];
|
|
|
|
// Build category sidebar
|
|
const cats = {};
|
|
settingsData.forEach(s => {
|
|
if (!cats[s.category]) cats[s.category] = [];
|
|
cats[s.category].push(s);
|
|
});
|
|
|
|
// Ensure bots category exists
|
|
cats['bots'] = cats['bots'] || [];
|
|
|
|
const catOrder = ['ai', 'storage', 'server', 'farm', 'invites', 'general', 'bots'];
|
|
let sidebarHtml = '';
|
|
const catNames = Object.keys(cats);
|
|
catNames.sort((a, b) => {
|
|
const ia = catOrder.indexOf(a), ib = catOrder.indexOf(b);
|
|
if (ia >= 0 && ib >= 0) return ia - ib;
|
|
if (ia >= 0) return -1;
|
|
if (ib >= 0) return 1;
|
|
return a.localeCompare(b);
|
|
});
|
|
catNames.forEach(cat => {
|
|
const count = cats[cat].length;
|
|
const label = cat === 'bots' ? 'Bots' : cat.charAt(0).toUpperCase()+cat.slice(1);
|
|
const activeClass = activeCategory === cat ? ' active' : (activeCategory === null && cat === catNames[0] ? ' active' : '');
|
|
sidebarHtml += '<button class="settings-sidebar-btn'+activeClass+'" onclick="switchSettingsCategory(\\''+escHtml(cat)+'\\')">'+escHtml(label)+(count > 0 ? ' <span style="color:#6b7280;font-size:.8em">('+count+')</span>' : '')+'</button>';
|
|
});
|
|
|
|
document.getElementById('settingsSidebar').innerHTML = sidebarHtml;
|
|
|
|
if (!activeCategory) {
|
|
activeCategory = catNames[0] || null;
|
|
}
|
|
|
|
if (activeCategory === 'bots') {
|
|
await loadBotSettings();
|
|
} else {
|
|
renderSettings(activeCategory);
|
|
}
|
|
} catch(e) {
|
|
document.getElementById('settingsMain').innerHTML = '<div class="settings-empty">Failed to load settings: '+escHtml(e.message)+'</div>';
|
|
}
|
|
}
|
|
|
|
async function switchSettingsCategory(cat) {
|
|
activeCategory = cat;
|
|
const btns = document.querySelectorAll('.settings-sidebar-btn');
|
|
btns.forEach(b => {
|
|
b.classList.remove('active');
|
|
if (b.textContent.trim().startsWith(cat === 'bots' ? 'Bots' : cat.charAt(0).toUpperCase()+cat.slice(1))) b.classList.add('active');
|
|
});
|
|
|
|
if (cat === 'bots') {
|
|
await loadBotSettings();
|
|
} else {
|
|
renderSettings(cat);
|
|
}
|
|
}
|
|
|
|
async function loadBotSettings() {
|
|
try {
|
|
const r = await fetch('/api/bot-settings');
|
|
if (!r.ok) throw new Error('Failed to load bot settings');
|
|
const d = await r.json();
|
|
botData = d.bots || [];
|
|
botRegistry = d.registry || [];
|
|
renderBotSettings();
|
|
} catch(e) {
|
|
document.getElementById('settingsMain').innerHTML = '<div class="settings-empty">Failed to load bot settings: '+escHtml(e.message)+'</div>';
|
|
}
|
|
}
|
|
|
|
function renderBotSettings() {
|
|
if (!botData.length) {
|
|
document.getElementById('settingsMain').innerHTML = '<div class="settings-empty">No bots configured</div>';
|
|
return;
|
|
}
|
|
|
|
let html = '<div class="bot-list">';
|
|
botData.forEach(bot => {
|
|
const autoConnect = bot.autoConnect?.value;
|
|
const onDemand = bot.onDemand?.value;
|
|
const isReady = bot.isReady ? ' (online)' : '';
|
|
let summaryParts = [];
|
|
if (autoConnect) summaryParts.push('<span class="on">auto-connect</span>');
|
|
else summaryParts.push('<span class="off">no auto-connect</span>');
|
|
if (onDemand) summaryParts.push('<span class="on">on-demand</span>');
|
|
if (bot.commands?.value && Array.isArray(bot.commands.value)) {
|
|
summaryParts.push('<span>cmds: '+escHtml(bot.commands.value.join(','))+'</span>');
|
|
}
|
|
|
|
html += '<div class="bot-card" id="botCard_'+escHtml(bot.name)+'">'+
|
|
'<div class="bot-card-header" onclick="toggleBotCard(\\''+escHtml(bot.name)+'\\')">'+
|
|
'<div>'+
|
|
'<div class="bot-card-name">'+escHtml(bot.name)+isReady+'</div>'+
|
|
'<div class="bot-card-summary">'+summaryParts.join('')+'</div>'+
|
|
'</div>'+
|
|
'<div class="bot-card-arrow">▼</div>'+
|
|
'</div>'+
|
|
'<div class="bot-card-body">';
|
|
|
|
botRegistry.forEach(br => {
|
|
const field = bot[br.key];
|
|
if (!field) return;
|
|
let val = field.value;
|
|
if (br.type === 'boolean') val = val === true || val === 'true';
|
|
const displayVal = br.type === 'secret' ? (val ? '***' : '') : val;
|
|
const isSecret = br.type === 'secret';
|
|
|
|
let inputHtml;
|
|
if (br.type === 'boolean') {
|
|
inputHtml = '<input type="checkbox" id="bot_'+escHtml(bot.name)+'_'+escHtml(br.key)+'"'+(val ? ' checked' : '')+' onchange="saveBotSetting(\\''+escHtml(bot.name)+'\\',\\''+escHtml(br.key)+'\\',this.checked)">';
|
|
} else if (br.type === 'json') {
|
|
const jsonStr = val ? JSON.stringify(val, null, 2) : '';
|
|
inputHtml = '<textarea id="bot_'+escHtml(bot.name)+'_'+escHtml(br.key)+'" rows="3">'+escHtml(jsonStr)+'</textarea>';
|
|
} else if (isSecret) {
|
|
inputHtml = '<input type="password" id="bot_'+escHtml(bot.name)+'_'+escHtml(br.key)+'" value="'+escHtml(String(val || ''))+'" placeholder="(unchanged)">';
|
|
} else if (br.type === 'number') {
|
|
inputHtml = '<input type="number" id="bot_'+escHtml(bot.name)+'_'+escHtml(br.key)+'" value="'+escHtml(String(val ?? ''))+'">';
|
|
} else {
|
|
inputHtml = '<input type="text" id="bot_'+escHtml(bot.name)+'_'+escHtml(br.key)+'" value="'+escHtml(String(val ?? ''))+'">';
|
|
}
|
|
|
|
html += '<div class="bot-field">'+
|
|
'<div class="bot-field-label">'+escHtml(br.label)+' <code style="color:#4b5563">'+escHtml(br.key)+'</code> '+escHtml(br.description ? '- '+br.description : '')+'</div>'+
|
|
'<div class="bot-field-row">'+inputHtml;
|
|
if (br.type !== 'boolean') {
|
|
html += '<button class="mini-save" id="btn_bot_'+escHtml(bot.name)+'_'+escHtml(br.key)+'" onclick="saveBotSetting(\\''+escHtml(bot.name)+'\\',\\''+escHtml(br.key)+'\\',document.getElementById(\\'bot_'+escHtml(bot.name)+'_'+escHtml(br.key)+'\\').value)">Save</button>';
|
|
}
|
|
html += '</div></div>';
|
|
});
|
|
|
|
html += '</div></div>';
|
|
});
|
|
html += '</div>';
|
|
document.getElementById('settingsMain').innerHTML = html;
|
|
}
|
|
|
|
function toggleBotCard(name) {
|
|
const card = document.getElementById('botCard_'+name);
|
|
if (!card) return;
|
|
card.classList.toggle('expanded');
|
|
}
|
|
|
|
async function saveBotSetting(botName, key, value) {
|
|
const btn = document.getElementById('btn_bot_'+botName+'_'+key);
|
|
try {
|
|
const r = await fetch('/api/bot-settings/'+encodeURIComponent(botName)+'/'+encodeURIComponent(key), {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ value: value })
|
|
});
|
|
if (!r.ok) {
|
|
const err = await r.json().catch(() => ({ error: 'Unknown error' }));
|
|
throw new Error(err.error || 'Failed to save');
|
|
}
|
|
// Update local data
|
|
const bot = botData.find(b => b.name === botName);
|
|
if (bot && bot[key]) {
|
|
bot[key].value = (key === 'password' && value) ? value : value;
|
|
}
|
|
|
|
if (btn) {
|
|
btn.classList.add('saved');
|
|
btn.textContent = 'Saved!';
|
|
setTimeout(() => { btn.classList.remove('saved'); btn.textContent = 'Save'; }, 2000);
|
|
}
|
|
// If password field, clear it after save
|
|
if (key === 'password') {
|
|
const inp = document.getElementById('bot_'+botName+'_'+key);
|
|
if (inp) inp.value = '';
|
|
}
|
|
showToast('Saved '+botName+'.'+key);
|
|
} catch(e) {
|
|
showToast('Error: '+e.message, true);
|
|
if (btn) { btn.style.borderColor = '#dc2626'; btn.textContent = 'Error';
|
|
setTimeout(() => { btn.style.borderColor = '#059669'; btn.textContent = 'Save'; }, 3000); }
|
|
}
|
|
}
|
|
|
|
function renderSettings(category) {
|
|
if (!category) {
|
|
document.getElementById('settingsMain').innerHTML = '<div class="settings-empty">Select a category</div>';
|
|
return;
|
|
}
|
|
|
|
const items = settingsData.filter(s => s.category === category);
|
|
if (items.length === 0) {
|
|
document.getElementById('settingsMain').innerHTML = '<div class="settings-empty">No settings in this category</div>';
|
|
return;
|
|
}
|
|
|
|
let html = '<div class="settings-grid">';
|
|
items.forEach(s => {
|
|
const key = escHtml(s.key);
|
|
const label = escHtml(s.label || s.key);
|
|
const desc = escHtml(s.description || '');
|
|
const isSecret = s.type === 'secret';
|
|
let inputHtml = '';
|
|
|
|
if (s.key === 'ai.prompts') {
|
|
inputHtml = buildPromptEditor(s);
|
|
} else if (s.key === 'ai.promptName') {
|
|
const prompts = getPromptsMap();
|
|
const names = Object.keys(prompts);
|
|
if (names.length === 0) names.push('asshole');
|
|
inputHtml = '<select id="inp_'+key+'" onchange="saveSetting(\\''+key+'\\', this.value)">'+
|
|
names.map(n => '<option value="'+escHtml(n)+'"'+(String(s.value) === n ? ' selected' : '')+'>'+escHtml(n)+'</option>').join('')+
|
|
'</select>';
|
|
} else if (s.key === 'ai.prompCustom') {
|
|
inputHtml = '<textarea id="inp_'+key+'" rows="4" style="width:100%">'+escHtml(String(s.value ?? ''))+'</textarea>';
|
|
} else if (s.type === 'boolean') {
|
|
const checked = s.value === true ? ' checked' : '';
|
|
inputHtml = '<input type="checkbox" id="inp_'+key+'"'+checked+' onchange="saveSetting(\\''+key+'\\', this.checked)">';
|
|
} else if (isSecret) {
|
|
inputHtml = '<input type="password" id="inp_'+key+'" value="'+escHtml(String(s.value ?? ''))+'" placeholder="(unchanged)">';
|
|
} else if (s.type === 'json') {
|
|
const jsonStr = s.value ? JSON.stringify(s.value, null, 2) : '';
|
|
inputHtml = '<textarea id="inp_'+key+'" rows="4">'+escHtml(jsonStr)+'</textarea>';
|
|
} else if (s.type === 'number') {
|
|
inputHtml = '<input type="number" id="inp_'+key+'" value="'+escHtml(String(s.value ?? ''))+'" step="any">';
|
|
} else {
|
|
inputHtml = '<input type="text" id="inp_'+key+'" value="'+escHtml(String(s.value ?? ''))+'">';
|
|
}
|
|
|
|
html += '<div class="settings-card">'+
|
|
'<div class="settings-card-header">'+
|
|
'<div><div class="settings-card-label">'+label+'</div><div class="settings-card-key">'+key+'</div></div>'+
|
|
'</div>'+
|
|
'<div class="settings-card-desc">'+desc+'</div>'+
|
|
'<div class="settings-card-body">'+inputHtml;
|
|
|
|
if (s.key === 'ai.prompts') {
|
|
// Prompt editor handles its own save
|
|
} else if (s.key === 'ai.promptName' || s.key === 'ai.prompCustom' || s.type !== 'boolean') {
|
|
html += '<button class="settings-btn-save" onclick="saveSetting(\\''+key+'\\', document.getElementById(\\'inp_'+key+'\\').value)">Save</button>';
|
|
}
|
|
|
|
html += '</div></div>';
|
|
});
|
|
html += '</div>';
|
|
|
|
document.getElementById('settingsMain').innerHTML = html;
|
|
|
|
const promptsItem = items.find(s => s.key === 'ai.prompts');
|
|
if (promptsItem) initPromptEditor(promptsItem);
|
|
}
|
|
|
|
async function saveSetting(key, value) {
|
|
const btn = event && event.target ? event.target : null;
|
|
try {
|
|
const r = await fetch('/api/settings/'+encodeURIComponent(key), {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ value: value })
|
|
});
|
|
if (!r.ok) {
|
|
const err = await r.json().catch(() => ({ error: 'Unknown error' }));
|
|
throw new Error(err.error || 'Failed to save');
|
|
}
|
|
const d = await r.json();
|
|
const item = settingsData.find(s => s.key === key);
|
|
if (item) item.value = d.value;
|
|
|
|
if (btn) {
|
|
btn.classList.add('saved');
|
|
btn.textContent = 'Saved!';
|
|
setTimeout(() => { btn.classList.remove('saved'); btn.textContent = 'Save'; }, 2000);
|
|
}
|
|
// Clear password field after save
|
|
const isSecret = settingsData.find(s => s.key === key)?.type === 'secret';
|
|
if (isSecret) {
|
|
const inp = document.getElementById('inp_'+key);
|
|
if (inp) inp.value = '';
|
|
}
|
|
showToast('Saved '+key);
|
|
} catch(e) {
|
|
showToast('Error: '+e.message, true);
|
|
if (btn) {
|
|
btn.style.borderColor = '#dc2626'; btn.style.background = '#7f1d1d'; btn.style.color = '#fca5a5'; btn.textContent = 'Error';
|
|
setTimeout(() => { btn.style.borderColor = '#059669'; btn.style.background = '#065f46'; btn.style.color = '#6ee7b7'; btn.textContent = 'Save'; }, 3000);
|
|
}
|
|
}
|
|
}
|
|
|
|
let selectedPromptName = null;
|
|
|
|
function getPromptsMap() {
|
|
const item = settingsData.find(s => s.key === 'ai.prompts');
|
|
if (item && item.value && typeof item.value === 'object' && !Array.isArray(item.value)) {
|
|
return item.value;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
function buildPromptEditor(s) {
|
|
return '<div class="prompt-editor">'+
|
|
'<div class="prompt-editor-layout">'+
|
|
'<div class="prompt-editor-sidebar">'+
|
|
'<div class="prompt-editor-sidebar-header">'+
|
|
'<span class="prompt-editor-sidebar-title">Prompts</span>'+
|
|
'<button class="prompt-editor-add-btn" onclick="addNewPrompt()">+ Add</button>'+
|
|
'</div>'+
|
|
'<div class="prompt-editor-sidebar-list" id="promptEditorList"></div>'+
|
|
'</div>'+
|
|
'<div class="prompt-editor-content">'+
|
|
'<span class="prompt-editor-content-label">Edit template for: <strong id="promptEditorActiveName">none</strong></span>'+
|
|
'<textarea id="promptEditorTextarea" placeholder="Select a prompt from the sidebar or add a new one..."></textarea>'+
|
|
'<div class="prompt-editor-actions">'+
|
|
'<button class="settings-btn-save" onclick="savePromptTemplate()">Save Template</button>'+
|
|
'<span style="font-size:0.75em;color:#6b7280" id="promptEditorSaved"></span>'+
|
|
'</div>'+
|
|
'<div class="prompt-editor-vars">Template variables: <code>\${name}</code> <code>\${interval}</code> <code>\${currentPlayers}</code> <code>\${toolsDocs}</code> <code>\${memoryContext}</code> <code>\${timeInfo}</code> <code>\${custom}</code></div>'+
|
|
'</div>'+
|
|
'</div>'+
|
|
'</div>';
|
|
}
|
|
|
|
function initPromptEditor(s) {
|
|
const prompts = getPromptsMap();
|
|
renderPromptList(prompts);
|
|
const names = Object.keys(prompts);
|
|
if (names.length > 0) {
|
|
selectPromptToEdit(names[0]);
|
|
}
|
|
}
|
|
|
|
function renderPromptList(prompts) {
|
|
const listEl = document.getElementById('promptEditorList');
|
|
if (!listEl) return;
|
|
const names = Object.keys(prompts);
|
|
listEl.innerHTML = names.map(name =>
|
|
'<div class="prompt-editor-prompt-item'+(name === selectedPromptName ? ' active' : '')+'" onclick="selectPromptToEdit(\\''+escHtml(name)+'\\')">'+
|
|
'<span>'+escHtml(name)+'</span>'+
|
|
'<span class="prompt-delete-x" onclick="event.stopPropagation();deletePrompt(\\''+escHtml(name)+'\\')">×</span>'+
|
|
'</div>'
|
|
).join('');
|
|
}
|
|
|
|
function selectPromptToEdit(name) {
|
|
selectedPromptName = name;
|
|
const prompts = getPromptsMap();
|
|
document.getElementById('promptEditorActiveName').textContent = name;
|
|
document.getElementById('promptEditorTextarea').value = prompts[name] || '';
|
|
document.getElementById('promptEditorSaved').textContent = '';
|
|
renderPromptList(prompts);
|
|
}
|
|
|
|
async function savePromptTemplate() {
|
|
if (!selectedPromptName) return;
|
|
const textarea = document.getElementById('promptEditorTextarea');
|
|
const template = textarea.value;
|
|
const prompts = getPromptsMap();
|
|
prompts[selectedPromptName] = template;
|
|
await saveSetting('ai.prompts', prompts);
|
|
document.getElementById('promptEditorSaved').textContent = 'Saved!';
|
|
setTimeout(() => { document.getElementById('promptEditorSaved').textContent = ''; }, 2000);
|
|
}
|
|
|
|
async function addNewPrompt() {
|
|
const name = prompt('New prompt name:');
|
|
if (!name || !name.trim()) return;
|
|
const trimmed = name.trim();
|
|
const prompts = getPromptsMap();
|
|
if (prompts[trimmed]) {
|
|
alert('Prompt "'+trimmed+'" already exists.');
|
|
return;
|
|
}
|
|
prompts[trimmed] = '';
|
|
await saveSetting('ai.prompts', prompts);
|
|
selectedPromptName = trimmed;
|
|
renderPromptList(prompts);
|
|
selectPromptToEdit(trimmed);
|
|
loadSettingsCategories();
|
|
}
|
|
|
|
async function deletePrompt(name) {
|
|
const prompts = getPromptsMap();
|
|
if (Object.keys(prompts).length <= 1) {
|
|
alert('Cannot delete the last prompt.');
|
|
return;
|
|
}
|
|
if (!confirm('Delete prompt "'+name+'"?')) return;
|
|
delete prompts[name];
|
|
await saveSetting('ai.prompts', prompts);
|
|
if (selectedPromptName === name) {
|
|
const remaining = Object.keys(prompts);
|
|
selectedPromptName = remaining.length > 0 ? remaining[0] : null;
|
|
}
|
|
if (selectedPromptName) {
|
|
selectPromptToEdit(selectedPromptName);
|
|
}
|
|
renderPromptList(prompts);
|
|
loadSettingsCategories();
|
|
}
|
|
|
|
function showToast(msg, isError) {
|
|
let toast = document.getElementById('settingsToast');
|
|
if (!toast) {
|
|
toast = document.createElement('div');
|
|
toast.id = 'settingsToast';
|
|
toast.className = 'settings-toast';
|
|
document.body.appendChild(toast);
|
|
}
|
|
toast.textContent = msg;
|
|
toast.className = 'settings-toast' + (isError ? ' error' : '');
|
|
setTimeout(() => toast.classList.add('show'), 10);
|
|
setTimeout(() => { toast.classList.remove('show'); }, 3000);
|
|
}
|
|
`,
|
|
};
|
|
|
|
module.exports = { createRouter, webUI };
|