/* * GNU AGPL-3.0 License * * Copyright (c) 2021 - present core.ai . All rights reserved. * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License * for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see https://opensource.org/licenses/AGPL-3.0. * */ // @INCLUDE_IN_API_DOCS /** * TaskManager module deals with managing long running tasks in phcode. It handles the `Tasks` dropdown in the status * bar where the user can see all running tasks, monitor its progress and close/pause the execution of the task if * supported by the task. * @module features/TaskManager */ define(function (require, exports, module) { const Strings = require("strings"), EventDispatcher = require("utils/EventDispatcher"), StringUtils = require("utils/StringUtils"); const taskList = {}; const STATUS_SUCCESS = "success", STATUS_FAIL ="fail", STATUS_INCOMPLETE = "incomplete"; const SPINNER_FAIL = "spinner-failure", SPINNER_SUCCESS = "spinner-success", SPINNER_NORMAL = "spinner-normal"; const SPINNER_HIDE_TIME = Phoenix.isTestWindow? 5 : 3000; // for tests , we wait only 1 ms let currentSpinnerType = null, spinnerHideTimer; let $spinner; /** * This is used by legacy extensions that used StatusBar.showBusyIndicator and hide apis that are deprecated. * here for legacy support. * @private * @type {boolean} */ let legacyExtensionBusy = false; let taskSelect; function _showSpinnerIcon(spinnerType) { // we only show the icon for a few seconds to be less distracting except if the persist option is specified // the persist option is now used only for errors and success tasks that has not been removed from the task // manager list(which usually happens if there is some user action needed). Even then on click the spinner will // be hidden again. if(spinnerType === SPINNER_FAIL) { clearTimeout(spinnerHideTimer); $spinner.removeClass("forced-hidden"); $spinner.removeClass(SPINNER_SUCCESS); $spinner.addClass(SPINNER_FAIL); currentSpinnerType = SPINNER_FAIL; return; } if(spinnerType === SPINNER_SUCCESS) { clearTimeout(spinnerHideTimer); $spinner.removeClass("forced-hidden"); $spinner.removeClass(SPINNER_FAIL); $spinner.addClass(SPINNER_SUCCESS); currentSpinnerType = SPINNER_SUCCESS; return; } if(spinnerType === SPINNER_NORMAL) { clearTimeout(spinnerHideTimer); $spinner.removeClass("forced-hidden"); $spinner.removeClass(SPINNER_FAIL); $spinner.removeClass(SPINNER_SUCCESS); spinnerHideTimer = setTimeout(hideSpinnerIcon, SPINNER_HIDE_TIME); currentSpinnerType = SPINNER_NORMAL; } } function hideSpinnerIcon() { clearTimeout(spinnerHideTimer); currentSpinnerType = null; $spinner.addClass("forced-hidden"); $spinner.removeClass(SPINNER_FAIL); $spinner.removeClass(SPINNER_SUCCESS); } /** * determines what the spinner icon to show(green-for success), red-fail, blue normal based on the active * tasks in list and renders. IF the active tasks has already been notified, it wont notify again. * @private */ function renderSpinnerIcon(showNormalSpinnerIfNone) { let unackSuccessTaskFound = false; if(currentSpinnerType && currentSpinnerType !== SPINNER_NORMAL) { // there is a success/fail spinner visible, clean it. For the normal spinner, it will be // auto-cleaned by timer. hideSpinnerIcon(); } for(let task of Object.values(taskList)){ if(!task._spinnerIconAck && task.isFailed()){ _showSpinnerIcon(SPINNER_FAIL); return; } if(!task._spinnerIconAck && task.isSucceeded()){ unackSuccessTaskFound = true; } } if(unackSuccessTaskFound) { _showSpinnerIcon(SPINNER_SUCCESS); return; } // for normal spinner, we dont show anything as its only shown briefly till SPINNER_HIDE_TIME // which was already handled, except when newTaskAdded if(showNormalSpinnerIfNone) { _showSpinnerIcon(SPINNER_NORMAL); } } function _onDropdownShown() { // the animating icon is a call to action that stops showing after a few seconds normally. On clicking the // task dropdown, the user has checked the notifications and we can hide the distracting spinner. for(let task of Object.values(taskList)){ task._spinnerIconAck = true; } hideSpinnerIcon(); } function _setTaskSelect(select) { taskSelect = select; $spinner = $("#status-tasks .spinner"); if(Phoenix.isTestWindow) { exports.taskSelect = taskSelect; exports.SPINNER_HIDE_TIME = SPINNER_HIDE_TIME; } hideSpinnerIcon(); } function _renderItem(item, index) { if(item === Strings.STATUSBAR_TASKS_UNKNOWN_EXTENSION_TASK){ return Strings.STATUSBAR_TASKS_UNKNOWN_EXTENSION_TASK; } if(!taskList[item]){ // This should never happen console.error("Cannot render task item", item); return "unknown"; } const task = taskList[item]; task._$html = $(`
${task._title||task._id}
${task._message||""}
`); const eventListeners= { ".pause-icon": "onPauseClick", ".play-icon": "onPlayClick", ".retry-icon": "onRetryClick", ".close-icon": "onStopClick" }; for(let iconClass of Object.keys(eventListeners)){ task._$html.find(iconClass).click((evt)=>{ console.log(iconClass); const cbFn = eventListeners[iconClass]; return task[cbFn] && task[cbFn](evt); }); } _reRenderComponents(task); return {$html: task._$html}; } function _onSelect(el, selection) { if(selection===Strings.STATUSBAR_TASKS_UNKNOWN_EXTENSION_TASK){ return; } if(!taskList[selection]){ // This should never happen console.error("Cannot select task item", selection); return; } const task = taskList[selection]; return task.onSelect && task.onSelect(el); } function _getDefaultTaskIDs() { if(legacyExtensionBusy){ return [Strings.STATUSBAR_TASKS_UNKNOWN_EXTENSION_TASK, Strings.STATUSBAR_TASKS_HIDE_SPINNER]; } return [Strings.STATUSBAR_TASKS_HIDE_SPINNER]; } function _showOrHideStatusBarIfNeeded() { const taskArray = Object.keys(taskList); if(taskArray.length) { taskArray.push("---"); } taskSelect.items = [...taskArray, ..._getDefaultTaskIDs()]; taskSelect.refresh(); if(Object.keys(taskList).length || legacyExtensionBusy){ $("#status-tasks").removeClass('forced-hidden'); } else { $("#status-tasks").addClass('forced-hidden'); taskSelect.closeDropdown(); } } function _renderProgressbar(task) { const $html = task._$html; if(!task._$html){ return; } $html.find(".progress") .removeClass("progress-bar-foreground") .removeClass("progress-bar-foreground-pulse") .removeClass("progress-bar-foreground-failure") .removeClass("progress-bar-foreground-success"); if(task._completedStatus === STATUS_SUCCESS){ $html.find(".progress") .addClass("progress-bar-foreground-success") .css('width', `100%`); return; } if(task._completedStatus === STATUS_FAIL){ $html.find(".progress") .addClass("progress-bar-foreground-failure") .css('width', `100%`); return; } if(task._percent){ $html.find(".progress") .addClass("progress-bar-foreground") .css('width', `${task._percent}%`); } else { $html.find(".progress") .addClass("progress-bar-foreground-pulse") .css('width', `100%`); } } function _renderPlayIcons(task) { const $html = task._$html; if(!task._$html){ return; } const iconMap = { ".close-icon": "_showStopIcon", ".pause-icon": "_showPauseIcon", ".play-icon": "_showPlayIcon", ".retry-icon": "_showRestartIcon" }; for(let iconClass of Object.keys(iconMap)){ const showIconMessage = iconMap[iconClass]; if(task[showIconMessage]){ $html.find(iconClass) .removeClass('forced-hidden') .attr("title", task[showIconMessage]); } else { $html.find(iconClass) .addClass('forced-hidden'); } } } function _reRenderComponents(task) { const $html = task._$html; if(!task._$html){ return; } $html.find(".task-title").text(task._title||task._id); $html.find(".task-message").text(task._message||""); if(task._iconHTML) { $html.find(".task-icon").html(task._iconHTML); } _renderProgressbar(task); _renderPlayIcons(task); } /** * @typedef {Object} TaskObject * Methods for managing the task's state and UI representation in the TaskManager. * * @property {function(): void} show - Shows the task popup in the ui. * @property {function(): void} close - Closes the task and removes it from the UI. * @property {function(string): void} setTitle - Sets the task's title. * @property {function(): string} getTitle - Returns the task's title. * @property {function(string): void} setMessage - Sets the task's message. * @property {function(): string} getMessage - Returns the task's message. * @property {function(number): void} setProgressPercent - Sets the task's progress percentage. * @property {function(): number} getProgressPercent - Returns the task's current progress percentage. * @property {function(): void} setFailed - Marks the task as failed. * @property {function(): boolean} isFailed - Returns true if the task is marked as failed. * @property {function(): void} setSucceded - Marks the task as succeeded. * @property {function(): boolean} isSucceded - Returns true if the task is marked as succeeded. * @property {function(string): void} showStopIcon - Shows the stop icon with an optional tooltip message. * @property {function(): void} hideStopIcon - Hides the stop icon. * @property {function(string): void} showPlayIcon - Shows the play icon with an optional tooltip message. * @property {function(): void} hidePlayIcon - Hides the play icon. * @property {function(string): void} showPauseIcon - Shows the pause icon with an optional tooltip message. * @property {function(): void} hidePauseIcon - Hides the pause icon. * @property {function(string): void} showRestartIcon - Shows the restart (retry) icon with an optional tooltip message. * @property {function(): void} hideRestartIcon - Hides the restart (retry) icon. * @property {function(): void} flashSpinnerForAttention - briefly flashes the task spinner icon for attention. */ /** * The addNewTask is designed for adding new tasks to the task management system. This function is central to * managing long-running tasks, providing a way to visually represent task progress, status, and control actions * directly from the UI in the status bar. * * @param {string} taskTitle - The title of the task. This is a mandatory parameter and is displayed in the UI. * @param {string} message - A message or status associated with the task. Displayed as additional information in the UI. * @param {string} [iconHTML] - Optional HTML string for the task's icon. Used to visually represent the task in the UI. * @param {Object} [options] - Optional settings and callbacks for the task. * @param {Function} [options.onPauseClick] - Callback function triggered when the pause button is clicked. * @param {Function} [options.onPlayClick] - Callback function triggered when the play button is clicked. * @param {Function} [options.onStopClick] - Callback function triggered when the stop button is clicked. * @param {Function} [options.onRetryClick] - Callback function triggered when the retry button is clicked. * @param {Function} [options.onSelect] - Callback function triggered when the task is selected from the dropdown. * @param {number} [options.progressPercent] - Initial progress percentage of the task. * @param {boolean} [options.noSpinnerNotification] - If set to true, will not show the task spinners for this task. * This can be used for silent background tasks where user attention is not needed. * @returns {TaskObject} Returns a task object with methods for updating the task's state and UI representation, * such as `setProgressPercent`, `setMessage`, `setSucceeded`, `setFailed`, and control visibility methods * like `showStopIcon`, `hideStopIcon`, etc. * * @example * // Example: Adding a new task with initial progress and attaching event handlers * const task = TaskManager.addNewTask( * 'Data Processing', * 'Processing data...', * '', * { * onPauseClick: () => console.log('Task paused'), * onPlayClick: () => console.log('Task resumed'), * onStopClick: () => console.log('Task stopped'), * onRetryClick: () => console.log('Task retried'), * onSelect: () => console.log('Task selected'), * progressPercent: 20 * } * ); * * // Updating task progress * task.setProgressPercent(60); * * // Updating task message * task.setMessage('60% completed'); * * // Marking task as succeeded * task.setSucceeded(); */ function addNewTask(taskTitle, message, iconHTML=null, options = { onPauseClick: null, onPlayClick: null, onStopClick: null, onRetryClick: null, onSelect: null, progressPercent: null, noSpinnerNotification: false }) { if(!taskTitle){ throw new Error("taskTitle is required to call addNewTask"); } const task = { _id: `${taskTitle}-${StringUtils.randomString(10)}`, _title: taskTitle, _message: message, _showPauseIcon: null, _showPlayIcon: null, _showStopIcon: null, _showRestartIcon: null, onPauseClick: options && options.onPauseClick, onPlayClick: options && options.onPlayClick, onStopClick: options && options.onStopClick, onRetryClick: options && options.onRetryClick, onSelect: options && options.onSelect, _percent: options && options.progressPercent, _completedStatus: STATUS_INCOMPLETE, _iconHTML: iconHTML, _noSpinnerNotification: options && options.noSpinnerNotification, _spinnerIconAck: options ? !!options.noSpinnerNotification : false // Spinner ack is set when the user has seen the spinner icon spinning and clicked to see // weather the task succeeded or failed. Some tasks may want to be silent by setting noSpinnerNotification. }; function close() { delete taskList[task._id]; _showOrHideStatusBarIfNeeded(); renderSpinnerIcon(); } function setIconHTML(html) { task._iconHTML = html; if(task._$html){ task._$html.find(".task-icon").html(task._iconHTML); } } function setTitle(title) { task._title = title; if(task._$html){ task._$html.find(".task-title").text(task._title||task._id); } } function getTitle() { return task._title; } function setMessage(_message) { task._message = _message; if(task._$html){ task._$html.find(".task-message").text(task._message||""); } } function getMessage() { return task._message; } function show() { $("#status-tasks .btn-dropdown").click(); } function setProgressPercent(percent) { task._percent = percent; task._completedStatus = STATUS_INCOMPLETE; _renderProgressbar(task); task._spinnerIconAck= true; // when progress changes, there is no notification visual in status bar. renderSpinnerIcon(); } function getProgressPercent() { return task._percent; } function setFailed(){ task._completedStatus = STATUS_FAIL; _renderProgressbar(task); if(!task._noSpinnerNotification){ task._spinnerIconAck= false; } renderSpinnerIcon(); } function isFailed(){ return task._completedStatus === STATUS_FAIL; } function setSucceeded(){ task._completedStatus = STATUS_SUCCESS; _renderProgressbar(task); if(!task._noSpinnerNotification){ task._spinnerIconAck= false; } renderSpinnerIcon(); } function isSucceeded(){ return task._completedStatus === STATUS_SUCCESS; } function showStopIcon(tooltipMessage) { task._showStopIcon = tooltipMessage || Strings.STATUSBAR_TASKS_STOP; _renderPlayIcons(task); } function showPlayIcon(tooltipMessage) { task._showPlayIcon = tooltipMessage || Strings.STATUSBAR_TASKS_PLAY; _renderPlayIcons(task); } function showPauseIcon(tooltipMessage) { task._showPauseIcon = tooltipMessage || Strings.STATUSBAR_TASKS_PAUSE; _renderPlayIcons(task); } function showRestartIcon(tooltipMessage) { task._showRestartIcon = tooltipMessage || Strings.STATUSBAR_TASKS_RESTART; _renderPlayIcons(task); } function hideStopIcon() { task._showStopIcon = null; _renderPlayIcons(task); } function hidePlayIcon() { task._showPlayIcon = null; _renderPlayIcons(task); } function hidePauseIcon() { task._showPauseIcon = null; _renderPlayIcons(task); } function hideRestartIcon() { task._showRestartIcon = null; _renderPlayIcons(task); } function flashSpinnerForAttention() { renderSpinnerIcon(true); } task.show = show; task.close = close; task.setTitle = setTitle; task.getTitle = getTitle; task.setMessage = setMessage; task.getMessage = getMessage; task.setSucceded = setSucceeded; task.isSucceeded = isSucceeded; task.setFailed = setFailed; task.isFailed = isFailed; task.setProgressPercent = setProgressPercent; task.getProgressPercent = getProgressPercent; task.setIconHTML = setIconHTML; task.showStopIcon = showStopIcon; task.hideStopIcon = hideStopIcon; task.showPlayIcon = showPlayIcon; task.hidePlayIcon = hidePlayIcon; task.showPauseIcon = showPauseIcon; task.hidePauseIcon = hidePauseIcon; task.showRestartIcon = showRestartIcon; task.hideRestartIcon = hideRestartIcon; task.flashSpinnerForAttention = flashSpinnerForAttention; taskList[task._id] = task; EventDispatcher.makeEventDispatcher(task); _showOrHideStatusBarIfNeeded(); if(!task._noSpinnerNotification){ renderSpinnerIcon(true); } else { renderSpinnerIcon(); } return task; } function _setLegacyExtensionBusy(busy) { legacyExtensionBusy = busy; if(busy){ renderSpinnerIcon(true); } else { renderSpinnerIcon(); } _showOrHideStatusBarIfNeeded(); } // private apis exports._setTaskSelect = _setTaskSelect; exports._onDropdownShown = _onDropdownShown; exports._renderItem = _renderItem; exports._onSelect = _onSelect; exports._setLegacyExtensionBusy = _setLegacyExtensionBusy; window.TaskManager = exports; // public apis exports.addNewTask = addNewTask; });