mirror of
https://github.com/infinition/Bjorn.git
synced 2026-03-10 14:42:04 +00:00
- Implemented methods for fetching AI stats, training history, and recent experiences. - Added functionality to set operation mode (MANUAL, AUTO, AI) with appropriate handling. - Included helper methods for querying the database and sending JSON responses. - Integrated model metadata extraction for visualization purposes.
438 lines
14 KiB
JavaScript
438 lines
14 KiB
JavaScript
/**
|
|
* Actions Dropdown — ES module replacement for the monolithic global.js
|
|
* actions/dropdown logic. Builds the dropdown menu, wires hover/touch/keyboard
|
|
* behaviour, and dispatches action API calls.
|
|
*/
|
|
|
|
import { $, el, toast } from './dom.js';
|
|
import { api } from './api.js';
|
|
import { t } from './i18n.js';
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Dropdown item definitions */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
const dropdownItems = [
|
|
{ action: 'restart_bjorn_service', textKey: 'actions.menu.restartService', tipKey: 'actions.tip.restartService' },
|
|
{ action: 'remove_All_Actions', textKey: 'actions.menu.deleteActionStatus', tipKey: 'actions.tip.deleteActionStatus' },
|
|
{ action: 'clear_output_folder', textKey: 'actions.menu.clearOutput', tipKey: 'actions.tip.clearOutput' },
|
|
{ action: 'clear_logs', textKey: 'actions.menu.clearLogs', tipKey: 'actions.tip.clearLogs' },
|
|
{ action: 'reload_images', textKey: 'actions.menu.reloadImages', tipKey: 'actions.tip.reloadImages' },
|
|
{ action: 'reload_fonts', textKey: 'actions.menu.reloadFonts', tipKey: 'actions.tip.reloadFonts' },
|
|
{ action: 'reload_generate_actions_json', textKey: 'actions.menu.reloadActionsJson', tipKey: 'actions.tip.reloadActionsJson' },
|
|
{ action: 'initialize_csv', textKey: 'actions.menu.initializeCsv', tipKey: 'actions.tip.initializeCsv' },
|
|
{ action: 'clear_livestatus', textKey: 'actions.menu.clearLivestatus', tipKey: 'actions.tip.clearLivestatus' },
|
|
{ action: 'clear_actions_file', textKey: 'actions.menu.refreshActionsFile', tipKey: 'actions.tip.refreshActionsFile' },
|
|
{ action: 'clear_netkb', textKey: 'actions.menu.clearNetkb', tipKey: 'actions.tip.clearNetkb' },
|
|
{ action: 'clear_shared_config_json', textKey: 'actions.menu.clearSharedConfig', tipKey: 'actions.tip.clearSharedConfig' },
|
|
{ action: 'erase_bjorn_memories', textKey: 'actions.menu.eraseMemories', tipKey: 'actions.tip.eraseMemories' },
|
|
{ action: 'reboot_system', textKey: 'actions.menu.reboot', tipKey: 'actions.tip.reboot' },
|
|
{ action: 'shutdown_system', textKey: 'actions.menu.shutdown', tipKey: 'actions.tip.shutdown' },
|
|
];
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Action handlers — each returns a Promise */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/**
|
|
* Helper: after a successful action that recommends a service restart,
|
|
* prompt the user and fire the restart if they agree.
|
|
*/
|
|
async function offerRestart() {
|
|
if (confirm(t('actions.confirm.restartRecommended'))) {
|
|
try {
|
|
await api.post('/restart_bjorn_service');
|
|
toast(t('actions.msg.restartingService'), 3000, 'success');
|
|
} catch (err) {
|
|
toast(`${t('actions.msg.restartFailed')}: ${err.message}`, 4000, 'error');
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Map of action name -> handler function */
|
|
const actionHandlers = {
|
|
async restart_bjorn_service() {
|
|
if (!confirm(t('actions.confirm.restartService'))) return;
|
|
await api.post('/restart_bjorn_service');
|
|
toast(t('actions.msg.restartingService'), 3000, 'success');
|
|
},
|
|
|
|
async remove_All_Actions() {
|
|
if (!confirm(t('actions.confirm.deleteActionStatus'))) return;
|
|
await api.post('/delete_all_actions', { ip: '' });
|
|
toast(t('actions.msg.actionStatusDeleted'), 3000, 'success');
|
|
},
|
|
|
|
async clear_output_folder() {
|
|
if (!confirm(t('actions.confirm.clearOutput'))) return;
|
|
await api.post('/clear_output_folder');
|
|
toast(t('actions.msg.outputCleared'), 3000, 'success');
|
|
},
|
|
|
|
async clear_logs() {
|
|
if (!confirm(t('actions.confirm.clearLogs'))) return;
|
|
await api.post('/clear_logs');
|
|
toast(t('actions.msg.logsCleared'), 3000, 'success');
|
|
},
|
|
|
|
async clear_netkb() {
|
|
if (!confirm(t('actions.confirm.clearNetkb'))) return;
|
|
await api.post('/clear_netkb');
|
|
toast(t('actions.msg.netkbCleared'), 3000, 'success');
|
|
await offerRestart();
|
|
},
|
|
|
|
async clear_livestatus() {
|
|
if (!confirm(t('actions.confirm.clearLivestatus'))) return;
|
|
await api.post('/clear_livestatus');
|
|
toast(t('actions.msg.livestatusDeleted'), 3000, 'success');
|
|
await offerRestart();
|
|
},
|
|
|
|
async clear_actions_file() {
|
|
if (!confirm(t('actions.confirm.refreshActionsFile'))) return;
|
|
await api.post('/clear_actions_file');
|
|
toast(t('actions.msg.actionsFileRefreshed'), 3000, 'success');
|
|
await offerRestart();
|
|
},
|
|
|
|
async clear_shared_config_json() {
|
|
if (!confirm(t('actions.confirm.clearSharedConfig'))) return;
|
|
await api.post('/clear_shared_config_json');
|
|
toast(t('actions.msg.sharedConfigDeleted'), 3000, 'success');
|
|
await offerRestart();
|
|
},
|
|
|
|
async erase_bjorn_memories() {
|
|
if (!confirm(t('actions.confirm.eraseMemories'))) return;
|
|
await api.post('/erase_bjorn_memories');
|
|
toast(t('actions.msg.memoriesErased'), 3000, 'success');
|
|
await offerRestart();
|
|
},
|
|
|
|
async reboot_system() {
|
|
if (!confirm(t('actions.confirm.reboot'))) return;
|
|
await api.post('/reboot_system');
|
|
toast(t('actions.msg.rebooting'), 3000, 'success');
|
|
},
|
|
|
|
async shutdown_system() {
|
|
if (!confirm(t('actions.confirm.shutdown'))) return;
|
|
await api.post('/shutdown_system');
|
|
toast(t('actions.msg.shuttingDown'), 3000, 'success');
|
|
},
|
|
|
|
async initialize_csv() {
|
|
await api.post('/initialize_csv');
|
|
toast(t('actions.msg.csvInitialized'), 3000, 'success');
|
|
},
|
|
|
|
async reload_generate_actions_json() {
|
|
await api.post('/reload_generate_actions_json');
|
|
toast(t('actions.msg.actionsJsonReloaded'), 3000, 'success');
|
|
},
|
|
|
|
async reload_images() {
|
|
await api.post('/reload_images');
|
|
toast(t('actions.msg.imagesReloaded'), 3000, 'success');
|
|
},
|
|
|
|
async reload_fonts() {
|
|
await api.post('/reload_fonts');
|
|
toast(t('actions.msg.fontsReloaded'), 3000, 'success');
|
|
},
|
|
};
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Dropdown open / close helpers */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
let actionsBtn = null;
|
|
let actionsMenu = null;
|
|
let actionsWrap = null;
|
|
|
|
/** Whether the menu was explicitly toggled open via pointer/keyboard */
|
|
let sticky = false;
|
|
let hoverTimer = null;
|
|
const hoverMQ = window.matchMedia('(hover: hover) and (pointer: fine)');
|
|
|
|
function openMenu() {
|
|
if (!actionsMenu || !actionsBtn) return;
|
|
actionsMenu.style.display = 'block';
|
|
actionsMenu.hidden = false;
|
|
actionsMenu.classList.add('open');
|
|
actionsMenu.setAttribute('aria-hidden', 'false');
|
|
actionsBtn.setAttribute('aria-expanded', 'true');
|
|
placeActionsMenu();
|
|
}
|
|
|
|
function closeMenu() {
|
|
if (!actionsMenu || !actionsBtn) return;
|
|
actionsMenu.classList.remove('open');
|
|
actionsMenu.setAttribute('aria-hidden', 'true');
|
|
actionsBtn.setAttribute('aria-expanded', 'false');
|
|
actionsMenu.hidden = true;
|
|
actionsMenu.style.display = '';
|
|
sticky = false;
|
|
}
|
|
|
|
function isOpen() {
|
|
return actionsMenu && actionsMenu.classList.contains('open');
|
|
}
|
|
|
|
/**
|
|
* Position the dropdown menu beneath the topbar, horizontally centered.
|
|
*/
|
|
function placeActionsMenu() {
|
|
if (!actionsMenu || !actionsBtn) return;
|
|
|
|
const btnRect = actionsBtn.getBoundingClientRect();
|
|
const top = Math.round(btnRect.bottom + 6);
|
|
const margin = 8;
|
|
|
|
actionsMenu.style.position = 'fixed';
|
|
actionsMenu.style.top = `${top}px`;
|
|
actionsMenu.style.left = '0px';
|
|
actionsMenu.style.transform = 'none';
|
|
|
|
const menuWidth = actionsMenu.offsetWidth || 320;
|
|
const viewportWidth = window.innerWidth || document.documentElement.clientWidth || 1024;
|
|
const maxLeft = Math.max(margin, viewportWidth - menuWidth - margin);
|
|
let left = Math.round(btnRect.left + (btnRect.width - menuWidth) / 2);
|
|
left = Math.max(margin, Math.min(maxLeft, left));
|
|
|
|
actionsMenu.style.left = `${left}px`;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Build the menu items into the DOM */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
function buildMenu() {
|
|
if (!actionsMenu) return;
|
|
|
|
// Clear any existing children (idempotent rebuild)
|
|
while (actionsMenu.firstChild) actionsMenu.removeChild(actionsMenu.firstChild);
|
|
|
|
for (const item of dropdownItems) {
|
|
const btn = el('button', {
|
|
class: 'dropdown-item',
|
|
role: 'menuitem',
|
|
tabindex: '-1',
|
|
title: t(item.tipKey),
|
|
'data-action': item.action,
|
|
}, [t(item.textKey)]);
|
|
|
|
actionsMenu.appendChild(btn);
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Execute an action by name */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
async function executeAction(actionName) {
|
|
const handler = actionHandlers[actionName];
|
|
if (!handler) {
|
|
toast(`${t('actions.msg.unknownAction')}: ${actionName}`, 3000, 'error');
|
|
return;
|
|
}
|
|
try {
|
|
await handler();
|
|
} catch (err) {
|
|
toast(`${t('actions.msg.actionFailed')}: ${err.message}`, 4000, 'error');
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Keyboard navigation helpers */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
function getMenuItems() {
|
|
if (!actionsMenu) return [];
|
|
return Array.from(actionsMenu.querySelectorAll('[role="menuitem"]'));
|
|
}
|
|
|
|
function focusItem(items, index) {
|
|
if (index < 0 || index >= items.length) return;
|
|
items[index].focus();
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Event wiring */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
function wireEvents() {
|
|
if (!actionsBtn || !actionsMenu || !actionsWrap) return;
|
|
|
|
/* -- Hover behavior (desktop only) -- */
|
|
actionsWrap.addEventListener('mouseenter', () => {
|
|
if (!hoverMQ.matches) return;
|
|
if (hoverTimer) {
|
|
clearTimeout(hoverTimer);
|
|
hoverTimer = null;
|
|
}
|
|
if (!sticky) openMenu();
|
|
});
|
|
|
|
actionsWrap.addEventListener('mouseleave', () => {
|
|
if (!hoverMQ.matches) return;
|
|
if (sticky) return;
|
|
hoverTimer = setTimeout(() => {
|
|
hoverTimer = null;
|
|
if (!sticky) closeMenu();
|
|
}, 150);
|
|
});
|
|
|
|
/* -- Button toggle (desktop + mobile) -- */
|
|
let lastToggleTime = 0;
|
|
function toggleFromButton(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
// Guard against double-firing (pointerup + click both fire on mobile tap)
|
|
const now = Date.now();
|
|
if (now - lastToggleTime < 300) return;
|
|
lastToggleTime = now;
|
|
|
|
if (isOpen()) {
|
|
closeMenu();
|
|
} else {
|
|
sticky = true;
|
|
openMenu();
|
|
}
|
|
}
|
|
actionsBtn.addEventListener('click', toggleFromButton);
|
|
actionsBtn.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') toggleFromButton(e);
|
|
});
|
|
|
|
/* -- Close on pointerdown outside -- */
|
|
document.addEventListener('pointerdown', (e) => {
|
|
if (!isOpen()) return;
|
|
if (!actionsWrap.contains(e.target)) {
|
|
closeMenu();
|
|
}
|
|
});
|
|
|
|
/* -- Menu item clicks -- */
|
|
actionsMenu.addEventListener('click', (e) => {
|
|
const item = e.target.closest('[data-action]');
|
|
if (!item) return;
|
|
const actionName = item.getAttribute('data-action');
|
|
closeMenu();
|
|
executeAction(actionName);
|
|
});
|
|
|
|
/* -- Keyboard navigation -- */
|
|
actionsWrap.addEventListener('keydown', (e) => {
|
|
const items = getMenuItems();
|
|
if (!items.length) return;
|
|
|
|
const currentIndex = items.indexOf(document.activeElement);
|
|
|
|
switch (e.key) {
|
|
case 'Escape':
|
|
e.preventDefault();
|
|
closeMenu();
|
|
actionsBtn.focus();
|
|
break;
|
|
|
|
case 'ArrowDown':
|
|
e.preventDefault();
|
|
if (!isOpen()) {
|
|
openMenu();
|
|
focusItem(items, 0);
|
|
} else {
|
|
focusItem(items, currentIndex < items.length - 1 ? currentIndex + 1 : 0);
|
|
}
|
|
break;
|
|
|
|
case 'ArrowUp':
|
|
e.preventDefault();
|
|
if (!isOpen()) {
|
|
openMenu();
|
|
focusItem(items, items.length - 1);
|
|
} else {
|
|
focusItem(items, currentIndex > 0 ? currentIndex - 1 : items.length - 1);
|
|
}
|
|
break;
|
|
|
|
case 'Home':
|
|
if (isOpen()) {
|
|
e.preventDefault();
|
|
focusItem(items, 0);
|
|
}
|
|
break;
|
|
|
|
case 'End':
|
|
if (isOpen()) {
|
|
e.preventDefault();
|
|
focusItem(items, items.length - 1);
|
|
}
|
|
break;
|
|
|
|
case 'Enter':
|
|
case ' ':
|
|
if (document.activeElement && document.activeElement.hasAttribute('data-action')) {
|
|
e.preventDefault();
|
|
const actionName = document.activeElement.getAttribute('data-action');
|
|
closeMenu();
|
|
executeAction(actionName);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
});
|
|
|
|
/* -- Reposition on resize / scroll -- */
|
|
window.addEventListener('resize', () => {
|
|
if (isOpen()) placeActionsMenu();
|
|
});
|
|
|
|
window.addEventListener('scroll', () => {
|
|
if (isOpen()) placeActionsMenu();
|
|
}, { passive: true });
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape' && isOpen()) closeMenu();
|
|
});
|
|
window.addEventListener('hashchange', closeMenu);
|
|
}
|
|
|
|
function onLanguageChanged() {
|
|
buildMenu();
|
|
if (isOpen()) placeActionsMenu();
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Public init — idempotent */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
let _initialised = false;
|
|
|
|
/**
|
|
* Initialise the Actions dropdown.
|
|
* Safe to call once; subsequent calls are no-ops.
|
|
*/
|
|
export function init() {
|
|
if (_initialised) return;
|
|
|
|
actionsBtn = $('#actionsBtn');
|
|
actionsMenu = $('#actionsMenu');
|
|
actionsWrap = $('#actionsWrap');
|
|
|
|
if (!actionsBtn || !actionsMenu || !actionsWrap) {
|
|
console.warn('[actions] Required DOM elements not found; skipping init.');
|
|
return;
|
|
}
|
|
|
|
buildMenu();
|
|
wireEvents();
|
|
window.addEventListener('i18n:changed', onLanguageChanged);
|
|
|
|
_initialised = true;
|
|
console.debug('[actions] initialised');
|
|
}
|