Files
Zos/Skills/@be/be/menu/menu-manager.js

336 lines
11 KiB
JavaScript

"use strict";
/**
* Menu Manager - Handles showing menus and submenus on Jibo's screen
*
* Supports:
* - Showing static menu definitions from JSON files
* - Dynamically scanning the Skills directory for menuEntry.json files
* - Submenu navigation with automatic back button
*
* Usage:
* const mm = require('./menu/menu-manager');
* mm.showMenuFromSkills('/opt/jibo/Jibo/Skills'); // Show main menu from skills
* mm.showSubmenu('/opt/jibo/Jibo/Skills/MySubmenu'); // Show specific submenu
*/
const path = require('path');
const fs = require('fs');
const skillsScanner = require(path.join(__dirname, 'skills-scanner'));
// Track menu history for back navigation
let menuHistory = [];
let currentSkillsRoot = '/opt/jibo/Jibo/Skills';
/**
* Build a view configuration object from a menu definition
* @param {object} menuDef - Menu definition with buttons array
* @param {object} options - Additional options (isSubmenu, parentPath, etc.)
* @returns {object} View configuration for jibo.face.views.changeView
*/
function buildViewConfig(menuDef, options = {}) {
const viewConfig = {
viewConfig: {
type: 'MenuView',
id: menuDef.id || 'menu',
title: menuDef.title || null,
ignoreSwipeDown: menuDef.allowSwipe === false ? true : false,
listDefault: menuDef.listDefault || { menuButtonType: 'ActionButton', colors: 'default' },
list: [],
},
open: menuDef.open || {},
defaultClose: menuDef.defaultClose || { remove: true, transitionClose: 'trans_down' },
defaultSelect: menuDef.defaultSelect || { remove: true, transitionClose: 'trans_down' }
};
// Add back button for submenus
if (options.isSubmenu || menuDef.isSubmenu) {
viewConfig.viewConfig.list.push({
id: '__back__',
label: '← Back',
iconSrc: 'core://resources/actionIcons/back.png',
colors: 'default',
actions: [{ type: 'be:menu_back' }]
});
}
// Process each button in the menu
(menuDef.buttons || []).forEach(b => {
const item = {
id: b.id || b.label,
label: b.label || b.id,
iconSrc: b.icon || b.iconSrc || skillsScanner.DEFAULT_ICON,
colors: b.colors || b.color || menuDef.colors || 'default',
};
// Determine actions based on button type
if (b.actions || b.events) {
// Explicit actions provided
item.actions = b.actions || b.events;
} else if (b.skillId) {
// Regular skill - launch it
item.actions = [{ type: 'be:launch', data: { skillId: b.skillId } }];
} else if (b.submenu && Array.isArray(b.submenu)) {
// Submenu with embedded buttons - open inline submenu
item.actions = [{
type: 'be:open_submenu',
data: {
submenu: b.submenu,
submenuTitle: b.submenuTitle || b.label,
submenuPath: b.submenuPath
}
}];
} else if (b.submenuPath) {
// Submenu by path - open from disk
item.actions = [{
type: 'be:open_submenu_path',
data: { submenuPath: b.submenuPath }
}];
} else if (b.utterance) {
// Legacy utterance-based action
item.actions = [{ type: 'utterance', data: { utterance: b.utterance } }];
} else {
// No action defined
item.actions = [];
}
// Copy hit area polygon if defined (for custom button shapes)
if (b.hitAreaPolygon) {
item.hitAreaPolygon = b.hitAreaPolygon;
}
viewConfig.viewConfig.list.push(item);
});
return viewConfig;
}
/**
* Handle menu item selection events
* @param {object} selection - The selected menu item
* @param {function} cb - Callback function
*/
function handleMenuSelection(selection, cb) {
if (!selection || !selection.actions || !selection.actions.length) {
if (cb) cb(null, selection);
return;
}
const action = selection.actions[0];
switch (action.type) {
case 'be:menu_back':
// Go back to previous menu
goBack(cb);
break;
case 'be:open_submenu':
// Open inline submenu with embedded data
if (action.data && action.data.submenu) {
showInlineSubmenu(action.data.submenu, action.data.submenuTitle, action.data.submenuPath, cb);
}
break;
case 'be:open_submenu_path':
// Open submenu from disk path
if (action.data && action.data.submenuPath) {
exports.showSubmenu(action.data.submenuPath, cb);
}
break;
case 'be:launch':
// Launch skill - let the BE framework handle this
if (cb) cb(null, selection);
break;
default:
if (cb) cb(null, selection);
}
}
/**
* Show an inline submenu with embedded button data
* @param {Array} submenuButtons - Array of button definitions
* @param {string} title - Submenu title
* @param {string} submenuPath - Path for history tracking
* @param {function} cb - Callback
*/
function showInlineSubmenu(submenuButtons, title, submenuPath, cb) {
// Save current state to history
menuHistory.push({ type: 'main', path: currentSkillsRoot });
const submenuDef = {
id: 'submenu-' + (title || 'sub').toLowerCase().replace(/\s+/g, '-'),
title: title || 'Menu',
isSubmenu: true,
buttons: submenuButtons.map(b => ({
id: b.id,
label: b.label,
icon: b.icon || b.iconSrc,
colors: b.colors || b.color,
skillId: b.skillId,
description: b.description
}))
};
const viewConfig = buildViewConfig(submenuDef, { isSubmenu: true });
if (typeof jibo === 'undefined' || !jibo.face || !jibo.face.views) {
if (cb) cb(new Error('jibo not initialized'));
return;
}
jibo.face.views.changeView(viewConfig, function(err, result) {
if (err) {
if (cb) cb(err);
return;
}
// Set up selection handler for submenu
if (result && result.on) {
result.on('select', function(selection) {
handleMenuSelection(selection, cb);
});
}
if (cb) cb(null, result);
});
}
/**
* Go back to the previous menu in history
* @param {function} cb - Callback
*/
function goBack(cb) {
const previous = menuHistory.pop();
if (!previous) {
// No history, show main menu
exports.showMenuFromSkills(currentSkillsRoot, cb);
return;
}
if (previous.type === 'main') {
exports.showMenuFromSkills(previous.path || currentSkillsRoot, cb);
} else if (previous.type === 'submenu' && previous.path) {
exports.showSubmenu(previous.path, cb);
} else {
exports.showMenuFromSkills(currentSkillsRoot, cb);
}
}
/**
* Show a menu from a static JSON file
* @param {string} menuPath - Relative path to menu JSON file
* @param {function} cb - Callback
*/
exports.showMenu = function (menuPath, cb) {
try {
const menuDef = require(path.join(__dirname, '..', menuPath));
const viewConfig = buildViewConfig(menuDef);
if (typeof jibo === 'undefined' || !jibo.face || !jibo.face.views) {
throw new Error('jibo not initialized');
}
jibo.face.views.changeView(viewConfig, cb || function(){});
}
catch (e) {
console.warn('menu-manager.showMenu error', e);
if (cb) cb(e);
}
};
/**
* Show the main menu by scanning the Skills directory for menuEntry.json files
* @param {string} skillsRootPath - Path to Skills directory
* @param {function} cb - Callback
*/
exports.showMenuFromSkills = function (skillsRootPath, cb) {
try {
const root = skillsRootPath || '/opt/jibo/Jibo/Skills';
currentSkillsRoot = root;
menuHistory = []; // Reset history when showing main menu
const menuDef = skillsScanner.scanSkills(root);
const viewConfig = buildViewConfig(menuDef);
if (typeof jibo === 'undefined' || !jibo.face || !jibo.face.views) {
throw new Error('jibo not initialized');
}
jibo.face.views.changeView(viewConfig, function(err, result) {
if (err) {
console.warn('menu-manager.showMenuFromSkills error', err);
if (cb) cb(err);
return;
}
// Set up selection handler
if (result && result.on) {
result.on('select', function(selection) {
handleMenuSelection(selection, cb);
});
}
if (cb) cb(null, result);
});
}
catch (e) {
console.warn('menu-manager.showMenuFromSkills error', e);
if (cb) cb(e);
}
};
/**
* Show a submenu by scanning a specific directory
* @param {string} submenuPath - Full path to the submenu directory
* @param {function} cb - Callback
*/
exports.showSubmenu = function (submenuPath, cb) {
try {
// Save current state to history
if (menuHistory.length === 0) {
menuHistory.push({ type: 'main', path: currentSkillsRoot });
}
const menuDef = skillsScanner.getSubmenu(submenuPath);
const viewConfig = buildViewConfig(menuDef, { isSubmenu: true });
if (typeof jibo === 'undefined' || !jibo.face || !jibo.face.views) {
throw new Error('jibo not initialized');
}
jibo.face.views.changeView(viewConfig, function(err, result) {
if (err) {
console.warn('menu-manager.showSubmenu error', err);
if (cb) cb(err);
return;
}
// Set up selection handler
if (result && result.on) {
result.on('select', function(selection) {
handleMenuSelection(selection, cb);
});
}
if (cb) cb(null, result);
});
}
catch (e) {
console.warn('menu-manager.showSubmenu error', e);
if (cb) cb(e);
}
};
/**
* Clear menu history (useful when closing menu completely)
*/
exports.clearHistory = function() {
menuHistory = [];
};
/**
* Get current menu history (for debugging)
*/
exports.getHistory = function() {
return menuHistory.slice();
};
/**
* Get the skills scanner module for direct access
*/
exports.skillsScanner = skillsScanner;