import { chat_metadata, callPopup, saveSettingsDebounced, getCurrentChatId } from "../../../script.js"; import { getContext, extension_settings, saveMetadataDebounced } from "../../extensions.js"; import { substituteParams, eventSource, event_types, generateQuietPrompt, } from "../../../script.js"; import { registerSlashCommand } from "../../slash-commands.js"; const MODULE_NAME = "Objective" let taskTree = null let globalTasks = [] let currentChatId = "" let currentObjective = null let currentTask = null let checkCounter = 0 const defaultPrompts = { "createTask": `Pause your roleplay and generate a list of tasks to complete an objective. Your next response must be formatted as a numbered list of plain text entries. Do not include anything but the numbered list. The list must be prioritized in the order that tasks must be completed. The objective that you must make a numbered task list for is: [{{objective}}]. The tasks created should take into account the character traits of {{char}}. These tasks may or may not involve {{user}} directly. Be sure to include the objective as the final task. Given an example objective of 'Make me a four course dinner', here is an example output: 1. Determine what the courses will be 2. Find recipes for each course 3. Go shopping for supplies with {{user}} 4. Cook the food 5. Get {{user}} to set the table 6. Serve the food 7. Enjoy eating the meal with {{user}} `, "checkTaskCompleted": `Pause your roleplay. Determine if this task is completed: [{{task}}]. To do this, examine the most recent messages. Your response must only contain either true or false, nothing other words. Example output: true `, 'currentTask':`Your current task is [{{task}}]. Balance existing roleplay with completing this task.` } let objectivePrompts = defaultPrompts //###############################// //# Task Management #// //###############################// // Return the task and index or throw an error function getTaskById(taskId){ if (taskId == null) { throw `Null task id` } return getTaskByIdRecurse(taskId, taskTree) } function getTaskByIdRecurse(taskId, task) { if (task.id == taskId){ return task } for (const childTask of task.children) { const foundTask = getTaskByIdRecurse(taskId, childTask); if (foundTask != null) { return foundTask; } } return null; } function substituteParamsPrompts(content) { content = content.replace(/{{objective}}/gi, currentObjective.description) content = content.replace(/{{task}}/gi, currentTask.description) content = substituteParams(content) return content } // Call Quiet Generate to create task list using character context, then convert to tasks. Should not be called much. async function generateTasks() { const prompt = substituteParamsPrompts(objectivePrompts.createTask); console.log(`Generating tasks for objective with prompt`) toastr.info('Generating tasks for objective', 'Please wait...'); const taskResponse = await generateQuietPrompt(prompt) // Clear all existing objective tasks when generating currentObjective.children = [] const numberedListPattern = /^\d+\./ // Create tasks from generated task list for (const task of taskResponse.split('\n').map(x => x.trim())) { if (task.match(numberedListPattern) != null) { currentObjective.addTask(task.replace(numberedListPattern,"").trim()) } } updateUiTaskList(); setCurrentTask(); console.info(`Response for Objective: '${taskTree.description}' was \n'${taskResponse}', \nwhich created tasks \n${JSON.stringify(globalTasks.map(v => {return v.toSaveState()}), null, 2)} `) toastr.success(`Generated ${globalTasks.length} tasks`, 'Done!'); } // Call Quiet Generate to check if a task is completed async function checkTaskCompleted() { // Make sure there are tasks if (jQuery.isEmptyObject(currentTask)) { return } checkCounter = $('#objective-check-frequency').val() const prompt = substituteParamsPrompts(objectivePrompts.checkTaskCompleted); const taskResponse = (await generateQuietPrompt(prompt)).toLowerCase() // Check response if task complete if (taskResponse.includes("true")) { console.info(`Character determined task '${currentTask.description} is completed.`) currentTask.completeTask() } else if (!(taskResponse.includes("false"))) { console.warn(`checkTaskCompleted response did not contain true or false. taskResponse: ${taskResponse}`) } else { console.debug(`Checked task completion. taskResponse: ${taskResponse}`) } } function getNextIncompleteTaskRecurse(task){ // Skip tasks with children, as they will have subtasks to determine completeness if (task.completed === false && task.children.length === 0){ return task } for (const childTask of task.children) { if (childTask.completed === true){ // Don't recurse into completed tasks continue } const foundTask = getNextIncompleteTaskRecurse(childTask); if (foundTask != null) { return foundTask; } } return null; } // Set a task in extensionPrompt context. Defaults to first incomplete function setCurrentTask(taskId = null) { const context = getContext(); // TODO: Should probably null this rather than set empty object currentTask = {}; // Find the task, either next incomplete, or by provided taskId if (taskId === null) { currentTask = getNextIncompleteTaskRecurse(taskTree) || {}; } else { currentTask = getTaskById(taskId); } // Don't just check for a current task, check if it has data const description = currentTask.description || null; if (description) { const extensionPromptText = substituteParamsPrompts(objectivePrompts.currentTask); // Remove highlights $('.objective-task').css({'border-color':'','border-width':''}) // Highlight current task let highlightTask = currentTask while (highlightTask.parentId !== ""){ if (highlightTask.descriptionSpan){ highlightTask.descriptionSpan.css({'border-color':'yellow','border-width':'2px'}); } const parent = getTaskById(highlightTask.parentId) highlightTask = parent } // Update the extension prompt context.setExtensionPrompt(MODULE_NAME, extensionPromptText, 1, $('#objective-chat-depth').val()); console.info(`Current task in context.extensionPrompts.Objective is ${JSON.stringify(context.extensionPrompts.Objective)}`); } else { context.setExtensionPrompt(MODULE_NAME, ''); console.info(`No current task`); } saveState(); } function getHighestTaskIdRecurse(task) { let nextId = task.id; for (const childTask of task.children) { const childId = getHighestTaskIdRecurse(childTask); if (childId > nextId) { nextId = childId; } } return nextId; } //###############################// //# Task Class #// //###############################// class ObjectiveTask { id description completed parentId children // UI Elements taskHtml descriptionSpan completedCheckbox deleteTaskButton addTaskButton constructor ({id=undefined, description, completed=false, parentId=""}) { this.description = description this.parentId = parentId this.children = [] this.completed = completed // Generate a new ID if none specified if (id==undefined){ this.id = getHighestTaskIdRecurse(taskTree) + 1 } else { this.id=id } } // Accepts optional index. Defaults to adding to end of list. addTask(description, index = null) { index = index != null ? index: index = this.children.length this.children.splice(index, 0, new ObjectiveTask( {description: description, parentId: this.id} )) saveState() } getIndex(){ if (this.parentId !== null) { const parent = getTaskById(this.parentId) const index = parent.children.findIndex(task => task.id === this.id) if (index === -1){ throw `getIndex failed: Task '${this.description}' not found in parent task '${parent.description}'` } return index } else { throw `getIndex failed: Task '${this.description}' has no parent` } } // Used to set parent to complete when all child tasks are completed checkParentComplete() { let all_completed = true; if (this.parentId !== ""){ const parent = getTaskById(this.parentId); for (const child of parent.children){ if (!child.completed){ all_completed = false; break; } } if (all_completed){ parent.completed = true; console.info(`Parent task '${parent.description}' completed after all child tasks complated.`) } else { parent.completed = false; } } } // Complete the current task, setting next task to next incomplete task completeTask() { this.completed = true console.info(`Task successfully completed: ${JSON.stringify(this.description)}`) this.checkParentComplete() setCurrentTask() updateUiTaskList() } // Add a single task to the UI and attach event listeners for user edits addUiElement() { const template = `
${this.description}

