mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			414 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			414 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
"use strict";
 | 
						|
 | 
						|
import { saveSettingsDebounced, substituteParams } from "../script.js";
 | 
						|
import { selected_group } from "./group-chats.js";
 | 
						|
import {
 | 
						|
    power_user,
 | 
						|
    context_presets,
 | 
						|
} from "./power-user.js";
 | 
						|
import { resetScrollHeight } from "./utils.js";
 | 
						|
 | 
						|
/**
 | 
						|
 * @type {any[]} Instruct mode presets.
 | 
						|
 */
 | 
						|
export let instruct_presets = [];
 | 
						|
 | 
						|
const controls = [
 | 
						|
    { id: "instruct_enabled", property: "enabled", isCheckbox: true },
 | 
						|
    { id: "instruct_wrap", property: "wrap", isCheckbox: true },
 | 
						|
    { id: "instruct_system_prompt", property: "system_prompt", isCheckbox: false },
 | 
						|
    { id: "instruct_system_sequence_prefix", property: "system_sequence_prefix", isCheckbox: false },
 | 
						|
    { id: "instruct_system_sequence_suffix", property: "system_sequence_suffix", isCheckbox: false },
 | 
						|
    { id: "instruct_separator_sequence", property: "separator_sequence", isCheckbox: false },
 | 
						|
    { id: "instruct_input_sequence", property: "input_sequence", isCheckbox: false },
 | 
						|
    { id: "instruct_output_sequence", property: "output_sequence", isCheckbox: false },
 | 
						|
    { id: "instruct_stop_sequence", property: "stop_sequence", isCheckbox: false },
 | 
						|
    { id: "instruct_names", property: "names", isCheckbox: true },
 | 
						|
    { id: "instruct_macro", property: "macro", isCheckbox: true },
 | 
						|
    { id: "instruct_names_force_groups", property: "names_force_groups", isCheckbox: true },
 | 
						|
    { id: "instruct_first_output_sequence", property: "first_output_sequence", isCheckbox: false },
 | 
						|
    { id: "instruct_last_output_sequence", property: "last_output_sequence", isCheckbox: false },
 | 
						|
    { id: "instruct_activation_regex", property: "activation_regex", isCheckbox: false },
 | 
						|
];
 | 
						|
 | 
						|
/**
 | 
						|
 * Loads instruct mode settings from the given data object.
 | 
						|
 * @param {object} data Settings data object.
 | 
						|
 */
 | 
						|
export function loadInstructMode(data) {
 | 
						|
    if (data.instruct !== undefined) {
 | 
						|
        instruct_presets = data.instruct;
 | 
						|
    }
 | 
						|
 | 
						|
    if (power_user.instruct.names_force_groups === undefined) {
 | 
						|
        power_user.instruct.names_force_groups = true;
 | 
						|
    }
 | 
						|
 | 
						|
    controls.forEach(control => {
 | 
						|
        const $element = $(`#${control.id}`);
 | 
						|
 | 
						|
        if (control.isCheckbox) {
 | 
						|
            $element.prop('checked', power_user.instruct[control.property]);
 | 
						|
        } else {
 | 
						|
            $element.val(power_user.instruct[control.property]);
 | 
						|
        }
 | 
						|
 | 
						|
        $element.on('input', function () {
 | 
						|
            power_user.instruct[control.property] = control.isCheckbox ? !!$(this).prop('checked') : $(this).val();
 | 
						|
            saveSettingsDebounced();
 | 
						|
            if (!control.isCheckbox) {
 | 
						|
                resetScrollHeight($element);
 | 
						|
            }
 | 
						|
        });
 | 
						|
    });
 | 
						|
 | 
						|
    instruct_presets.forEach((preset) => {
 | 
						|
        const name = preset.name;
 | 
						|
        const option = document.createElement('option');
 | 
						|
        option.value = name;
 | 
						|
        option.innerText = name;
 | 
						|
        option.selected = name === power_user.instruct.preset;
 | 
						|
        $('#instruct_presets').append(option);
 | 
						|
    });
 | 
						|
 | 
						|
    highlightDefaultPreset();
 | 
						|
}
 | 
						|
 | 
						|
