PHP Classes

File: templates/admin/ai_bots.twig

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   templates/admin/ai_bots.twig   Download  
File: templates/admin/ai_bots.twig
Role: Auxiliary data
Content type: text/plain
Description: Auxiliary data
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 24,049 bytes
 

Contents

Class file image Download
{% extends "base.twig" %} {% block title %}{{ t('ui.admin.ai_bots.page_title', {}, 'common') }}{% endblock %} {% block content %} <div class="container-fluid"> <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"> <h1 class="h2">{{ t('ui.admin.ai_bots.heading', {}, 'common') }}</h1> <button type="button" class="btn btn-primary" onclick="showCreateBotModal()"> <i class="fas fa-plus me-2"></i>{{ t('ui.admin.ai_bots.add_bot', {}, 'common') }} </button> </div> <div class="card shadow"> <div class="card-body"> <div class="table-responsive"> <table class="table table-hover" id="aiBotsTable"> <thead class="table-dark"> <tr> <th>{{ t('ui.common.name', {}, 'common') }}</th> <th>{{ t('ui.admin.ai_bots.username', {}, 'common') }}</th> <th>{{ t('ui.common.description', {}, 'common') }}</th> <th>{{ t('ui.admin.ai_bots.weekly_spend', {}, 'common') }}</th> <th>{{ t('ui.admin.ai_bots.status', {}, 'common') }}</th> <th>{{ t('ui.admin.ai_bots.actions', {}, 'common') }}</th> </tr> </thead> <tbody id="aiBotsTableBody"> <!-- Populated by JavaScript --> </tbody> </table> </div> </div> </div> </div> <!-- Create / Edit modal --> <div class="modal fade" id="aiBotModal" tabindex="-1" aria-labelledby="aiBotModalLabel" aria-hidden="true"> <div class="modal-dialog modal-lg"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title" id="aiBotModalLabel">{{ t('ui.admin.ai_bots.add_bot_title', {}, 'common') }}</h5> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ t('ui.common.close', {}, 'common') }}"></button> </div> <div class="modal-body"> <form id="aiBotForm"> <input type="hidden" id="aiBotId" name="aiBotId"> <div class="row g-3 mb-3"> <div class="col-md-6"> <label for="aiBotName" class="form-label">{{ t('ui.common.name', {}, 'common') }} <span class="text-danger">*</span></label> <input type="text" class="form-control" id="aiBotName" name="name" maxlength="100" required> </div> <div class="col-md-6"> <label for="aiBotUsername" class="form-label">{{ t('ui.admin.ai_bots.username', {}, 'common') }} <span class="text-danger">*</span></label> <input type="text" class="form-control" id="aiBotUsername" name="username" maxlength="50" pattern="[A-Za-z0-9_]+" title="Alphanumeric and underscore only" required> <div class="form-text" id="aiBotUsernameNote">{{ t('ui.admin.ai_bots.username_note', {}, 'common') }}</div> </div> </div> <div class="mb-3"> <label for="aiBotDescription" class="form-label">{{ t('ui.common.description', {}, 'common') }}</label> <input type="text" class="form-control" id="aiBotDescription" name="description"> </div> <div class="mb-3"> <label for="aiBotSystemPrompt" class="form-label">{{ t('ui.admin.ai_bots.system_prompt', {}, 'common') }} <span class="text-danger">*</span></label> <textarea class="form-control font-monospace" id="aiBotSystemPrompt" name="system_prompt" rows="6" required></textarea> <div class="form-text">{{ t('ui.admin.ai_bots.system_prompt_note', {}, 'common') }}</div> </div> <div class="row g-3 mb-3"> <div class="col-md-4"> <label for="aiBotProvider" class="form-label">{{ t('ui.admin.ai_bots.provider', {}, 'common') }}</label> <select class="form-select" id="aiBotProvider" name="provider"> <option value="">{{ t('ui.admin.ai_bots.provider_default', {}, 'common') }}</option> <option value="openai">OpenAI</option> <option value="anthropic">Anthropic</option> </select> </div> <div class="col-md-4"> <label for="aiBotModel" class="form-label">{{ t('ui.admin.ai_bots.model', {}, 'common') }}</label> <input type="text" class="form-control" id="aiBotModel" name="model" placeholder="{{ t('ui.admin.ai_bots.model_placeholder', {}, 'common') }}"> </div> <div class="col-md-2"> <label for="aiBotBudget" class="form-label">{{ t('ui.admin.ai_bots.weekly_budget', {}, 'common') }} ($)</label> <input type="number" class="form-control" id="aiBotBudget" name="weekly_budget_usd" min="0" step="0.01" value="1.00" required> </div> <div class="col-md-2"> <label for="aiBotContextMessages" class="form-label">{{ t('ui.admin.ai_bots.context_messages', {}, 'common') }}</label> <input type="number" class="form-control" id="aiBotContextMessages" name="context_messages" min="1" max="50" value="10" required> </div> </div> <div class="mb-3"> <div class="form-check form-switch"> <input class="form-check-input" type="checkbox" id="aiBotActive" name="is_active" checked> <label class="form-check-label" for="aiBotActive">{{ t('ui.admin.ai_bots.active', {}, 'common') }}</label> </div> </div> <hr> <h6>{{ t('ui.admin.ai_bots.activities', {}, 'common') }}</h6> <div class="mb-3"> <div class="form-check form-switch mb-2"> <input class="form-check-input" type="checkbox" id="actLocalChatEnabled" checked> <label class="form-check-label fw-semibold" for="actLocalChatEnabled">{{ t('ui.admin.ai_bots.activity_local_chat', {}, 'common') }}</label> </div> <div class="ms-4 border-start ps-3" id="actLocalChatConfig"> <div class="row g-2 mb-3"> <div class="col-auto"> <div class="form-check"> <input class="form-check-input" type="checkbox" id="actChatRespondDm" checked> <label class="form-check-label" for="actChatRespondDm">{{ t('ui.admin.ai_bots.chat_respond_dm', {}, 'common') }}</label> </div> </div> <div class="col-auto"> <div class="form-check"> <input class="form-check-input" type="checkbox" id="actChatRespondRooms" checked> <label class="form-check-label" for="actChatRespondRooms">{{ t('ui.admin.ai_bots.chat_respond_rooms', {}, 'common') }}</label> </div> </div> </div> <div class="d-flex justify-content-between align-items-center mb-2"> <span class="fw-semibold small">{{ t('ui.admin.ai_bots.middleware_pipeline', {}, 'common') }}</span> <button type="button" class="btn btn-sm btn-outline-secondary" onclick="addMiddlewareEntry()"> <i class="fas fa-plus me-1"></i>{{ t('ui.admin.ai_bots.middleware_add', {}, 'common') }} </button> </div> <div id="middlewareList" class="mb-2"></div> <div class="form-text">{{ t('ui.admin.ai_bots.middleware_pipeline_note', {}, 'common') }}</div> </div> </div> </form> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t('ui.common.cancel', {}, 'common') }}</button> <button type="button" class="btn btn-primary" onclick="saveBot()">{{ t('ui.admin.ai_bots.save_bot', {}, 'common') }}</button> </div> </div> </div> </div> <script> function uiT(key, fallback, params = {}) { return window.t ? window.t(key, params, fallback) : fallback; } function apiErr(payload, fallback) { return window.getApiErrorMessage ? window.getApiErrorMessage(payload, fallback) : (payload?.error || fallback); } // Preserved from the loaded bot so we don't clobber fields not exposed in the UI. let _chatAllowedRoomIds = []; let _chatBlockedUserIds = []; // Config hints shown as textarea placeholder for each built-in class. const MIDDLEWARE_HINTS = { SlashCommandMiddleware: '{"commands":{"/help":"Help text here","/ping":"Pong!"}}', FilePromptInjectorMiddleware: '{"path":"config/bots/mybot/context.md","position":"append"}', UrlPromptInjectorMiddleware: '{"url":"https://example.com/context.txt","ttl":3600,"max_bytes":4000}', RegexFilterMiddleware: '{"pattern":"/^!nobot/i","action":"abort"}', RagPromptInjectorMiddleware: '{"db_path":"tools/support-bot/binkterm_knowledge.db","script_path":"tools/support-bot/query_retrieve.py","top_k":4}', }; document.addEventListener('DOMContentLoaded', function() { loadI18nNamespaces(['common', 'errors']).then(() => loadBots()); // Auto-suggest username from name on create document.getElementById('aiBotName').addEventListener('input', function() { const usernameField = document.getElementById('aiBotUsername'); if (!document.getElementById('aiBotId').value) { usernameField.value = this.value.replace(/[^A-Za-z0-9_]/g, '_').toLowerCase(); } }); }); function showAlert(message, type) { const alertDiv = document.createElement('div'); alertDiv.className = `alert alert-${type} alert-dismissible fade show`; alertDiv.innerHTML = `${message}<button type="button" class="btn-close" data-bs-dismiss="alert"></button>`; const container = document.querySelector('.container-fluid'); container.insertBefore(alertDiv, container.firstChild); setTimeout(() => alertDiv.remove(), 6000); } function spendBadge(spend, budget) { const pct = budget > 0 ? (spend / budget) * 100 : 0; const color = pct >= 90 ? 'danger' : (pct >= 60 ? 'warning' : 'success'); return `<span class="badge bg-${color}">$${spend.toFixed(4)} / $${budget.toFixed(2)}</span>`; } function loadBots() { fetch('/admin/api/ai-bots') .then(r => r.json()) .then(data => displayBots(data.bots || [])) .catch(() => showAlert(uiT('ui.admin.ai_bots.load_failed', 'Error loading bots'), 'danger')); } function displayBots(bots) { const tbody = document.getElementById('aiBotsTableBody'); tbody.innerHTML = ''; if (bots.length === 0) { tbody.innerHTML = `<tr><td colspan="6" class="text-center text-muted">${uiT('ui.admin.ai_bots.no_bots', 'No bots configured.')}</td></tr>`; return; } bots.forEach(bot => { const row = document.createElement('tr'); row.innerHTML = ` <td><strong>${escHtml(bot.name)}</strong></td> <td><code>@${escHtml(bot.username)}</code></td> <td>${escHtml(bot.description || '')}</td> <td>${spendBadge(bot.weekly_spend_usd, bot.weekly_budget_usd)}</td> <td><span class="badge bg-${bot.is_active ? 'success' : 'secondary'}"> ${bot.is_active ? uiT('ui.admin.ai_bots.active', 'Active') : uiT('ui.admin.ai_bots.inactive', 'Inactive')} </span></td> <td> <div class="btn-group btn-group-sm"> <button class="btn btn-outline-primary" onclick="editBot(${bot.id})" title="${uiT('ui.common.edit','Edit')}"> <i class="fas fa-edit"></i> </button> <button class="btn btn-outline-danger" onclick="deleteBot(${bot.id})" title="${uiT('ui.common.delete','Delete')}"> <i class="fas fa-trash"></i> </button> </div> </td> `; tbody.appendChild(row); }); } function escHtml(str) { const d = document.createElement('div'); d.textContent = str; return d.innerHTML; } // ?? Middleware editor ???????????????????????????????????????????????????????? function addMiddlewareEntry(classVal, configJson) { const list = document.getElementById('middlewareList'); const idx = list.children.length; const entry = document.createElement('div'); entry.className = 'middleware-entry card mb-2'; entry.innerHTML = ` <div class="card-body p-2"> <div class="d-flex gap-2 align-items-start"> <div class="flex-grow-1"> <label class="form-label small mb-1">${uiT('ui.admin.ai_bots.middleware_class', 'Class')}</label> <select class="form-select form-select-sm mb-2 mw-class-select" onchange="onMiddlewareClassChange(this)"> <option value="SlashCommandMiddleware">SlashCommandMiddleware</option> <option value="FilePromptInjectorMiddleware">FilePromptInjectorMiddleware</option> <option value="UrlPromptInjectorMiddleware">UrlPromptInjectorMiddleware</option> <option value="RegexFilterMiddleware">RegexFilterMiddleware</option> <option value="RagPromptInjectorMiddleware">RagPromptInjectorMiddleware</option> <option value="_custom">${uiT('ui.admin.ai_bots.middleware_custom_class', 'Custom...')}</option> </select> <input type="text" class="form-control form-control-sm mb-2 mw-custom-class d-none" placeholder="${uiT('ui.admin.ai_bots.middleware_custom_class', 'Custom class (fully qualified)')}"> <label class="form-label small mb-1">${uiT('ui.admin.ai_bots.middleware_config', 'Config (JSON)')}</label> <textarea class="form-control form-control-sm font-monospace mw-config" rows="2"></textarea> </div> <button type="button" class="btn btn-sm btn-outline-danger mt-4 flex-shrink-0" onclick="this.closest('.middleware-entry').remove()" title="Remove"> <i class="fas fa-times"></i> </button> </div> </div>`; list.appendChild(entry); const sel = entry.querySelector('.mw-class-select'); const cfg = entry.querySelector('.mw-config'); if (classVal && classVal !== '_custom' && MIDDLEWARE_HINTS[classVal] !== undefined) { sel.value = classVal; } else if (classVal) { sel.value = '_custom'; entry.querySelector('.mw-custom-class').classList.remove('d-none'); entry.querySelector('.mw-custom-class').value = classVal; } cfg.placeholder = MIDDLEWARE_HINTS[sel.value] || '{}'; if (configJson !== undefined && configJson !== null) { cfg.value = typeof configJson === 'string' ? configJson : JSON.stringify(configJson, null, 2); } } function onMiddlewareClassChange(sel) { const entry = sel.closest('.middleware-entry'); const custom = entry.querySelector('.mw-custom-class'); const cfg = entry.querySelector('.mw-config'); if (sel.value === '_custom') { custom.classList.remove('d-none'); cfg.placeholder = '{}'; } else { custom.classList.add('d-none'); cfg.placeholder = MIDDLEWARE_HINTS[sel.value] || '{}'; } } function populateMiddleware(middlewareArray) { document.getElementById('middlewareList').innerHTML = ''; (middlewareArray || []).forEach(entry => { const cfg = entry.config !== undefined ? entry.config : null; addMiddlewareEntry(entry.class || '', cfg); }); } function collectMiddleware() { const entries = []; document.querySelectorAll('#middlewareList .middleware-entry').forEach(entry => { const sel = entry.querySelector('.mw-class-select'); const custom = entry.querySelector('.mw-custom-class'); const cfgRaw = entry.querySelector('.mw-config').value.trim(); const className = sel.value === '_custom' ? custom.value.trim() : sel.value; if (!className) return; let config = {}; if (cfgRaw) { try { config = JSON.parse(cfgRaw); } catch { config = {}; } } entries.push({ class: className, config }); }); return entries; } // ?? Modal helpers ???????????????????????????????????????????????????????????? function showCreateBotModal() { document.getElementById('aiBotModalLabel').textContent = uiT('ui.admin.ai_bots.add_bot_title', 'Add AI Bot'); document.getElementById('aiBotForm').reset(); document.getElementById('aiBotId').value = ''; document.getElementById('aiBotUsername').readOnly = false; document.getElementById('aiBotUsernameNote').style.display = ''; document.getElementById('aiBotActive').checked = true; document.getElementById('actLocalChatEnabled').checked = true; document.getElementById('actChatRespondDm').checked = true; document.getElementById('actChatRespondRooms').checked = true; _chatAllowedRoomIds = []; _chatBlockedUserIds = []; populateMiddleware([]); new bootstrap.Modal(document.getElementById('aiBotModal')).show(); } function editBot(botId) { fetch(`/admin/api/ai-bots/${botId}`) .then(r => r.json()) .then(bot => { document.getElementById('aiBotModalLabel').textContent = uiT('ui.admin.ai_bots.edit_bot_title', 'Edit AI Bot'); document.getElementById('aiBotId').value = bot.id; document.getElementById('aiBotName').value = bot.name; document.getElementById('aiBotUsername').value = bot.username || ''; document.getElementById('aiBotUsername').readOnly = true; document.getElementById('aiBotUsernameNote').style.display = 'none'; document.getElementById('aiBotDescription').value = bot.description || ''; document.getElementById('aiBotSystemPrompt').value = bot.system_prompt || ''; document.getElementById('aiBotProvider').value = bot.provider || ''; document.getElementById('aiBotModel').value = bot.model || ''; document.getElementById('aiBotBudget').value = bot.weekly_budget_usd ?? 1.00; document.getElementById('aiBotContextMessages').value = bot.context_messages ?? 10; document.getElementById('aiBotActive').checked = !!bot.is_active; const chatAct = bot.activities?.local_chat || {}; const chatCfg = chatAct.config_json || {}; document.getElementById('actLocalChatEnabled').checked = chatAct.is_enabled !== false; document.getElementById('actChatRespondDm').checked = chatCfg.respond_in_dm !== false; document.getElementById('actChatRespondRooms').checked = chatCfg.respond_in_rooms !== false; // Preserve fields not shown in the UI so they survive a save. _chatAllowedRoomIds = chatCfg.allowed_room_ids || []; _chatBlockedUserIds = chatCfg.blocked_user_ids || []; populateMiddleware(chatCfg.middleware || []); new bootstrap.Modal(document.getElementById('aiBotModal')).show(); }) .catch(() => showAlert(uiT('ui.admin.ai_bots.load_single_failed', 'Error loading bot'), 'danger')); } function saveBot() { const botId = document.getElementById('aiBotId').value; const payload = { name: document.getElementById('aiBotName').value, username: document.getElementById('aiBotUsername').value, description: document.getElementById('aiBotDescription').value, system_prompt: document.getElementById('aiBotSystemPrompt').value, provider: document.getElementById('aiBotProvider').value || null, model: document.getElementById('aiBotModel').value || null, weekly_budget_usd: parseFloat(document.getElementById('aiBotBudget').value), context_messages: parseInt(document.getElementById('aiBotContextMessages').value, 10), is_active: document.getElementById('aiBotActive').checked, activities: { local_chat: { is_enabled: document.getElementById('actLocalChatEnabled').checked, config_json: { respond_in_dm: document.getElementById('actChatRespondDm').checked, respond_in_rooms: document.getElementById('actChatRespondRooms').checked, allowed_room_ids: _chatAllowedRoomIds, blocked_user_ids: _chatBlockedUserIds, middleware: collectMiddleware(), } } } }; const url = botId ? `/admin/api/ai-bots/${botId}` : '/admin/api/ai-bots'; const method = botId ? 'PUT' : 'POST'; fetch(url, { method, headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) }) .then(r => r.json()) .then(data => { if (data.success || data.id) { const msg = botId ? uiT('ui.admin.ai_bots.updated_success', 'Bot updated successfully') : uiT('ui.admin.ai_bots.created_success', 'Bot created successfully'); showAlert(msg, 'success'); bootstrap.Modal.getInstance(document.getElementById('aiBotModal')).hide(); loadBots(); } else { showAlert(apiErr(data, uiT('ui.admin.ai_bots.save_failed', 'Error saving bot')), 'danger'); } }) .catch(() => showAlert(uiT('ui.admin.ai_bots.save_failed', 'Error saving bot'), 'danger')); } function deleteBot(botId) { if (!confirm(uiT('ui.admin.ai_bots.delete_confirm', 'Delete this bot and its system user? This cannot be undone.'))) return; fetch(`/admin/api/ai-bots/${botId}`, { method: 'DELETE' }) .then(r => r.json()) .then(data => { if (data.success) { showAlert(uiT('ui.admin.ai_bots.deleted_success', 'Bot deleted successfully'), 'success'); loadBots(); } else { showAlert(apiErr(data, uiT('ui.admin.ai_bots.delete_failed', 'Error deleting bot')), 'danger'); } }) .catch(() => showAlert(uiT('ui.admin.ai_bots.delete_failed', 'Error deleting bot'), 'danger')); } </script> {% endblock %}