mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			482 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			482 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| import { chat_metadata } 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 globalObjective = ""
 | ||
| let globalTasks = []
 | ||
| let currentChatId = ""
 | ||
| let currentTask = null
 | ||
| let checkCounter = 0
 | ||
| 
 | ||
| 
 | ||
| const objectivePrompts = {
 | ||
|     "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
 | ||
|     `
 | ||
| }
 | ||
| 
 | ||
| const extensionPrompt = "Your current task is [{{task}}]. Balance existing roleplay with completing this task."
 | ||
| 
 | ||
| 
 | ||
| //###############################//
 | ||
| //#       Task Management       #//
 | ||
| //###############################//
 | ||
| 
 | ||
| // Accepts optional index. Defaults to adding to end of list.
 | ||
| function addTask(description, index = null) {
 | ||
|     index = index != null ? index: index = globalTasks.length
 | ||
|     globalTasks.splice(index, 0, new ObjectiveTask(
 | ||
|         {description: description}
 | ||
|     ))
 | ||
|     saveState()
 | ||
| }
 | ||
| 
 | ||
| // Return the task and index or throw an error
 | ||
| function getTaskById(taskId){
 | ||
|     if (taskId == null) {
 | ||
|         throw `Null task id`
 | ||
|     }
 | ||
|     const index = globalTasks.findIndex((task) => task.id === taskId);
 | ||
|     if (index !== -1) {
 | ||
|         return { task: globalTasks[index], index: index };
 | ||
|     } else {
 | ||
|         throw `Cannot find task with ${taskId}`
 | ||
| 
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| function deleteTask(taskId){
 | ||
|     const { task, index } = getTaskById(taskId)
 | ||
| 
 | ||
|     globalTasks.splice(index, 1)
 | ||
|     setCurrentTask()
 | ||
|     updateUiTaskList()
 | ||
| }
 | ||
| 
 | ||
| // Call Quiet Generate to create task list using character context, then convert to tasks. Should not be called much.
 | ||
| async function generateTasks() {
 | ||
|     const prompt = substituteParams(objectivePrompts["createTask"].replace(/{{objective}}/gi, globalObjective));
 | ||
|     console.log(`Generating tasks for objective with prompt`)
 | ||
|     toastr.info('Generating tasks for objective', 'Please wait...');
 | ||
|     const taskResponse = await generateQuietPrompt(prompt)
 | ||
| 
 | ||
|     // Clear all existing global tasks when generating
 | ||
|     globalTasks = []
 | ||
|     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) {
 | ||
|             addTask(task.replace(numberedListPattern,"").trim())
 | ||
|         }
 | ||
|     }
 | ||
|     updateUiTaskList()
 | ||
|     console.info(`Response for Objective: '${globalObjective}' 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 = substituteParams(objectivePrompts["checkTaskCompleted"].replace(/{{task}}/gi, currentTask.description));
 | ||
|     const taskResponse = (await generateQuietPrompt(prompt)).toLowerCase()
 | ||
| 
 | ||
|     // Check response if task complete
 | ||
|     if (taskResponse.includes("true")) {
 | ||
|         console.info(`Character determined task '${JSON.stringify(currentTask.toSaveState())} 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}`)
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| 
 | ||
| // Set a task in extensionPrompt context. Defaults to first incomplete
 | ||
| function setCurrentTask(taskId = null) {
 | ||
|     const context = getContext();
 | ||
|     currentTask = {};
 | ||
| 
 | ||
|     // Set current task to either the next incomplete task, or the index
 | ||
|     if (taskId === null) {
 | ||
|         currentTask = globalTasks.find(task => !task.completed) || {};
 | ||
|     } else {
 | ||
|         const { _, index } = getTaskById(taskId)
 | ||
|         currentTask = globalTasks[index];
 | ||
|     }
 | ||
| 
 | ||
|     // Get the task description and add to extension prompt
 | ||
|     const description = currentTask.description || null;
 | ||
| 
 | ||
|     // Now update the extension prompt
 | ||
| 
 | ||
|     if (description) {
 | ||
|         const extensionPromptText = extensionPrompt.replace(/{{task}}/gi, description);
 | ||
|         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();
 | ||
| }
 | ||
| 
 | ||
| let taskIdCounter = 0
 | ||
| function getNextTaskId(){
 | ||
|     // Make sure id does not exist
 | ||
|     while (globalTasks.find(task => task.id == taskIdCounter) != undefined) {
 | ||
|         taskIdCounter += 1
 | ||
|     }
 | ||
|     const nextId = taskIdCounter
 | ||
|     console.log(`TaskID assigned: ${nextId}`)
 | ||
|     taskIdCounter += 1
 | ||
|     return nextId
 | ||
| }
 | ||
| class ObjectiveTask {
 | ||
|     id
 | ||
|     description
 | ||
|     completed
 | ||
|     parent
 | ||
|     children
 | ||
| 
 | ||
|     // UI Elements
 | ||
|     taskHtml
 | ||
|     descriptionSpan
 | ||
|     completedCheckbox
 | ||
|     deleteTaskButton
 | ||
|     addTaskButton
 | ||
| 
 | ||
|     constructor ({id=undefined, description, completed=false, parent=null}) {
 | ||
|         this.description = description
 | ||
|         this.parent = parent
 | ||
|         this.children = []
 | ||
|         this.completed = completed
 | ||
| 
 | ||
|         // Generate a new ID if none specified
 | ||
|         if (id==undefined){
 | ||
|             this.id = getNextTaskId()
 | ||
|         } else {
 | ||
|             this.id=id
 | ||
|         }
 | ||
|     }
 | ||
| 
 | ||
| 
 | ||
|     // Complete the current task, setting next task to next incomplete task
 | ||
|     completeTask() {
 | ||
|         this.completed = true
 | ||
|         console.info(`Task successfully completed: ${JSON.stringify(this.description)}`)
 | ||
|         setCurrentTask()
 | ||
|         updateUiTaskList()
 | ||
|     }
 | ||
| 
 | ||
|     // Add a single task to the UI and attach event listeners for user edits
 | ||
|     addUiElement() {
 | ||
|         const template = `
 | ||
|         <div id="objective-task-label-${this.id}" class="flex1 checkbox_label">
 | ||
|             <input id="objective-task-complete-${this.id}" type="checkbox">
 | ||
|             <span class="text_pole" style="display: block" id="objective-task-description-${this.id}" contenteditable>${this.description}</span>
 | ||
|             <div id="objective-task-delete-${this.id}" class="objective-task-button fa-solid fa-xmark fa-2x" title="Delete Task"></div>
 | ||
|             <div id="objective-task-add-${this.id}" class="objective-task-button fa-solid fa-plus fa-2x" title="Add Task"></div>
 | ||
|         </div><br>
 | ||
|         `;
 | ||
| 
 | ||
|         // 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}`);
 | ||
| 
 | ||
|         // 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()));
 | ||
|     }
 | ||
| 
 | ||
|     onCompleteClick(){
 | ||
|         this.completed = this.completedCheckbox.prop('checked')
 | ||
|         setCurrentTask();
 | ||
|     }
 | ||
| 
 | ||
|     onDescriptionUpdate(){
 | ||
|         this.description = this.descriptionSpan.text();
 | ||
|     }
 | ||
|     onDescriptionFocusout(){
 | ||
|         setCurrentTask();
 | ||
|     }
 | ||
| 
 | ||
|     onDeleteClick(){
 | ||
|         deleteTask(this.id);
 | ||
|     }
 | ||
| 
 | ||
|     onAddClick(){
 | ||
|         const {_, index} = getTaskById(this.id)
 | ||
|         addTask("", index + 1);
 | ||
|         setCurrentTask();
 | ||
|         updateUiTaskList();
 | ||
|     }
 | ||
| 
 | ||
|     toSaveState() {
 | ||
|         return {
 | ||
|             "id":this.id,
 | ||
|             "description":this.description,
 | ||
|             "completed":this.completed,
 | ||
|             "parent": this.parent,
 | ||
|         }
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| //###############################//
 | ||
| //#       UI AND Settings       #//
 | ||
| //###############################//
 | ||
| 
 | ||
| 
 | ||
| const defaultSettings = {
 | ||
|     objective: "",
 | ||
|     tasks: [],
 | ||
|     chatDepth: 2,
 | ||
|     checkFrequency: 3,
 | ||
|     hideTasks: false
 | ||
| }
 | ||
| 
 | ||
| // Convenient single call. Not much at the moment.
 | ||
| function resetState() {
 | ||
|     loadSettings();
 | ||
| }
 | ||
| 
 | ||
| //
 | ||
| function saveState() {
 | ||
|     const context = getContext();
 | ||
| 
 | ||
|     if (currentChatId == "") {
 | ||
|         currentChatId = context.chatId
 | ||
|     }
 | ||
| 
 | ||
|     // Convert globalTasks for saving
 | ||
|     const tasks = globalTasks.map(task => {return task.toSaveState()})
 | ||
| 
 | ||
|     chat_metadata['objective'] = {
 | ||
|         objective: globalObjective,
 | ||
|         tasks: tasks,
 | ||
|         checkFrequency: $('#objective-check-frequency').val(),
 | ||
|         chatDepth: $('#objective-chat-depth').val(),
 | ||
|         hideTasks: $('#objective-hide-tasks').prop('checked'),
 | ||
|     }
 | ||
| 
 | ||
|     saveMetadataDebounced();
 | ||
| }
 | ||
| 
 | ||
| // Dump core state
 | ||
| function debugObjectiveExtension() {
 | ||
|     console.log(JSON.stringify({
 | ||
|         "currentTask": currentTask.toSaveState(),
 | ||
|         "currentChatId": currentChatId,
 | ||
|         "checkCounter": checkCounter,
 | ||
|         "globalObjective": globalObjective,
 | ||
|         "globalTasks": globalTasks.map(v => {return v.toSaveState()}),
 | ||
|         "extension_settings": chat_metadata['objective'],
 | ||
|     }, null, 2))
 | ||
| }
 | ||
| 
 | ||
| window.debugObjectiveExtension = debugObjectiveExtension
 | ||
| 
 | ||
| 
 | ||
| // Populate UI task list
 | ||
| function updateUiTaskList() {
 | ||
|     $('#objective-tasks').empty()
 | ||
|     // Show tasks if there are any
 | ||
|     if (globalTasks.length > 0){
 | ||
|         for (const task of globalTasks) {
 | ||
|             task.addUiElement()
 | ||
|         }
 | ||
|     } else {
 | ||
|         // Show button to add tasks if there are none
 | ||
|         $('#objective-tasks').append(`
 | ||
|         <input id="objective-task-add-first" type="button" class="menu_button" value="Add Task">
 | ||
|         `)
 | ||
|         $("#objective-task-add-first").on('click', () => {
 | ||
|             addTask("")
 | ||
|             setCurrentTask()
 | ||
|             updateUiTaskList()
 | ||
|         })
 | ||
|     }
 | ||
| }
 | ||
| 
 | ||
| 
 | ||
| // Trigger creation of new tasks with given objective.
 | ||
| async function onGenerateObjectiveClick() {
 | ||
|     globalObjective = $('#objective-text').val()
 | ||
|     await generateTasks()
 | ||
|     saveState()
 | ||
| }
 | ||
| 
 | ||
| // Update extension prompts
 | ||
| function onChatDepthInput() {
 | ||
|     saveState()
 | ||
|     setCurrentTask() // Ensure extension prompt is updated
 | ||
| }
 | ||
| 
 | ||
| // 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 loadSettings() {
 | ||
|     // Load/Init settings for chatId
 | ||
|     currentChatId = getContext().chatId
 | ||
| 
 | ||
|     // Bail on home screen
 | ||
|     if (currentChatId == undefined) {
 | ||
|         return
 | ||
|     }
 | ||
| 
 | ||
|     // Migrate existing settings
 | ||
|     if (currentChatId in extension_settings.objective) {
 | ||
|         chat_metadata['objective'] = extension_settings.objective[currentChatId];
 | ||
|         delete extension_settings.objective[currentChatId];
 | ||
|     }
 | ||
| 
 | ||
|     if (!('objective' in chat_metadata)) {
 | ||
|         Object.assign(chat_metadata, { objective: defaultSettings });
 | ||
|     }
 | ||
| 
 | ||
|     // Update globals
 | ||
|     globalObjective = chat_metadata['objective'].objective
 | ||
|     globalTasks = chat_metadata['objective'].tasks.map(task => {
 | ||
|         return new ObjectiveTask({
 | ||
|             id: task.id,
 | ||
|             description: task.description,
 | ||
|             completed: task.completed,
 | ||
|             parent: task.parent,
 | ||
|         })
 | ||
|     });
 | ||
|     checkCounter = chat_metadata['objective'].checkFrequency
 | ||
| 
 | ||
|     // Update UI elements
 | ||
|     $('#objective-counter').text(checkCounter)
 | ||
|     $("#objective-text").text(globalObjective)
 | ||
|     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)
 | ||
|     onHideTasksInput()
 | ||
|     setCurrentTask()
 | ||
| }
 | ||
| 
 | ||
| function addManualTaskCheckUi() {
 | ||
|     $('#extensionsMenu').prepend(`
 | ||
|         <div id="objective-task-manual-check-menu-item" class="list-group-item flex-container flexGap5">
 | ||
|             <div id="objective-task-manual-check" class="extensionsMenuExtensionButton fa-regular fa-square-check"/></div>
 | ||
|             Manual Task Check
 | ||
|         </div>`)
 | ||
|     $('#objective-task-manual-check-menu-item').attr('title', 'Trigger AI check of completed tasks').on('click', checkTaskCompleted)
 | ||
| }
 | ||
| 
 | ||
| jQuery(() => {
 | ||
|     const settingsHtml = `
 | ||
|     <div class="objective-settings">
 | ||
|         <div class="inline-drawer">
 | ||
|             <div class="inline-drawer-toggle inline-drawer-header">
 | ||
|             <b>Objective</b>
 | ||
|             <div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
 | ||
|         </div>
 | ||
|         <div class="inline-drawer-content">
 | ||
|             <label for="objective-text"><small>Enter an objective and generate tasks. The AI will attempt to complete tasks autonomously</small></label>
 | ||
|             <textarea id="objective-text" type="text" class="text_pole textarea_compact" rows="4"></textarea>
 | ||
|             <div class="objective_block flex-container">
 | ||
|                 <input id="objective-generate" class="menu_button" type="submit" value="Auto-Generate Tasks" />
 | ||
|                 <label class="checkbox_label"><input id="objective-hide-tasks" type="checkbox"> Hide Tasks</label>
 | ||
|             </div>
 | ||
| 
 | ||
|             <div id="objective-tasks"> </div>
 | ||
|             <div class="objective_block margin-bot-10px">
 | ||
|                 <div class="objective_block objective_block_control flex1 flexFlowColumn">
 | ||
|                     <label for="objective-chat-depth">Position in Chat</label>
 | ||
|                     <input id="objective-chat-depth" class="text_pole widthUnset" type="number" min="0" max="99" />
 | ||
|                 </div>
 | ||
|                 <br>
 | ||
|                 <div class="objective_block objective_block_control flex1">
 | ||
| 
 | ||
|                     <label for="objective-check-frequency">Task Check Frequency</label>
 | ||
|                     <input id="objective-check-frequency" class="text_pole widthUnset" type="number" min="0" max="99" />
 | ||
|                     <small>(0 = disabled)</small>
 | ||
|                 </div>
 | ||
|             </div>
 | ||
|             <span> Messages until next AI task completion check <span id="objective-counter">0</span></span>
 | ||
|             <hr class="sysHR">
 | ||
|         </div>
 | ||
|     </div>`;
 | ||
| 
 | ||
|     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)
 | ||
|     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);
 | ||
| });
 |