mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2024-12-16 11:19:40 +01:00
824 lines
30 KiB
JavaScript
824 lines
30 KiB
JavaScript
import { chat_metadata, callPopup, saveSettingsDebounced, is_send_press } 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";
|
||
import { waitUntilCondition } from "../../utils.js";
|
||
import { is_group_generating, selected_group } from "../../group-chats.js";
|
||
|
||
const MODULE_NAME = "Objective"
|
||
|
||
|
||
let taskTree = null
|
||
let globalTasks = []
|
||
let currentChatId = ""
|
||
let currentObjective = null
|
||
let currentTask = null
|
||
let checkCounter = 0
|
||
let lastMessageWasSwipe = false
|
||
|
||
|
||
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)
|
||
if (currentTask.parent){
|
||
content = content.replace(/{{parent}}/gi, currentTask.parent.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: '${currentObjective.description}' was \n'${taskResponse}', \nwhich created tasks \n${JSON.stringify(currentObjective.children.map(v => {return v.toSaveState()}), null, 2)} `)
|
||
toastr.success(`Generated ${currentObjective.children.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
|
||
}
|
||
|
||
try {
|
||
// Wait for group to finish generating
|
||
if (selected_group) {
|
||
await waitUntilCondition(() => is_group_generating === false, 1000, 10);
|
||
}
|
||
// Another extension might be doing something with the chat, so wait for it to finish
|
||
await waitUntilCondition(() => is_send_press === false, 30000, 10);
|
||
} catch {
|
||
console.debug("Failed to wait for group to finish generating")
|
||
return;
|
||
}
|
||
|
||
checkCounter = $('#objective-check-frequency').val()
|
||
toastr.info("Checking for task completion.")
|
||
|
||
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){
|
||
if (task.completed === false // Return task if incomplete
|
||
&& task.children.length === 0 // Ensure task has no children, it's subtasks will determine completeness
|
||
&& task.parentId !== "" // Must have parent id. Only root task will be missing this and we dont want that
|
||
){
|
||
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 = `
|
||
<div id="objective-task-label-${this.id}" class="flex1 checkbox_label">
|
||
<input id="objective-task-complete-${this.id}" type="checkbox">
|
||
<span class="text_pole objective-task" 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 id="objective-task-add-branch-${this.id}" class="objective-task-button fa-solid fa-code-fork fa-2x" title="Branch 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}`);
|
||
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 += `
|
||
<div class="objective_prompt_modal">
|
||
<small>Edit prompts used by Objective for this session. You can use {{objective}} or {{task}} plus any other standard template variables. Save template to persist changes.</small>
|
||
<br>
|
||
<div>
|
||
<label for="objective-prompt-generate">Generation Prompt</label>
|
||
<textarea id="objective-prompt-generate" type="text" class="text_pole textarea_compact" rows="8"></textarea>
|
||
<label for="objective-prompt-check">Completion Check Prompt</label>
|
||
<textarea id="objective-prompt-check" type="text" class="text_pole textarea_compact" rows="8"></textarea>
|
||
<label for="objective-prompt-extension-prompt">Injected Prompt</label>
|
||
<textarea id="objective-prompt-extension-prompt" type="text" class="text_pole textarea_compact" rows="8"></textarea>
|
||
</div>
|
||
<div class="objective_prompt_block">
|
||
<label for="objective-custom-prompt-select">Custom Prompt Select</label>
|
||
<select id="objective-custom-prompt-select"><select>
|
||
</div>
|
||
<div class="objective_prompt_block">
|
||
<input id="objective-custom-prompt-new" class="menu_button" type="submit" value="New Prompt" />
|
||
<input id="objective-custom-prompt-save" class="menu_button" type="submit" value="Save Prompt" />
|
||
<input id="objective-custom-prompt-delete" class="menu_button" type="submit" value="Delete Prompt" />
|
||
</div>
|
||
</div>`
|
||
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('<h3>Custom Prompt name:</h3>', '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() {
|
||
lastMessageWasSwipe = false
|
||
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(`
|
||
<input id="objective-task-add-first" type="button" class="menu_button" value="Add Task">
|
||
`)
|
||
$("#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(`
|
||
<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-parent" class="objective_block flex-container">
|
||
<i class="objective-task-button fa-solid fa-circle-left fa-2x" title="Go to Parent"></i>
|
||
<small>Go to parent task</small>
|
||
</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>
|
||
<div class="objective_block flex-container">
|
||
<input id="objective_prompt_edit" class="menu_button" type="submit" value="Edit Prompts" />
|
||
</div>
|
||
<hr class="sysHR">
|
||
</div>
|
||
</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)
|
||
$('#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_SWIPED, () => {
|
||
lastMessageWasSwipe = true
|
||
})
|
||
eventSource.on(event_types.MESSAGE_RECEIVED, () => {
|
||
if (currentChatId == undefined || jQuery.isEmptyObject(currentTask) || lastMessageWasSwipe) {
|
||
lastMessageWasSwipe = false
|
||
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);
|
||
});
|