`; // Add the filled out template $('#objective-tasks').append(template); this.completedCheckbox = $(`#objective-task-complete-${this.id}`); this.descriptionSpan = $(`#objective-task-description-${this.id}`); this.addButton = $(`#objective-task-add-${this.id}`); this.deleteButton = $(`#objective-task-delete-${this.id}`); this.taskHtml = $(`#objective-task-label-${this.id}`); this.branchButton = $(`#objective-task-add-branch-${this.id}`) // Handle sub-task forking style if (this.children.length > 0){ this.branchButton.css({'color':'#33cc33'}) } else { this.branchButton.css({'color':''}) } // Add event listeners and set properties $(`#objective-task-complete-${this.id}`).prop('checked', this.completed); $(`#objective-task-complete-${this.id}`).on('click', () => (this.onCompleteClick())); $(`#objective-task-description-${this.id}`).on('keyup', () => (this.onDescriptionUpdate())); $(`#objective-task-description-${this.id}`).on('focusout', () => (this.onDescriptionFocusout())); $(`#objective-task-delete-${this.id}`).on('click', () => (this.onDeleteClick())); $(`#objective-task-add-${this.id}`).on('click', () => (this.onAddClick())); this.branchButton.on('click', () => (this.onBranchClick())) } onBranchClick() { currentObjective = this updateUiTaskList(); setCurrentTask(); } onCompleteClick(){ this.completed = this.completedCheckbox.prop('checked') this.checkParentComplete() setCurrentTask(); } onDescriptionUpdate(){ this.description = this.descriptionSpan.text(); } onDescriptionFocusout(){ setCurrentTask(); } onDeleteClick(){ const index = this.getIndex() const parent = getTaskById(this.parentId) parent.children.splice(index, 1) updateUiTaskList() setCurrentTask() } onAddClick(){ const index = this.getIndex() const parent = getTaskById(this.parentId) parent.addTask("", index + 1); updateUiTaskList(); setCurrentTask(); } toSaveStateRecurse() { let children = [] if (this.children.length > 0){ for (const child of this.children){ children.push(child.toSaveStateRecurse()) } } return { "id":this.id, "description":this.description, "completed":this.completed, "parentId": this.parentId, "children": children, } } } //###############################// //# Custom Prompts #// //###############################// function onEditPromptClick() { let popupText = '' popupText += `
Edit prompts used by Objective for this session. You can use {{objective}} or {{task}} plus any other standard template variables
` callPopup(popupText, 'text') populateCustomPrompts() // Set current values $('#objective-prompt-generate').val(objectivePrompts.createTask) $('#objective-prompt-check').val(objectivePrompts.checkTaskCompleted) $('#objective-prompt-extension-prompt').val(objectivePrompts.currentTask) // Handle value updates $('#objective-prompt-generate').on('input', () => { objectivePrompts.createTask = $('#objective-prompt-generate').val() }) $('#objective-prompt-check').on('input', () => { objectivePrompts.checkTaskCompleted = $('#objective-prompt-check').val() }) $('#objective-prompt-extension-prompt').on('input', () => { objectivePrompts.currentTask = $('#objective-prompt-extension-prompt').val() }) // Handle new $('#objective-custom-prompt-new').on('click', () => { newCustomPrompt() }) // Handle save $('#objective-custom-prompt-save').on('click', () => { saveCustomPrompt() }) // Handle delete $('#objective-custom-prompt-delete').on('click', () => { deleteCustomPrompt() }) // Handle load $('#objective-custom-prompt-select').on('change', loadCustomPrompt) } async function newCustomPrompt() { const customPromptName = await callPopup('

Custom Prompt name:

', 'input'); if (customPromptName == "") { toastr.warning("Please set custom prompt name to save.") return } if (customPromptName == "default"){ toastr.error("Cannot save over default prompt") return } extension_settings.objective.customPrompts[customPromptName] = {} Object.assign(extension_settings.objective.customPrompts[customPromptName], objectivePrompts) saveSettingsDebounced() populateCustomPrompts() } function saveCustomPrompt() { const customPromptName = $("#objective-custom-prompt-select").find(':selected').val() if (customPromptName == "default"){ toastr.error("Cannot save over default prompt") return } Object.assign(extension_settings.objective.customPrompts[customPromptName], objectivePrompts) saveSettingsDebounced() populateCustomPrompts() } function deleteCustomPrompt(){ const customPromptName = $("#objective-custom-prompt-select").find(':selected').val() if (customPromptName == "default"){ toastr.error("Cannot delete default prompt") return } delete extension_settings.objective.customPrompts[customPromptName] saveSettingsDebounced() populateCustomPrompts() loadCustomPrompt() } function loadCustomPrompt(){ const optionSelected = $("#objective-custom-prompt-select").find(':selected').val() Object.assign(objectivePrompts, extension_settings.objective.customPrompts[optionSelected]) $('#objective-prompt-generate').val(objectivePrompts.createTask) $('#objective-prompt-check').val(objectivePrompts.checkTaskCompleted) $('#objective-prompt-extension-prompt').val(objectivePrompts.currentTask) } function populateCustomPrompts(){ // Populate saved prompts $('#objective-custom-prompt-select').empty() for (const customPromptName in extension_settings.objective.customPrompts){ const option = document.createElement('option'); option.innerText = customPromptName; option.value = customPromptName; option.selected = customPromptName $('#objective-custom-prompt-select').append(option) } } //###############################// //# UI AND Settings #// //###############################// const defaultSettings = { currentObjectiveId: null, taskTree: null, chatDepth: 2, checkFrequency: 3, hideTasks: false, prompts: defaultPrompts, } // Convenient single call. Not much at the moment. function resetState() { loadSettings(); } // function saveState() { const context = getContext(); if (currentChatId == "") { currentChatId = context.chatId } chat_metadata['objective'] = { currentObjectiveId: currentObjective.id, taskTree: taskTree.toSaveStateRecurse(), checkFrequency: $('#objective-check-frequency').val(), chatDepth: $('#objective-chat-depth').val(), hideTasks: $('#objective-hide-tasks').prop('checked'), prompts: objectivePrompts, } saveMetadataDebounced(); } // Dump core state function debugObjectiveExtension() { console.log(JSON.stringify({ "currentTask": currentTask, "currentObjective": currentObjective, "taskTree": taskTree.toSaveStateRecurse(), "chat_metadata": chat_metadata['objective'], "extension_settings": extension_settings['objective'], "prompts": objectivePrompts }, null, 2)) } window.debugObjectiveExtension = debugObjectiveExtension // Populate UI task list function updateUiTaskList() { $('#objective-tasks').empty() // Show button to navigate back to parent objective if parent exists if (currentObjective){ if (currentObjective.parentId !== "") { $('#objective-parent').show() } else { $('#objective-parent').hide() } } $('#objective-text').val(currentObjective.description) if (currentObjective.children.length > 0){ // Show tasks if there are any to show for (const task of currentObjective.children) { task.addUiElement() } } else { // Show button to add tasks if there are none $('#objective-tasks').append(` `) $("#objective-task-add-first").on('click', () => { currentObjective.addTask("") setCurrentTask() updateUiTaskList() }) } } function onParentClick() { currentObjective = getTaskById(currentObjective.parentId) updateUiTaskList() setCurrentTask() } // Trigger creation of new tasks with given objective. async function onGenerateObjectiveClick() { await generateTasks() saveState() } // Update extension prompts function onChatDepthInput() { saveState() setCurrentTask() // Ensure extension prompt is updated } function onObjectiveTextFocusOut(){ if (currentObjective){ currentObjective.description = $('#objective-text').val() saveState() } } // Update how often we check for task completion function onCheckFrequencyInput() { checkCounter = $("#objective-check-frequency").val() $('#objective-counter').text(checkCounter) saveState() } function onHideTasksInput() { $('#objective-tasks').prop('hidden', $('#objective-hide-tasks').prop('checked')) saveState() } function loadTaskChildrenRecurse(savedTask) { let tempTaskTree = new ObjectiveTask({ id: savedTask.id, description: savedTask.description, completed: savedTask.completed, parentId: savedTask.parentId, }) for (const task of savedTask.children){ const childTask = loadTaskChildrenRecurse(task) tempTaskTree.children.push(childTask) } return tempTaskTree } function loadSettings() { // Load/Init settings for chatId currentChatId = getContext().chatId // Reset Objectives and Tasks in memory taskTree = null; currentObjective = null; // Init extension settings if (Object.keys(extension_settings.objective).length === 0) { Object.assign(extension_settings.objective, { 'customPrompts': {'default':defaultPrompts}}) } // Bail on home screen if (currentChatId == undefined) { return } // Migrate existing settings if (currentChatId in extension_settings.objective) { // TODO: Remove this soon chat_metadata['objective'] = extension_settings.objective[currentChatId]; delete extension_settings.objective[currentChatId]; } if (!('objective' in chat_metadata)) { Object.assign(chat_metadata, { objective: defaultSettings }); } // Migrate legacy flat objective to new objectiveTree and currentObjective if ('objective' in chat_metadata.objective) { // Create root objective from legacy objective taskTree = new ObjectiveTask({id:0, description: chat_metadata.objective.objective}); currentObjective = taskTree; // Populate root objective tree from legacy tasks if ('tasks' in chat_metadata.objective) { let idIncrement = 0; taskTree.children = chat_metadata.objective.tasks.map(task => { idIncrement += 1; return new ObjectiveTask({ id: idIncrement, description: task.description, completed: task.completed, parentId: taskTree.id, }) }); } saveState(); delete chat_metadata.objective.objective; delete chat_metadata.objective.tasks; } else { // Load Objectives and Tasks (Normal path) if (chat_metadata.objective.taskTree){ taskTree = loadTaskChildrenRecurse(chat_metadata.objective.taskTree) } } // Make sure there's a root task if (!taskTree) { taskTree = new ObjectiveTask({id:0,description:$('#objective-text').val()}) } currentObjective = taskTree checkCounter = chat_metadata['objective'].checkFrequency // Update UI elements $('#objective-counter').text(checkCounter) $("#objective-text").text(taskTree.description) updateUiTaskList() $('#objective-chat-depth').val(chat_metadata['objective'].chatDepth) $('#objective-check-frequency').val(chat_metadata['objective'].checkFrequency) $('#objective-hide-tasks').prop('checked', chat_metadata['objective'].hideTasks) $('#objective-tasks').prop('hidden', $('#objective-hide-tasks').prop('checked')) setCurrentTask() } function addManualTaskCheckUi() { $('#extensionsMenu').prepend(`
Manual Task Check
`) $('#objective-task-manual-check-menu-item').attr('title', 'Trigger AI check of completed tasks').on('click', checkTaskCompleted) } jQuery(() => { const settingsHtml = `
Objective
Go to parent task

(0 = disabled)
Messages until next AI task completion check 0

`; addManualTaskCheckUi() $('#extensions_settings').append(settingsHtml); $('#objective-generate').on('click', onGenerateObjectiveClick) $('#objective-chat-depth').on('input', onChatDepthInput) $("#objective-check-frequency").on('input', onCheckFrequencyInput) $('#objective-hide-tasks').on('click', onHideTasksInput) $('#objective_prompt_edit').on('click', onEditPromptClick) $('#objective-parent').hide() $('#objective-parent').on('click',onParentClick) $('#objective-text').on('focusout',onObjectiveTextFocusOut) loadSettings() eventSource.on(event_types.CHAT_CHANGED, () => { resetState() }); eventSource.on(event_types.MESSAGE_RECEIVED, () => { if (currentChatId == undefined) { return } if ($("#objective-check-frequency").val() > 0) { // Check only at specified interval if (checkCounter <= 0) { checkTaskCompleted(); } checkCounter -= 1 } setCurrentTask(); $('#objective-counter').text(checkCounter) }); registerSlashCommand('taskcheck', checkTaskCompleted, [], ' – checks if the current task is completed', true, true); });