Blame Raw
Cohee · 51ad27fb · · 870 lines (33.9 KB)
2 contributors
1'use strict';
2
3import { extension_prompt_types, name1, name2, online_status, saveSettingsDebounced, substituteParams } from '../script.js';
4import { selected_group } from './group-chats.js';
5import { parseExampleIntoIndividual } from './openai.js';
6import {
7 power_user,
8 context_presets,
9} from './power-user.js';
10import { onlyUnique, regexFromString, resetScrollHeight } from './utils.js';
11
12/**
13 * @type {InstructSettings[]} Instruct mode presets.
14 */
15export let instruct_presets = [];
16
17export const names_behavior_types = {
18 NONE: 'none',
19 FORCE: 'force',
20 ALWAYS: 'always',
21};
22
23const controls = [
24 { id: 'instruct_enabled', property: 'enabled', isCheckbox: true },
25 { id: 'instruct_wrap', property: 'wrap', isCheckbox: true },
26 { id: 'instruct_macro', property: 'macro', isCheckbox: true },
27 { id: 'instruct_story_string_prefix', property: 'story_string_prefix', isCheckbox: false },
28 { id: 'instruct_story_string_suffix', property: 'story_string_suffix', isCheckbox: false },
29 { id: 'instruct_input_sequence', property: 'input_sequence', isCheckbox: false },
30 { id: 'instruct_input_suffix', property: 'input_suffix', isCheckbox: false },
31 { id: 'instruct_output_sequence', property: 'output_sequence', isCheckbox: false },
32 { id: 'instruct_output_suffix', property: 'output_suffix', isCheckbox: false },
33 { id: 'instruct_system_sequence', property: 'system_sequence', isCheckbox: false },
34 { id: 'instruct_system_suffix', property: 'system_suffix', isCheckbox: false },
35 { id: 'instruct_last_system_sequence', property: 'last_system_sequence', isCheckbox: false },
36 { id: 'instruct_user_alignment_message', property: 'user_alignment_message', isCheckbox: false },
37 { id: 'instruct_stop_sequence', property: 'stop_sequence', isCheckbox: false },
38 { id: 'instruct_first_output_sequence', property: 'first_output_sequence', isCheckbox: false },
39 { id: 'instruct_last_output_sequence', property: 'last_output_sequence', isCheckbox: false },
40 { id: 'instruct_first_input_sequence', property: 'first_input_sequence', isCheckbox: false },
41 { id: 'instruct_last_input_sequence', property: 'last_input_sequence', isCheckbox: false },
42 { id: 'instruct_activation_regex', property: 'activation_regex', isCheckbox: false },
43 { id: 'instruct_bind_to_context', property: 'bind_to_context', isCheckbox: true },
44 { id: 'instruct_skip_examples', property: 'skip_examples', isCheckbox: true },
45 { id: 'instruct_names_behavior', property: 'names_behavior', isCheckbox: false },
46 { id: 'instruct_system_same_as_user', property: 'system_same_as_user', isCheckbox: true, trigger: true },
47 { id: 'instruct_sequences_as_stop_strings', property: 'sequences_as_stop_strings', isCheckbox: true },
48];
49
50/**
51 * Migrates instruct mode settings into the evergreen format.
52 * @param {object} settings Instruct mode settings.
53 * @returns {void}
54 */
55function migrateInstructModeSettings(settings) {
56 // Separator sequence => Output suffix
57 if (settings.separator_sequence !== undefined) {
58 settings.output_suffix = settings.separator_sequence || '';
59 delete settings.separator_sequence;
60 }
61
62 // names, names_force_groups => names_behavior
63 if (settings.names !== undefined) {
64 settings.names_behavior = settings.names
65 ? names_behavior_types.ALWAYS
66 : (settings.names_force_groups ? names_behavior_types.FORCE : names_behavior_types.NONE);
67 delete settings.names;
68 delete settings.names_force_groups;
69 }
70
71 const defaults = {
72 input_suffix: '',
73 system_sequence: '',
74 system_suffix: '',
75 user_alignment_message: '',
76 last_system_sequence: '',
77 first_input_sequence: '',
78 last_input_sequence: '',
79 skip_examples: false,
80 system_same_as_user: false,
81 names_behavior: names_behavior_types.FORCE,
82 sequences_as_stop_strings: true,
83 story_string_prefix: '',
84 story_string_suffix: '',
85 };
86
87 for (let key in defaults) {
88 if (settings[key] === undefined) {
89 settings[key] = defaults[key];
90 }
91 }
92
93 const obsoleteFields = [
94 'names',
95 'names_force_groups',
96 'system_sequence_prefix',
97 'system_sequence_suffix',
98 ];
99
100 for (const field of obsoleteFields) {
101 if (Object.hasOwn(settings, field)) {
102 delete settings[field];
103 }
104 }
105}
106
107/**
108 * Loads instruct mode settings from the given data object.
109 * @param {object} data Settings data object.
110 */
111export async function loadInstructMode(data) {
112 if (data.instruct !== undefined) {
113 instruct_presets = data.instruct;
114 }
115
116 migrateInstructModeSettings(power_user.instruct);
117
118 $('#instruct_enabled').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct.enabled);
119 $('#instructSettingsBlock, #InstructSequencesColumn').toggleClass('disabled', !power_user.instruct.enabled);
120 $('#instruct_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct_derived);
121 $('#instruct_bind_to_context').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct.bind_to_context);
122
123 controls.forEach(control => {
124 const $element = $(`#${control.id}`);
125
126 if (control.isCheckbox) {
127 $element.prop('checked', power_user.instruct[control.property]);
128 } else if ($element.is('select')) {
129 const value = power_user.instruct[control.property];
130 $element.val(value);
131 $element.filter(`[value="${value}"]`).prop('checked', true);
132 } else {
133 $element.val(power_user.instruct[control.property]);
134 }
135
136 $element.on('input', async function () {
137 power_user.instruct[control.property] = control.isCheckbox ? !!$(this).prop('checked') : $(this).val();
138 if (!CSS.supports('field-sizing', 'content') && $(this).is('textarea')) {
139 await resetScrollHeight($(this));
140 }
141 saveSettingsDebounced();
142 });
143
144 if (control.trigger) {
145 $element.trigger('input');
146 }
147 });
148
149 instruct_presets.forEach((preset) => {
150 const name = preset.name;
151 const option = document.createElement('option');
152 option.value = name;
153 option.innerText = name;
154 option.selected = name === power_user.instruct.preset;
155 $('#instruct_presets').append(option);
156 });
157}
158
159/**
160 * Updates the bind model template state based on the current model, instruct and context preset.
161 */
162export function updateBindModelTemplatesState() {
163 const bindModelTemplates = power_user.model_templates_mappings[online_status] ?? power_user.model_templates_mappings[power_user.chat_template_hash];
164 const bindingsMatch = (bindModelTemplates && power_user.context.preset === bindModelTemplates.context && (!power_user.instruct.enabled || power_user.instruct.preset === bindModelTemplates.instruct)) ?? false;
165 const currentState = $('#bind_model_templates').prop('checked');
166 if (bindingsMatch === currentState) {
167 // No change needed
168 return;
169 }
170 $('#bind_model_templates').prop('checked', bindingsMatch);
171}
172
173/**
174 * Select context template if not already selected.
175 * @param {string} preset Preset name.
176 * @param {object} [options={}] Optional arguments.
177 * @param {boolean} [options.quiet=false] Suppress toast messages.
178 * @param {boolean} [options.isAuto=false] Is auto-select.
179 */
180export function selectContextPreset(preset, { quiet = false, isAuto = false } = {}) {
181 const presetExists = context_presets.some(x => x.name === preset);
182 if (!presetExists) {
183 console.warn(`Context template "${preset}" not found`);
184 return;
185 }
186
187 // If context template is not already selected, select it
188 if (preset !== power_user.context.preset) {
189 $('#context_presets').val(preset).trigger('change');
190 !quiet && toastr.info(`Context Template: "${preset}" ${isAuto ? 'auto-' : ''}selected`);
191 }
192
193 updateBindModelTemplatesState();
194
195 saveSettingsDebounced();
196}
197
198/**
199 * Select instruct preset if not already selected.
200 * @param {string} preset Preset name.
201 * @param {object} [options={}] Optional arguments.
202 * @param {boolean} [options.quiet=false] Suppress toast messages.
203 * @param {boolean} [options.isAuto=false] Is auto-select.
204 */
205export function selectInstructPreset(preset, { quiet = false, isAuto = false } = {}) {
206 const presetExists = instruct_presets.some(x => x.name === preset);
207 if (!presetExists) {
208 console.warn(`Instruct template "${preset}" not found`);
209 return;
210 }
211
212 // If instruct preset is not already selected, select it
213 if (preset !== power_user.instruct.preset) {
214 $('#instruct_presets').val(preset).trigger('change');
215 !quiet && toastr.info(`Instruct Template: "${preset}" ${isAuto ? 'auto-' : ''}selected`);
216 }
217
218 // If instruct mode is disabled, enable it
219 if (!power_user.instruct.enabled) {
220 power_user.instruct.enabled = true;
221 $('#instruct_enabled').prop('checked', true).trigger('change');
222 !quiet && toastr.info('Instruct Mode enabled');
223 }
224
225 updateBindModelTemplatesState();
226
227 saveSettingsDebounced();
228}
229
230/**
231 * Automatically select instruct preset based on model id.
232 * Otherwise, if default instruct preset is set, selects it.
233 * @param {string} modelId Model name reported by the API.
234 * @returns {boolean} True if instruct preset was activated by model id, false otherwise.
235 */
236export function autoSelectInstructPreset(modelId) {
237 const modelTemplatesMap = power_user.model_templates_mappings[modelId];
238
239 if (modelTemplatesMap) {
240 const { instruct, context } = modelTemplatesMap;
241 if (instruct) {
242 selectInstructPreset(instruct, { isAuto: true });
243 }
244 if (context) {
245 selectContextPreset(context, { isAuto: true });
246 }
247 return true;
248 } else {
249 updateBindModelTemplatesState();
250 }
251
252 // If instruct mode is disabled, don't do anything
253 if (!power_user.instruct.enabled) {
254 return false;
255 }
256
257 // Select matching instruct preset
258 let foundMatch = false;
259
260 for (const preset of instruct_presets) {
261 // If activation regex is set, check if it matches the model id
262 if (preset.activation_regex) {
263 try {
264 const regex = regexFromString(preset.activation_regex);
265
266 // Stop on first match so it won't cycle back and forth between presets if multiple regexes match
267 if (regex instanceof RegExp && regex.test(modelId)) {
268 selectInstructPreset(preset.name, { isAuto: true });
269 foundMatch = true;
270 break;
271 }
272 } catch {
273 // If regex is invalid, ignore it
274 console.warn(`Invalid instruct activation regex in preset "${preset.name}"`);
275 }
276 }
277 }
278
279 // If no match was found, auto-select instruct preset
280 if (!foundMatch && power_user.instruct.bind_to_context) {
281 for (const instruct_preset of instruct_presets) {
282 // If instruct preset matches the context template
283 if (instruct_preset.name === power_user.context.preset) {
284 selectInstructPreset(instruct_preset.name, { isAuto: true });
285 foundMatch = true;
286 break;
287 }
288 }
289 }
290
291 return foundMatch;
292}
293
294/**
295 * Converts instruct mode sequences to an array of stopping strings.
296 * @param {Object} options
297 * @param {InstructSettings?} [options.customInstruct=null] - Custom instruct settings.
298 * @param {boolean?} [options.useStopStrings] - Decides whether to use "Chat Start" and "Example Separator"
299 * @returns {string[]} Array of instruct mode stopping strings.
300 */
301export function getInstructStoppingSequences({ customInstruct = null, useStopStrings = null } = {}) {
302 const instruct = structuredClone(customInstruct ?? power_user.instruct);
303
304 /**
305 * Adds instruct mode sequence to the result array.
306 * @param {string} sequence Sequence string.
307 * @returns {void}
308 */
309 function addInstructSequence(sequence) {
310 // Cohee: oobabooga's textgen always appends newline before the sequence as a stopping string
311 // But it's a problem for Metharme which doesn't use newlines to separate them.
312 const wrap = (s) => instruct.wrap ? '\n' + s : s;
313 // Sequence must be a non-empty string
314 if (typeof sequence === 'string' && sequence.length > 0) {
315 // If sequence is just a whitespace or newline - we don't want to make it a stopping string
316 // User can always add it as a custom stop string if really needed
317 if (sequence.trim().length > 0) {
318 const wrappedSequence = wrap(sequence);
319 // Need to respect "insert macro" setting
320 const stopString = instruct.macro ? substituteParams(wrappedSequence) : wrappedSequence;
321 result.push(stopString);
322 }
323 }
324 }
325
326 const result = [];
327
328 // Since preset's don't have "enabled", we assume it's always enabled
329 if (customInstruct ?? instruct.enabled) {
330 const stop_sequence = instruct.stop_sequence || '';
331 const input_sequence = instruct.input_sequence?.replace(/{{name}}/gi, name1) || '';
332 const output_sequence = instruct.output_sequence?.replace(/{{name}}/gi, name2) || '';
333 const first_output_sequence = instruct.first_output_sequence?.replace(/{{name}}/gi, name2) || '';
334 const last_output_sequence = instruct.last_output_sequence?.replace(/{{name}}/gi, name2) || '';
335 const system_sequence = instruct.system_sequence?.replace(/{{name}}/gi, 'System') || '';
336 const last_system_sequence = instruct.last_system_sequence?.replace(/{{name}}/gi, 'System') || '';
337
338 const combined_sequence = [
339 stop_sequence,
340 ];
341
342 if (instruct.sequences_as_stop_strings) {
343 combined_sequence.push(
344 input_sequence,
345 output_sequence,
346 first_output_sequence,
347 last_output_sequence,
348 system_sequence,
349 last_system_sequence,
350 );
351 }
352
353 combined_sequence.join('\n').split('\n').filter(onlyUnique).forEach(addInstructSequence);
354 }
355
356 if (useStopStrings ?? power_user.context.use_stop_strings) {
357 if (power_user.context.chat_start) {
358 result.push(`\n${substituteParams(power_user.context.chat_start)}`);
359 }
360
361 if (power_user.context.example_separator) {
362 result.push(`\n${substituteParams(power_user.context.example_separator)}`);
363 }
364 }
365
366 return result;
367}
368
369export const force_output_sequence = {
370 FIRST: 1,
371 LAST: 2,
372};
373
374/**
375 * Formats instruct mode chat message.
376 * @param {string} name Character name.
377 * @param {string} mes Message text.
378 * @param {boolean} isUser Is the message from the user.
379 * @param {boolean} isNarrator Is the message from the narrator.
380 * @param {string} forceAvatar Force avatar string.
381 * @param {string} name1 User name.
382 * @param {string} name2 Character name.
383 * @param {boolean|number} forceOutputSequence Force to use first/last output sequence (if configured).
384 * @param {InstructSettings} customInstruct Custom instruct mode settings.
385 * @returns {string} Formatted instruct mode chat message.
386 */
387export function formatInstructModeChat(name, mes, isUser, isNarrator, forceAvatar, name1, name2, forceOutputSequence, customInstruct = null) {
388 const instruct = structuredClone(customInstruct ?? power_user.instruct);
389 let includeNames = isNarrator ? false : instruct.names_behavior === names_behavior_types.ALWAYS;
390
391 if (!isNarrator && instruct.names_behavior === names_behavior_types.FORCE && ((selected_group && name !== name1) || (forceAvatar && name !== name1))) {
392 includeNames = true;
393 }
394
395 function getPrefix() {
396 if (isNarrator) {
397 return instruct.system_same_as_user ? instruct.input_sequence : instruct.system_sequence;
398 }
399
400 if (isUser) {
401 if (forceOutputSequence === force_output_sequence.FIRST) {
402 return instruct.first_input_sequence || instruct.input_sequence;
403 }
404
405 if (forceOutputSequence === force_output_sequence.LAST) {
406 return instruct.last_input_sequence || instruct.input_sequence;
407 }
408
409 return instruct.input_sequence;
410 }
411
412 if (forceOutputSequence === force_output_sequence.FIRST) {
413 return instruct.first_output_sequence || instruct.output_sequence;
414 }
415
416 if (forceOutputSequence === force_output_sequence.LAST) {
417 return instruct.last_output_sequence || instruct.output_sequence;
418 }
419
420 return instruct.output_sequence;
421 }
422
423 function getSuffix() {
424 if (isNarrator) {
425 return instruct.system_same_as_user ? instruct.input_suffix : instruct.system_suffix;
426 }
427
428 if (isUser) {
429 return instruct.input_suffix;
430 }
431
432 return instruct.output_suffix;
433 }
434
435 let prefix = getPrefix() || '';
436 let suffix = getSuffix() || '';
437
438 if (instruct.macro) {
439 prefix = substituteParams(prefix, { name1Override: name1, name2Override: name2 });
440 prefix = prefix.replace(/{{name}}/gi, name || 'System');
441
442 suffix = substituteParams(suffix, { name1Override: name1, name2Override: name2 });
443 suffix = suffix.replace(/{{name}}/gi, name || 'System');
444 }
445
446 if (!suffix && instruct.wrap) {
447 suffix = '\n';
448 }
449
450 const separator = instruct.wrap ? '\n' : '';
451
452 // Don't include the name if it's empty
453 const textArray = includeNames && name ? [prefix, `${name}: ${mes}` + suffix] : [prefix, mes + suffix];
454 const text = textArray.filter(x => x).join(separator);
455
456 return text;
457}
458
459/**
460 * Formats instruct mode system prompt.
461 * @param {string} systemPrompt System prompt string.
462 * @param {InstructSettings} _customInstruct Custom instruct mode settings.
463 * @returns {string} Formatted instruct mode system prompt.
464 * @deprecated Currently doesn't do anything useful.
465 */
466export function formatInstructModeSystemPrompt(systemPrompt, _customInstruct = null) {
467 return systemPrompt || '';
468}
469
470/**
471 * Formats instruct mode story string.
472 * @param {string} storyString Story string and anchors
473 * @param {object} [params]
474 * @param {ContextSettings} [params.customContext] Custom context settings.
475 * @param {InstructSettings} [params.customInstruct] Custom instruct mode settings.
476 * @returns {string} Formatted instruct mode story string.
477 */
478export function formatInstructModeStoryString(storyString, { customContext = null, customInstruct = null } = {}) {
479 if (!storyString) {
480 return '';
481 }
482
483 const instructSettings = structuredClone(customInstruct ?? power_user.instruct);
484 const contextSettings = structuredClone(customContext ?? power_user.context);
485 const storyStringPosition = contextSettings.story_string_position ?? extension_prompt_types.IN_PROMPT;
486
487 // Only wrap if not in-chat position (it will be wrapped by message sequences instead)
488 const applySequences = storyStringPosition !== extension_prompt_types.IN_CHAT;
489 const separator = instructSettings.wrap ? '\n' : '';
490 if (applySequences && instructSettings.story_string_prefix) {
491 // TODO: Replace with a proper 'System' prompt entity name input
492 const prefix = substituteParams(instructSettings.story_string_prefix).replace(/{{name}}/gi, 'System');
493 storyString = prefix + separator + storyString;
494 }
495
496 if (applySequences && instructSettings.story_string_suffix) {
497 const suffix = substituteParams(instructSettings.story_string_suffix);
498 storyString = storyString + suffix;
499 }
500
501 return storyString;
502}
503
504/**
505 * Formats example messages according to instruct mode settings.
506 * @param {string[]} mesExamplesArray Example messages array.
507 * @param {string} name1 User name.
508 * @param {string} name2 Character name.
509 * @returns {string[]} Formatted example messages string.
510 */
511export function formatInstructModeExamples(mesExamplesArray, name1, name2) {
512 const blockHeading = power_user.context.example_separator ? `${substituteParams(power_user.context.example_separator)}\n` : '';
513
514 if (power_user.instruct.skip_examples) {
515 return mesExamplesArray.map(x => x.replace(/<START>\n/i, blockHeading));
516 }
517
518 const includeNames = power_user.instruct.names_behavior === names_behavior_types.ALWAYS;
519 const includeGroupNames = selected_group && [names_behavior_types.ALWAYS, names_behavior_types.FORCE].includes(power_user.instruct.names_behavior);
520
521 let inputPrefix = power_user.instruct.input_sequence || '';
522 let outputPrefix = power_user.instruct.output_sequence || '';
523 let inputSuffix = power_user.instruct.input_suffix || '';
524 let outputSuffix = power_user.instruct.output_suffix || '';
525
526 if (power_user.instruct.macro) {
527 inputPrefix = substituteParams(inputPrefix, { name1Override: name1, name2Override: name2 });
528 outputPrefix = substituteParams(outputPrefix, { name1Override: name1, name2Override: name2 });
529 inputSuffix = substituteParams(inputSuffix, { name1Override: name1, name2Override: name2 });
530 outputSuffix = substituteParams(outputSuffix, { name1Override: name1, name2Override: name2 });
531
532 inputPrefix = inputPrefix.replace(/{{name}}/gi, name1);
533 outputPrefix = outputPrefix.replace(/{{name}}/gi, name2);
534 inputSuffix = inputSuffix.replace(/{{name}}/gi, name1);
535 outputSuffix = outputSuffix.replace(/{{name}}/gi, name2);
536
537 if (!inputSuffix && power_user.instruct.wrap) {
538 inputSuffix = '\n';
539 }
540
541 if (!outputSuffix && power_user.instruct.wrap) {
542 outputSuffix = '\n';
543 }
544 }
545
546 const separator = power_user.instruct.wrap ? '\n' : '';
547 const formattedExamples = [];
548
549 for (const item of mesExamplesArray) {
550 const cleanedItem = item.replace(/<START>/i, '{Example Dialogue:}').replace(/\r/gm, '');
551 const blockExamples = parseExampleIntoIndividual(cleanedItem, includeGroupNames);
552
553 if (blockExamples.length === 0) {
554 continue;
555 }
556
557 if (blockHeading) {
558 formattedExamples.push(blockHeading);
559 }
560
561 for (const example of blockExamples) {
562 // If group names were included, we don't want to add any additional prefix as it already was applied.
563 // Otherwise, if force group/persona names is set, we should override the include names for the user placeholder
564 const includeThisName = !includeGroupNames && (includeNames || (power_user.instruct.names_behavior === names_behavior_types.FORCE && example.name == 'example_user'));
565
566 const prefix = example.name == 'example_user' ? inputPrefix : outputPrefix;
567 const suffix = example.name == 'example_user' ? inputSuffix : outputSuffix;
568 const name = example.name == 'example_user' ? name1 : name2;
569 const messageContent = includeThisName ? `${name}: ${example.content}` : example.content;
570 const formattedMessage = [prefix, messageContent + suffix].filter(x => x).join(separator);
571 formattedExamples.push(formattedMessage);
572 }
573 }
574
575 if (formattedExamples.length === 0) {
576 return mesExamplesArray.map(x => x.replace(/<START>\n/i, blockHeading));
577 }
578 return formattedExamples;
579}
580
581/**
582 * Formats instruct mode last prompt line.
583 * @param {string} name Character name.
584 * @param {boolean} isImpersonate Is generation in impersonation mode.
585 * @param {string} promptBias Prompt bias string.
586 * @param {string} name1 User name.
587 * @param {string} name2 Character name.
588 * @param {boolean} isQuiet Is quiet mode generation.
589 * @param {boolean} isQuietToLoud Is quiet to loud generation.
590 * @param {InstructSettings} customInstruct Custom instruct settings.
591 * @returns {string} Formatted instruct mode last prompt line.
592 */
593export function formatInstructModePrompt(name, isImpersonate, promptBias, name1, name2, isQuiet, isQuietToLoud, customInstruct = null) {
594 const instruct = structuredClone(customInstruct ?? power_user.instruct);
595 const includeNames = name && (instruct.names_behavior === names_behavior_types.ALWAYS || (!!selected_group && instruct.names_behavior === names_behavior_types.FORCE)) && !(isQuiet && !isQuietToLoud);
596
597 function getSequence() {
598 // User impersonation prompt
599 if (isImpersonate) {
600 return instruct.last_input_sequence || instruct.input_sequence;
601 }
602
603 // Neutral / system / quiet prompt
604 // Use a special quiet instruct sequence if defined, or assistant's output sequence otherwise
605 if (isQuiet && !isQuietToLoud) {
606 return instruct.last_system_sequence || instruct.output_sequence;
607 }
608
609 // Quiet in-character prompt
610 if (isQuiet && isQuietToLoud) {
611 return instruct.last_output_sequence || instruct.output_sequence;
612 }
613
614 // Default AI response
615 return instruct.last_output_sequence || instruct.output_sequence;
616 }
617
618 let sequence = getSequence() || '';
619 let nameFiller = '';
620
621 // A hack for Mistral's formatting that has a normal output sequence ending with a space
622 if (
623 includeNames &&
624 instruct.last_output_sequence &&
625 instruct.output_sequence &&
626 sequence === instruct.last_output_sequence &&
627 /\s$/.test(instruct.output_sequence) &&
628 !/\s$/.test(instruct.last_output_sequence)
629 ) {
630 nameFiller = instruct.output_sequence.slice(-1);
631 }
632
633 if (instruct.macro) {
634 sequence = substituteParams(sequence, { name1Override: name1, name2Override: name2 });
635 sequence = sequence.replace(/{{name}}/gi, name || 'System');
636 }
637
638 const separator = instruct.wrap ? '\n' : '';
639 let text = includeNames ? (separator + sequence + separator + nameFiller + `${name}:`) : (separator + sequence);
640
641 // Quiet prompt already has a newline at the end
642 if (isQuiet && separator) {
643 text = text.slice(separator.length);
644 }
645
646 if (!isImpersonate && promptBias) {
647 text += (includeNames ? promptBias : (separator + promptBias.trimStart()));
648 }
649
650 return (instruct.wrap ? text.trimEnd() : text) + (includeNames ? '' : separator);
651}
652
653/**
654 * Select context template matching instruct preset.
655 * @param {string} name Preset name.
656 */
657function selectMatchingContextTemplate(name) {
658 for (const context_preset of context_presets) {
659 // If context template matches the instruct preset
660 if (context_preset.name === name) {
661 selectContextPreset(context_preset.name, { isAuto: true });
662 break;
663 }
664 }
665}
666
667/**
668 * Replaces instruct mode macros in the given input string.
669 * @param {Object<string, *>} env - Map of macro names to the values they'll be substituted with. If the param
670 * values are functions, those functions will be called and their return values are used.
671 * @returns {import('./macros.js').Macro[]} Macro objects.
672 */
673export function getInstructMacros(env) {
674 /** @type {{ key: string,value: string, enabled: boolean }[]} */
675 const instructMacros = [
676 // Instruct template macros
677 {
678 key: 'instructStoryStringPrefix',
679 value: power_user.instruct.story_string_prefix,
680 enabled: power_user.instruct.enabled,
681 },
682 {
683 key: 'instructStoryStringSuffix',
684 value: power_user.instruct.story_string_suffix,
685 enabled: power_user.instruct.enabled,
686 },
687 {
688 key: 'instructInput|instructUserPrefix',
689 value: power_user.instruct.input_sequence,
690 enabled: power_user.instruct.enabled,
691 },
692 {
693 key: 'instructUserSuffix',
694 value: power_user.instruct.input_suffix,
695 enabled: power_user.instruct.enabled,
696 },
697 {
698 key: 'instructOutput|instructAssistantPrefix',
699 value: power_user.instruct.output_sequence,
700 enabled: power_user.instruct.enabled,
701 },
702 {
703 key: 'instructSeparator|instructAssistantSuffix',
704 value: power_user.instruct.output_suffix,
705 enabled: power_user.instruct.enabled,
706 },
707 {
708 key: 'instructSystemPrefix',
709 value: power_user.instruct.system_sequence,
710 enabled: power_user.instruct.enabled,
711 },
712 {
713 key: 'instructSystemSuffix',
714 value: power_user.instruct.system_suffix,
715 enabled: power_user.instruct.enabled,
716 },
717 {
718 key: 'instructFirstOutput|instructFirstAssistantPrefix',
719 value: power_user.instruct.first_output_sequence || power_user.instruct.output_sequence,
720 enabled: power_user.instruct.enabled,
721 },
722 {
723 key: 'instructLastOutput|instructLastAssistantPrefix',
724 value: power_user.instruct.last_output_sequence || power_user.instruct.output_sequence,
725 enabled: power_user.instruct.enabled,
726 },
727 {
728 key: 'instructStop',
729 value: power_user.instruct.stop_sequence,
730 enabled: power_user.instruct.enabled,
731 },
732 {
733 key: 'instructUserFiller',
734 value: power_user.instruct.user_alignment_message,
735 enabled: power_user.instruct.enabled,
736 },
737 {
738 key: 'instructSystemInstructionPrefix',
739 value: power_user.instruct.last_system_sequence,
740 enabled: power_user.instruct.enabled,
741 },
742 {
743 key: 'instructFirstInput|instructFirstUserPrefix',
744 value: power_user.instruct.first_input_sequence || power_user.instruct.input_sequence,
745 enabled: power_user.instruct.enabled,
746 },
747 {
748 key: 'instructLastInput|instructLastUserPrefix',
749 value: power_user.instruct.last_input_sequence || power_user.instruct.input_sequence,
750 enabled: power_user.instruct.enabled,
751 },
752 // System prompt macros
753 {
754 key: 'systemPrompt',
755 value: power_user.prefer_character_prompt && env.charPrompt ? env.charPrompt : power_user.sysprompt.content,
756 enabled: power_user.sysprompt.enabled,
757 },
758 {
759 key: 'defaultSystemPrompt|instructSystem|instructSystemPrompt',
760 value: power_user.sysprompt.content,
761 enabled: power_user.sysprompt.enabled,
762 },
763 // Context template macros
764 {
765 key: 'chatSeparator',
766 value: power_user.context.example_separator,
767 enabled: true,
768 },
769 {
770 key: 'chatStart',
771 value: power_user.context.chat_start,
772 enabled: true,
773 },
774 ];
775
776 const macros = [];
777
778 for (const { key, value, enabled } of instructMacros) {
779 const regex = new RegExp(`{{(${key})}}`, 'gi');
780 const replace = () => enabled ? value : '';
781 macros.push({ regex, replace });
782 }
783
784 return macros;
785}
786
787jQuery(() => {
788 $('#instruct_system_same_as_user').on('input', function () {
789 const state = !!$(this).prop('checked');
790 if (state) {
791 $('#instruct_system_sequence_block').addClass('disabled');
792 $('#instruct_system_suffix_block').addClass('disabled');
793 $('#instruct_system_sequence').prop('readOnly', true);
794 $('#instruct_system_suffix').prop('readOnly', true);
795 } else {
796 $('#instruct_system_sequence_block').removeClass('disabled');
797 $('#instruct_system_suffix_block').removeClass('disabled');
798 $('#instruct_system_sequence').prop('readOnly', false);
799 $('#instruct_system_suffix').prop('readOnly', false);
800 }
801 });
802
803 $('#instruct_enabled').on('change', function () {
804 //color toggle for the main switch
805 $('#instruct_enabled').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct.enabled);
806 $('#instructSettingsBlock, #InstructSequencesColumn').toggleClass('disabled', !power_user.instruct.enabled);
807
808 if (!power_user.instruct.bind_to_context) {
809 return;
810 }
811
812 // When instruct mode gets enabled, select context template matching selected instruct preset
813 if (power_user.instruct.enabled) {
814 selectMatchingContextTemplate(power_user.instruct.preset);
815 }
816 });
817
818 $('#instruct_derived').on('change', function () {
819 $('#instruct_derived').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct_derived);
820 });
821
822 $('#instruct_bind_to_context').on('change', function () {
823 $('#instruct_bind_to_context').parent().find('i').toggleClass('toggleEnabled', !!power_user.instruct.bind_to_context);
824 });
825
826 $('#instruct_presets').on('change', function () {
827 const name = String($(this).find(':selected').val());
828 const preset = instruct_presets.find(x => x.name === name);
829
830 if (!preset) {
831 return;
832 }
833
834 migrateInstructModeSettings(preset);
835
836 power_user.instruct.preset = String(name);
837 controls.forEach(control => {
838 if (preset[control.property] !== undefined) {
839 power_user.instruct[control.property] = preset[control.property];
840 const $element = $(`#${control.id}`);
841
842 if (control.isCheckbox) {
843 $element.prop('checked', power_user.instruct[control.property]).trigger('input');
844 } else if ($element.is('select')) {
845 const value = power_user.instruct[control.property];
846 $element.val(value);
847 $element.filter(`[value="${value}"]`).prop('checked', true).trigger('input');
848 } else {
849 $element.val(power_user.instruct[control.property]);
850 $element.trigger('input');
851 }
852 }
853 });
854
855 if (power_user.instruct.bind_to_context) {
856 // Select matching context template
857 selectMatchingContextTemplate(name);
858 }
859
860 updateBindModelTemplatesState();
861 });
862
863 if (!CSS.supports('field-sizing', 'content')) {
864 $('#InstructSequencesColumn details').on('toggle', function () {
865 if ($(this).prop('open')) {
866 resetScrollHeight($(this).find('textarea'));
867 }
868 });
869 }
870});