Merge pull request #2901 from SillyTavern/connection-profile-omit Add ability to omit settings from Connection Profile
Signed| @@ -0,0 +1,12 @@ | |||
| 1 | <div> | ||
| 2 | <h3>Included settings:</h3> | ||
| 3 | <div class="justifyLeft flex-container flexFlowColumn flexNoGap"> | ||
| 4 | {{#each settings}} | ||
| 5 | <label class="checkbox_label"> | ||
| 6 | <input type="checkbox" value="{{@key}}" name="exclude"{{#if this}} checked{{/if}}> | ||
| 7 | <span>{{@key}}</span> | ||
| 8 | </label> | ||
| 9 | {{/each}} | ||
| 10 | </div> | ||
| 11 | <h3>Profile name:</h3> | ||
| 12 | </div> | ||
| @@ -1,6 +1,6 @@ | |||
| 1 | import { event_types, eventSource, main_api, saveSettingsDebounced } from '../../../script.js'; | 1 | import { event_types, eventSource, main_api, saveSettingsDebounced } from '../../../script.js'; |
| 2 | import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js'; | 2 | import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js'; |
| 3 | import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js'; | 3 | import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js'; |
| 4 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; | 4 | import { SlashCommand } from '../../slash-commands/SlashCommand.js'; |
| 5 | import { SlashCommandAbortController } from '../../slash-commands/SlashCommandAbortController.js'; | 5 | import { SlashCommandAbortController } from '../../slash-commands/SlashCommandAbortController.js'; |
| 6 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; | 6 | import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js'; |
| @@ -135,6 +135,7 @@ const profilesProvider = () => [ | |||
| 135 | * @property {string} [context] Context Template | 135 | * @property {string} [context] Context Template |
| 136 | * @property {string} [instruct-state] Instruct Mode | 136 | * @property {string} [instruct-state] Instruct Mode |
| 137 | * @property {string} [tokenizer] Tokenizer | 137 | * @property {string} [tokenizer] Tokenizer |
| 138 | * @property {string[]} [exclude] Commands to exclude | ||
| 138 | */ | 139 | */ |
| 139 | 140 | ||
| 140 | /** | 141 | /** |
| @@ -171,8 +172,13 @@ function findProfileByName(value) { | |||
| 171 | async function readProfileFromCommands(mode, profile, cleanUp = false) { | 172 | async function readProfileFromCommands(mode, profile, cleanUp = false) { |
| 172 | const commands = mode === 'cc' ? CC_COMMANDS : TC_COMMANDS; | 173 | const commands = mode === 'cc' ? CC_COMMANDS : TC_COMMANDS; |
| 173 | const opposingCommands = mode === 'cc' ? TC_COMMANDS : CC_COMMANDS; | 174 | const opposingCommands = mode === 'cc' ? TC_COMMANDS : CC_COMMANDS; |
| 175 | const excludeList = Array.isArray(profile.exclude) ? profile.exclude : []; | ||
| 174 | for (const command of commands) { | 176 | for (const command of commands) { |
| 175 | try { | 177 | try { |
| 178 | if (excludeList.includes(command)) { | ||
| 179 | continue; | ||
| 180 | } | ||
| 181 | |||
| 176 | const args = getNamedArguments(); | 182 | const args = getNamedArguments(); |
| 177 | const result = await SlashCommandParser.commands[command].callback(args, ''); | 183 | const result = await SlashCommandParser.commands[command].callback(args, ''); |
| 178 | if (result) { | 184 | if (result) { |
| @@ -208,15 +214,37 @@ async function readProfileFromCommands(mode, profile, cleanUp = false) { | |||
| 208 | async function createConnectionProfile(forceName = null) { | 214 | async function createConnectionProfile(forceName = null) { |
| 209 | const mode = main_api === 'openai' ? 'cc' : 'tc'; | 215 | const mode = main_api === 'openai' ? 'cc' : 'tc'; |
| 210 | const id = uuidv4(); | 216 | const id = uuidv4(); |
| 217 | /** @type {ConnectionProfile} */ | ||
| 211 | const profile = { | 218 | const profile = { |
| 212 | id, | 219 | id, |
| 213 | mode, | 220 | mode, |
| 221 | exclude: [], | ||
| 214 | }; | 222 | }; |
| 215 | 223 | ||
| 216 | await readProfileFromCommands(mode, profile); | 224 | await readProfileFromCommands(mode, profile); |
| 217 | 225 | ||
| 218 | const profileForDisplay = makeFancyProfile(profile); | 226 | const profileForDisplay = makeFancyProfile(profile); |
| 219 | const template = await renderExtensionTemplateAsync(MODULE_NAME, 'profile', { profile: profileForDisplay }); | 227 | const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'profile', { profile: profileForDisplay })); |
| 228 | template.find('input[name="exclude"]').on('input', function () { | ||
| 229 | const fancyName = String($(this).val()); | ||
| 230 | const keyName = Object.entries(FANCY_NAMES).find(x => x[1] === fancyName)?.[0]; | ||
| 231 | if (!keyName) { | ||
| 232 | console.warn('Key not found for fancy name:', fancyName); | ||
| 233 | return; | ||
| 234 | } | ||
| 235 | |||
| 236 | if (!Array.isArray(profile.exclude)) { | ||
| 237 | profile.exclude = []; | ||
| 238 | } | ||
| 239 | |||
| 240 | const excludeState = !$(this).prop('checked'); | ||
| 241 | if (excludeState) { | ||
| 242 | profile.exclude.push(keyName); | ||
| 243 | } else { | ||
| 244 | const index = profile.exclude.indexOf(keyName); | ||
| 245 | index !== -1 && profile.exclude.splice(index, 1); | ||
| 246 | } | ||
| 247 | }); | ||
| 220 | const isNameTaken = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n); | 248 | const isNameTaken = (n) => extension_settings.connectionManager.profiles.some(p => p.name === n); |
| 221 | const suggestedName = getUniqueName(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), isNameTaken); | 249 | const suggestedName = getUniqueName(collapseSpaces(`${profile.api ?? ''} ${profile.model ?? ''} - ${profile.preset ?? ''}`), isNameTaken); |
| 222 | const name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 }); | 250 | const name = forceName ?? await callGenericPopup(template, POPUP_TYPE.INPUT, suggestedName, { rows: 2 }); |
| @@ -230,7 +258,13 @@ async function createConnectionProfile(forceName = null) { | |||
| 230 | return null; | 258 | return null; |
| 231 | } | 259 | } |
| 232 | 260 | ||
| 233 | profile.name = name; | 261 | if (Array.isArray(profile.exclude)) { |
| 262 | for (const command of profile.exclude) { | ||
| 263 | delete profile[command]; | ||
| 264 | } | ||
| 265 | } | ||
| 266 | |||
| 267 | profile.name = String(name); | ||
| 234 | return profile; | 268 | return profile; |
| 235 | } | 269 | } |
| 236 | 270 | ||
| @@ -357,7 +391,11 @@ async function renderDetailsContent(detailsContent) { | |||
| 357 | const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile); | 391 | const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile); |
| 358 | if (profile) { | 392 | if (profile) { |
| 359 | const profileForDisplay = makeFancyProfile(profile); | 393 | const profileForDisplay = makeFancyProfile(profile); |
| 360 | const template = await renderExtensionTemplateAsync(MODULE_NAME, 'view', { profile: profileForDisplay }); | 394 | const templateParams = { profile: profileForDisplay }; |
| 395 | if (Array.isArray(profile.exclude) && profile.exclude.length > 0) { | ||
| 396 | templateParams.omitted = profile.exclude.map(e => FANCY_NAMES[e]).join(', '); | ||
| 397 | } | ||
| 398 | const template = await renderExtensionTemplateAsync(MODULE_NAME, 'view', templateParams); | ||
| 361 | detailsContent.innerHTML = template; | 399 | detailsContent.innerHTML = template; |
| 362 | } else { | 400 | } else { |
| 363 | detailsContent.textContent = 'No profile selected'; | 401 | detailsContent.textContent = 'No profile selected'; |
| @@ -472,8 +510,8 @@ async function renderDetailsContent(detailsContent) { | |||
| 472 | await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, NONE); | 510 | await eventSource.emit(event_types.CONNECTION_PROFILE_LOADED, NONE); |
| 473 | }); | 511 | }); |
| 474 | 512 | ||
| 475 | const renameButton = document.getElementById('rename_connection_profile'); | 513 | const editButton = document.getElementById('edit_connection_profile'); |
| 476 | renameButton.addEventListener('click', async () => { | 514 | editButton.addEventListener('click', async () => { |
| 477 | const selectedProfile = extension_settings.connectionManager.selectedProfile; | 515 | const selectedProfile = extension_settings.connectionManager.selectedProfile; |
| 478 | const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile); | 516 | const profile = extension_settings.connectionManager.profiles.find(p => p.id === selectedProfile); |
| 479 | if (!profile) { | 517 | if (!profile) { |
| @@ -481,20 +519,62 @@ async function renderDetailsContent(detailsContent) { | |||
| 481 | return; | 519 | return; |
| 482 | } | 520 | } |
| 483 | 521 | ||
| 484 | const newName = await Popup.show.input('Enter a new name', null, profile.name, { rows: 2 }); | 522 | if (!Array.isArray(profile.exclude)) { |
| 523 | profile.exclude = []; | ||
| 524 | } | ||
| 525 | |||
| 526 | let saveChanges = false; | ||
| 527 | const commands = profile.mode === 'cc' ? CC_COMMANDS : TC_COMMANDS; | ||
| 528 | const settings = commands.reduce((acc, command) => { | ||
| 529 | const fancyName = FANCY_NAMES[command]; | ||
| 530 | acc[fancyName] = !profile.exclude.includes(command); | ||
| 531 | return acc; | ||
| 532 | }, {}); | ||
| 533 | const template = $(await renderExtensionTemplateAsync(MODULE_NAME, 'edit', { name: profile.name, settings })); | ||
| 534 | const newName = await callGenericPopup(template, POPUP_TYPE.INPUT, profile.name, { | ||
| 535 | customButtons: [{ | ||
| 536 | text: 'Save and Update', | ||
| 537 | classes: ['popup-button-ok'], | ||
| 538 | result: POPUP_RESULT.AFFIRMATIVE, | ||
| 539 | action: () => { | ||
| 540 | saveChanges = true; | ||
| 541 | }, | ||
| 542 | }], | ||
| 543 | }); | ||
| 544 | |||
| 485 | if (!newName) { | 545 | if (!newName) { |
| 486 | return; | 546 | return; |
| 487 | } | 547 | } |
| 488 | 548 | ||
| 489 | if (extension_settings.connectionManager.profiles.some(p => p.name === newName)) { | 549 | if (profile.name !== newName && extension_settings.connectionManager.profiles.some(p => p.name === newName)) { |
| 490 | toastr.error('A profile with the same name already exists.'); | 550 | toastr.error('A profile with the same name already exists.'); |
| 491 | return; | 551 | return; |
| 492 | } | 552 | } |
| 493 | 553 | ||
| 494 | profile.name = newName; | 554 | const newExcludeList = template.find('input[name="exclude"]:not(:checked)').map(function () { |
| 555 | return Object.entries(FANCY_NAMES).find(x => x[1] === String($(this).val()))?.[0]; | ||
| 556 | }).get(); | ||
| 557 | |||
| 558 | if (newExcludeList.length !== profile.exclude.length || !newExcludeList.every(e => profile.exclude.includes(e))) { | ||
| 559 | profile.exclude = newExcludeList; | ||
| 560 | for (const command of newExcludeList) { | ||
| 561 | delete profile[command]; | ||
| 562 | } | ||
| 563 | if (saveChanges) { | ||
| 564 | await updateConnectionProfile(profile); | ||
| 565 | } else { | ||
| 566 | toastr.info('Press "Update" to record them into the profile.', 'Included settings list updated'); | ||
| 567 | } | ||
| 568 | } | ||
| 569 | |||
| 570 | if (profile.name !== newName) { | ||
| 571 | toastr.success('Connection profile renamed.'); | ||
| 572 | profile.name = String(newName); | ||
| 573 | } | ||
| 574 | |||
| 495 | saveSettingsDebounced(); | 575 | saveSettingsDebounced(); |
| 496 | renderConnectionProfiles(profiles); | 576 | renderConnectionProfiles(profiles); |
| 497 | toastr.success('Connection profile renamed', '', { timeOut: 1500 }); | 577 | await renderDetailsContent(detailsContent); |
| 498 | }); | 578 | }); |
| 499 | 579 | ||
| 500 | /** @type {HTMLElement} */ | 580 | /** @type {HTMLElement} */ |
| @@ -2,11 +2,20 @@ | |||
| 2 | <h2 data-i18n="Creating a Connection Profile"> | 2 | <h2 data-i18n="Creating a Connection Profile"> |
| 3 | Creating a Connection Profile | 3 | Creating a Connection Profile |
| 4 | </h2> | 4 | </h2> |
| 5 | <ul class="justifyLeft"> | 5 | <div class="justifyLeft flex-container flexFlowColumn flexNoGap"> |
| 6 | {{#each profile}} | 6 | {{#each profile}} |
| 7 | <li><strong data-i18n="{{@key}}">{{@key}}:</strong> {{this}}</li> | 7 | <label class="checkbox_label"> |
| 8 | <input type="checkbox" value="{{@key}}" name="exclude" checked> | ||
| 9 | <span><strong data-i18n="{{@key}}">{{@key}}:</strong> {{this}}</span> | ||
| 10 | </label> | ||
| 8 | {{/each}} | 11 | {{/each}} |
| 9 | </ul> | 12 | </div> |
| 13 | <div class="marginTop5"> | ||
| 14 | <small> | ||
| 15 | <b>Hint:</b> | ||
| 16 | <i>Click on the setting name to omit it from the profile.</i> | ||
| 17 | </small> | ||
| 18 | </div> | ||
| 10 | <h3 data-i18n="Enter a name:"> | 19 | <h3 data-i18n="Enter a name:"> |
| 11 | Enter a name: | 20 | Enter a name: |
| 12 | </h3> | 21 | </h3> |
| @@ -13,7 +13,7 @@ | |||
| 13 | <i id="view_connection_profile" class="menu_button fa-solid fa-info-circle" title="View connection profile details" data-i18n="[title]View connection profile details"></i> | 13 | <i id="view_connection_profile" class="menu_button fa-solid fa-info-circle" title="View connection profile details" data-i18n="[title]View connection profile details"></i> |
| 14 | <i id="create_connection_profile" class="menu_button fa-solid fa-file-circle-plus" title="Create a new connection profile" data-i18n="[title]Create a new connection profile"></i> | 14 | <i id="create_connection_profile" class="menu_button fa-solid fa-file-circle-plus" title="Create a new connection profile" data-i18n="[title]Create a new connection profile"></i> |
| 15 | <i id="update_connection_profile" class="menu_button fa-solid fa-save" title="Update a connection profile" data-i18n="[title]Update a connection profile"></i> | 15 | <i id="update_connection_profile" class="menu_button fa-solid fa-save" title="Update a connection profile" data-i18n="[title]Update a connection profile"></i> |
| 16 | <i id="rename_connection_profile" class="menu_button fa-solid fa-pencil" title="Rename a connection profile" data-i18n="[title]Rename a connection profile"></i> | 16 | <i id="edit_connection_profile" class="menu_button fa-solid fa-pencil" title="Edit a connection profile" data-i18n="[title]Edit a connection profile"></i> |
| 17 | <i id="reload_connection_profile" class="menu_button fa-solid fa-recycle" title="Reload a connection profile" data-i18n="[title]Reload a connection profile"></i> | 17 | <i id="reload_connection_profile" class="menu_button fa-solid fa-recycle" title="Reload a connection profile" data-i18n="[title]Reload a connection profile"></i> |
| 18 | <i id="delete_connection_profile" class="menu_button fa-solid fa-trash-can" title="Delete a connection profile" data-i18n="[title]Delete a connection profile"></i> | 18 | <i id="delete_connection_profile" class="menu_button fa-solid fa-trash-can" title="Delete a connection profile" data-i18n="[title]Delete a connection profile"></i> |
| 19 | </div> | 19 | </div> |
| @@ -3,3 +3,8 @@ | |||
| 3 | <li><strong data-i18n="{{@key}}">{{@key}}:</strong> {{this}}</li> | 3 | <li><strong data-i18n="{{@key}}">{{@key}}:</strong> {{this}}</li> |
| 4 | {{/each}} | 4 | {{/each}} |
| 5 | </ul> | 5 | </ul> |
| 6 | {{#if omitted}} | ||
| 7 | <div class="margin5"> | ||
| 8 | <strong data-i18n="Omitted Settings:">Omitted Settings:</strong> <span>{{omitted}}</span> | ||
| 9 | </div> | ||
| 10 | {{/if}} | ||