function highlightDefaultPreset() {
 | 
						|
    $('#instruct_set_default').toggleClass('default', power_user.default_instruct === power_user.instruct.preset);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Select context template if not already selected.
 | 
						|
 * @param {string} preset Preset name.
 | 
						|
 */
 | 
						|
function selectContextPreset(preset) {
 | 
						|
    // If context template is not already selected, select it
 | 
						|
    if (preset !== power_user.context.preset) {
 | 
						|
        $('#context_presets').val(preset).trigger('change');
 | 
						|
        toastr.info(`Context Template: preset "${preset}" auto-selected`);
 | 
						|
    }
 | 
						|
 | 
						|
    // If instruct mode is disabled, enable it, except for default context template
 | 
						|
    if (!power_user.instruct.enabled && preset !== power_user.default_context) {
 | 
						|
        power_user.instruct.enabled = true;
 | 
						|
        $('#instruct_enabled').prop('checked', true).trigger('change');
 | 
						|
        toastr.info(`Instruct Mode enabled`);
 | 
						|
    }
 | 
						|
 | 
						|
    saveSettingsDebounced();
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Select instruct preset if not already selected.
 | 
						|
 * @param {string} preset Preset name.
 | 
						|
 */
 | 
						|
export function selectInstructPreset(preset) {
 | 
						|
    // If instruct preset is not already selected, select it
 | 
						|
    if (preset !== power_user.instruct.preset) {
 | 
						|
        $('#instruct_presets').val(preset).trigger('change');
 | 
						|
        toastr.info(`Instruct Mode: preset "${preset}" auto-selected`);
 | 
						|
    }
 | 
						|
 | 
						|
    // If instruct mode is disabled, enable it
 | 
						|
    if (!power_user.instruct.enabled) {
 | 
						|
        power_user.instruct.enabled = true;
 | 
						|
        $('#instruct_enabled').prop('checked', true).trigger('change');
 | 
						|
        toastr.info(`Instruct Mode enabled`);
 | 
						|
    }
 | 
						|
 | 
						|
    saveSettingsDebounced();
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Automatically select instruct preset based on model id.
 | 
						|
 * Otherwise, if default instruct preset is set, selects it.
 | 
						|
 * @param {string} modelId Model name reported by the API.
 | 
						|
 * @returns {boolean} True if instruct preset was activated by model id, false otherwise.
 | 
						|
 */
 | 
						|
export function autoSelectInstructPreset(modelId) {
 | 
						|
    // If instruct mode is disabled, don't do anything
 | 
						|
    if (!power_user.instruct.enabled) {
 | 
						|
        return false;
 | 
						|
    }
 | 
						|
 | 
						|
    // Select matching instruct preset
 | 
						|
    let foundMatch = false;
 | 
						|
    for (const instruct_preset of instruct_presets) {
 | 
						|
        // If instruct preset matches the context template
 | 
						|
        if (instruct_preset.name === power_user.context.preset) {
 | 
						|
            foundMatch = true;
 | 
						|
            selectInstructPreset(instruct_preset.name);
 | 
						|
            break;
 | 
						|
        }
 | 
						|
    }
 | 
						|
    // If no match was found, auto-select instruct preset
 | 
						|
    if (!foundMatch) {
 | 
						|
        for (const preset of instruct_presets) {
 | 
						|
            // If activation regex is set, check if it matches the model id
 | 
						|
            if (preset.activation_regex) {
 | 
						|
                try {
 | 
						|
                    const regex = new RegExp(preset.activation_regex, 'i');
 | 
						|
 | 
						|
                    // Stop on first match so it won't cycle back and forth between presets if multiple regexes match
 | 
						|
                    if (regex.test(modelId)) {
 | 
						|
                        selectInstructPreset(preset.name);
 | 
						|
 | 
						|
                        return true;
 | 
						|
                    }
 | 
						|
                } catch {
 | 
						|
                    // If regex is invalid, ignore it
 | 
						|
                    console.warn(`Invalid instruct activation regex in preset "${preset.name}"`);
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
 | 
						|
        if (power_user.default_instruct && power_user.instruct.preset !== power_user.default_instruct) {
 | 
						|
            if (instruct_presets.some(p => p.name === power_user.default_instruct)) {
 | 
						|
                console.log(`Instruct mode: default preset "${power_user.default_instruct}" selected`);
 | 
						|
                $('#instruct_presets').val(power_user.default_instruct).trigger('change');
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    return false;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Converts instruct mode sequences to an array of stopping strings.
 | 
						|
 * @returns {string[]} Array of instruct mode stopping strings.
 | 
						|
 */
 | 
						|
export function getInstructStoppingSequences() {
 | 
						|
    /**
 | 
						|
     * Adds instruct mode sequence to the result array.
 | 
						|
     * @param {string} sequence Sequence string.
 | 
						|
     * @returns {void}
 | 
						|
     */
 | 
						|
    function addInstructSequence(sequence) {
 | 
						|
        // Cohee: oobabooga's textgen always appends newline before the sequence as a stopping string
 | 
						|
        // But it's a problem for Metharme which doesn't use newlines to separate them.
 | 
						|
        const wrap = (s) => power_user.instruct.wrap ? '\n' + s : s;
 | 
						|
        // Sequence must be a non-empty string
 | 
						|
        if (typeof sequence === 'string' && sequence.length > 0) {
 | 
						|
            // If sequence is just a whitespace or newline - we don't want to make it a stopping string
 | 
						|
            // User can always add it as a custom stop string if really needed
 | 
						|
            if (sequence.trim().length > 0) {
 | 
						|
                const wrappedSequence = wrap(sequence);
 | 
						|
                // Need to respect "insert macro" setting
 | 
						|
                const stopString = power_user.instruct.macro ? substituteParams(wrappedSequence) : wrappedSequence;
 | 
						|
                result.push(stopString);
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    const result = [];
 | 
						|
 | 
						|
    if (power_user.instruct.enabled) {
 | 
						|
        const input_sequence = power_user.instruct.input_sequence;
 | 
						|
        const output_sequence = power_user.instruct.output_sequence;
 | 
						|
        const first_output_sequence = power_user.instruct.first_output_sequence;
 | 
						|
        const last_output_sequence = power_user.instruct.last_output_sequence;
 | 
						|
 | 
						|
        const combined_sequence = `${input_sequence}\n${output_sequence}\n${first_output_sequence}\n${last_output_sequence}`;
 | 
						|
 | 
						|
        combined_sequence.split('\n').filter((line, index, self) => self.indexOf(line) === index).forEach(addInstructSequence);
 | 
						|
    }
 | 
						|
 | 
						|
    return result;
 | 
						|
}
 | 
						|
 | 
						|
export const force_output_sequence = {
 | 
						|
    FIRST: 1,
 | 
						|
    LAST: 2,
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Formats instruct mode chat message.
 | 
						|
 * @param {string} name Character name.
 | 
						|
 * @param {string} mes Message text.
 | 
						|
 * @param {boolean} isUser Is the message from the user.
 | 
						|
 * @param {boolean} isNarrator Is the message from the narrator.
 | 
						|
 * @param {string} forceAvatar Force avatar string.
 | 
						|
 * @param {string} name1 User name.
 | 
						|
 * @param {string} name2 Character name.
 | 
						|
 * @param {boolean|number} forceOutputSequence Force to use first/last output sequence (if configured).
 | 
						|
 * @returns {string} Formatted instruct mode chat message.
 | 
						|
 */
 | 
						|
export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvatar, name1, name2, forceOutputSequence) {
 | 
						|
    let includeNames = isNarrator ? false : power_user.instruct.names;
 | 
						|
 | 
						|
    if (!isNarrator && power_user.instruct.names_force_groups && (selected_group || forceAvatar)) {
 | 
						|
        includeNames = true;
 | 
						|
    }
 | 
						|
 | 
						|
    let sequence = (isUser || isNarrator) ? power_user.instruct.input_sequence : power_user.instruct.output_sequence;
 | 
						|
 | 
						|
    if (forceOutputSequence && sequence === power_user.instruct.output_sequence) {
 | 
						|
        if (forceOutputSequence === force_output_sequence.FIRST && power_user.instruct.first_output_sequence) {
 | 
						|
            sequence = power_user.instruct.first_output_sequence;
 | 
						|
        } else if (forceOutputSequence === force_output_sequence.LAST && power_user.instruct.last_output_sequence) {
 | 
						|
            sequence = power_user.instruct.last_output_sequence;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    if (power_user.instruct.macro) {
 | 
						|
        sequence = substituteParams(sequence, name1, name2);
 | 
						|
    }
 | 
						|
 | 
						|
    const separator = power_user.instruct.wrap ? '\n' : '';
 | 
						|
    const separatorSequence = power_user.instruct.separator_sequence && !isUser
 | 
						|
        ? power_user.instruct.separator_sequence
 | 
						|
        : separator;
 | 
						|
    const textArray = includeNames ? [sequence, `${name}: ${mes}` + separatorSequence] : [sequence, mes + separatorSequence];
 | 
						|
    const text = textArray.filter(x => x).join(separator);
 | 
						|
    return text;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Formats instruct mode system prompt.
 | 
						|
 * @param {string} systemPrompt System prompt string.
 | 
						|
 * @returns {string} Formatted instruct mode system prompt.
 | 
						|
 */
 | 
						|
export function formatInstructModeSystemPrompt(systemPrompt){
 | 
						|
    const separator = power_user.instruct.wrap ? '\n' : '';
 | 
						|
 | 
						|
    if (power_user.instruct.system_sequence_prefix) {
 | 
						|
        systemPrompt = power_user.instruct.system_sequence_prefix + separator + systemPrompt;
 | 
						|
    }
 | 
						|
 | 
						|
    if (power_user.instruct.system_sequence_suffix) {
 | 
						|
        systemPrompt = systemPrompt + separator + power_user.instruct.system_sequence_suffix;
 | 
						|
    }
 | 
						|
 | 
						|
    return systemPrompt;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Formats example messages according to instruct mode settings.
 | 
						|
 * @param {string} mesExamples Example messages string.
 | 
						|
 * @param {string} name1 User name.
 | 
						|
 * @param {string} name2 Character name.
 | 
						|
 * @returns {string} Formatted example messages string.
 | 
						|
 */
 | 
						|
export function formatInstructModeExamples(mesExamples, name1, name2) {
 | 
						|
    const includeNames = power_user.instruct.names || (!!selected_group && power_user.instruct.names_force_groups);
 | 
						|
 | 
						|
    let inputSequence = power_user.instruct.input_sequence;
 | 
						|
    let outputSequence = power_user.instruct.output_sequence;
 | 
						|
 | 
						|
    if (power_user.instruct.macro) {
 | 
						|
        inputSequence = substituteParams(inputSequence, name1, name2);
 | 
						|
        outputSequence = substituteParams(outputSequence, name1, name2);
 | 
						|
    }
 | 
						|
 | 
						|
    const separator = power_user.instruct.wrap ? '\n' : '';
 | 
						|
    const separatorSequence = power_user.instruct.separator_sequence ? power_user.instruct.separator_sequence : separator;
 | 
						|
 | 
						|
    mesExamples = mesExamples.replace(new RegExp(`\n${name1}: `, "gm"), separatorSequence + inputSequence + separator + (includeNames ? `${name1}: ` : ''));
 | 
						|
    mesExamples = mesExamples.replace(new RegExp(`\n${name2}: `, "gm"), separator + outputSequence + separator + (includeNames ? `${name2}: ` : ''));
 | 
						|
 | 
						|
    return mesExamples;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Formats instruct mode last prompt line.
 | 
						|
 * @param {string} name Character name.
 | 
						|
 * @param {boolean} isImpersonate Is generation in impersonation mode.
 | 
						|
 * @param {string} promptBias Prompt bias string.
 | 
						|
 * @param {string} name1 User name.
 | 
						|
 * @param {string} name2 Character name.
 | 
						|
 * @returns {string} Formatted instruct mode last prompt line.
 | 
						|
 */
 | 
						|
export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2) {
 | 
						|
    const includeNames = power_user.instruct.names || (!!selected_group && power_user.instruct.names_force_groups);
 | 
						|
    const getOutputSequence = () => power_user.instruct.last_output_sequence || power_user.instruct.output_sequence;
 | 
						|
    let sequence = isImpersonate ? power_user.instruct.input_sequence : getOutputSequence();
 | 
						|
 | 
						|
    if (power_user.instruct.macro) {
 | 
						|
        sequence = substituteParams(sequence, name1, name2);
 | 
						|
    }
 | 
						|
 | 
						|
    const separator = power_user.instruct.wrap ? '\n' : '';
 | 
						|
    let text = includeNames ? (separator + sequence + separator + `${name}:`) : (separator + sequence);
 | 
						|
 | 
						|
    if (!isImpersonate && promptBias) {
 | 
						|
        text += (includeNames ? promptBias : (separator + promptBias));
 | 
						|
    }
 | 
						|
 | 
						|
    return text.trimEnd() + (includeNames ? '' : separator);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Select context template matching instruct preset.
 | 
						|
 * @param {string} name Preset name.
 | 
						|
 */
 | 
						|
function selectMatchingContextTemplate(name) {
 | 
						|
    let foundMatch = false;
 | 
						|
    for (const context_preset of context_presets) {
 | 
						|
        // If context template matches the instruct preset
 | 
						|
        if (context_preset.name === name) {
 | 
						|
            foundMatch = true;
 | 
						|
            selectContextPreset(context_preset.name);
 | 
						|
            break;
 | 
						|
        }
 | 
						|
    }
 | 
						|
    if (!foundMatch) {
 | 
						|
        // If no match was found, select default context preset
 | 
						|
        selectContextPreset(power_user.default_context);
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
jQuery(() => {
 | 
						|
    $('#instruct_set_default').on('click', function () {
 | 
						|
        if (power_user.instruct.preset === power_user.default_instruct) {
 | 
						|
            power_user.default_instruct = null;
 | 
						|
            $(this).removeClass('default');
 | 
						|
            toastr.info('Default instruct preset cleared');
 | 
						|
        } else {
 | 
						|
            power_user.default_instruct = power_user.instruct.preset;
 | 
						|
            $(this).addClass('default');
 | 
						|
            toastr.info(`Default instruct preset set to ${power_user.default_instruct}`);
 | 
						|
        }
 | 
						|
 | 
						|
        saveSettingsDebounced();
 | 
						|
    });
 | 
						|
 | 
						|
    $('#instruct_enabled').on('change', function () {
 | 
						|
        // When instruct mode gets enabled, select context template matching selected instruct preset
 | 
						|
        if (power_user.instruct.enabled) {
 | 
						|
            selectMatchingContextTemplate(power_user.instruct.preset);
 | 
						|
        // When instruct mode gets disabled, select default context preset
 | 
						|
        } else {
 | 
						|
            selectContextPreset(power_user.default_context);
 | 
						|
        }
 | 
						|
    });
 | 
						|
 | 
						|
    $('#instruct_presets').on('change', function () {
 | 
						|
        const name = String($(this).find(':selected').val());
 | 
						|
        const preset = instruct_presets.find(x => x.name === name);
 | 
						|
 | 
						|
        if (!preset) {
 | 
						|
            return;
 | 
						|
        }
 | 
						|
 | 
						|
        power_user.instruct.preset = String(name);
 | 
						|
        controls.forEach(control => {
 | 
						|
            if (preset[control.property] !== undefined) {
 | 
						|
                power_user.instruct[control.property] = preset[control.property];
 | 
						|
                const $element = $(`#${control.id}`);
 | 
						|
 | 
						|
                if (control.isCheckbox) {
 | 
						|
                    $element.prop('checked', power_user.instruct[control.property]).trigger('input');
 | 
						|
                } else {
 | 
						|
                    $element.val(power_user.instruct[control.property]).trigger('input');
 | 
						|
                }
 | 
						|
            }
 | 
						|
        });
 | 
						|
 | 
						|
        // Select matching context template
 | 
						|
        selectMatchingContextTemplate(name);
 | 
						|
 | 
						|
        highlightDefaultPreset();
 | 
						|
    });
 | 
						|
});
 |