| 1 | import { |
| 2 | main_api, |
| 3 | saveSettingsDebounced, |
| 4 | } from '../script.js'; |
| 5 | //import { BIAS_CACHE, displayLogitBias, getLogitBiasListResult } from './logit-bias.js'; |
| 6 | //import { getEventSourceStream } from './sse-stream.js'; |
| 7 | //import { getSortableDelay, onlyUnique } from './utils.js'; |
| 8 | //import { getCfgPrompt } from './cfg-scale.js'; |
| 9 | import { setting_names as TGsamplerNames, showTGSamplerControls, textgenerationwebui_settings } from './textgen-settings.js'; |
| 10 | import { renderTemplateAsync } from './templates.js'; |
| 11 | import { Popup, POPUP_TYPE } from './popup.js'; |
| 12 | import { localforage } from '../lib.js'; |
| 13 | |
| 14 | const forcedOnColoring = 'color: #89db35;'; |
| 15 | const forcedOffColoring = 'color: #e84f62;'; |
| 16 | const SELECT_SAMPLER = { |
| 17 | DATA: 'selectsampler', |
| 18 | SHOWN: 'shown', |
| 19 | HIDDEN: 'hidden', |
| 20 | }; |
| 21 | |
| 22 | const textGenObjectStore = localforage.createInstance({ name: 'SillyTavern_TextCompletions' }); |
| 23 | let selectedSamplers = {}; |
| 24 | |
| 25 | // Goal 1: show popup with all samplers for active API |
| 26 | async function showSamplerSelectPopup() { |
| 27 | const html = $(document.createElement('div')); |
| 28 | html.attr('id', 'sampler_view_list') |
| 29 | .addClass('flex-container flexFlowColumn'); |
| 30 | html.append(await renderTemplateAsync('samplerSelector')); |
| 31 | |
| 32 | const listContainer = $('<div id="apiSamplersList" class="flex-container flexNoGap"></div>'); |
| 33 | const APISamplers = await listSamplers(main_api); |
| 34 | listContainer.append(APISamplers.toString()); |
| 35 | html.append(listContainer); |
| 36 | |
| 37 | const showPromise = new Popup(html, POPUP_TYPE.TEXT, null, { wide: true, large: true, allowVerticalScrolling: true }).show(); |
| 38 | |
| 39 | setSamplerListListeners(); |
| 40 | |
| 41 | $('#resetSelectedSamplers').off('click').on('click', async function () { |
| 42 | console.log('saw sampler select reset click'); |
| 43 | |
| 44 | if (main_api === 'textgenerationwebui') { |
| 45 | $('#prioritizeManuallySelectedSamplers').toggleClass('toggleEnabled', false); |
| 46 | await resetApiSelectedSamplers(null, true); |
| 47 | } |
| 48 | |
| 49 | await validateDisabledSamplers(true); |
| 50 | }); |
| 51 | |
| 52 | if (main_api === 'textgenerationwebui') { |
| 53 | $('#prioritizeManuallySelectedSamplers').show(); |
| 54 | $('#prioritizeManuallySelectedSamplers').toggleClass('toggleEnabled', isSamplerManualPriorityEnabled()); |
| 55 | $('#prioritizeManuallySelectedSamplers').off('click').on('click', function () { |
| 56 | $(this).toggleClass('toggleEnabled'); |
| 57 | |
| 58 | const isActive = $(this).hasClass('toggleEnabled'); |
| 59 | |
| 60 | toggleSamplerManualPriority(isActive); |
| 61 | }); |
| 62 | } else { |
| 63 | $('#prioritizeManuallySelectedSamplers').hide(); |
| 64 | $('#prioritizeManuallySelectedSamplers').off('click'); |
| 65 | } |
| 66 | |
| 67 | await showPromise; |
| 68 | if (main_api === 'textgenerationwebui') await saveApiSelectedSamplers(); |
| 69 | } |
| 70 | |
| 71 | function getRelatedDOMElement(samplerName) { |
| 72 | let relatedDOMElement = $(`#${samplerName}_${main_api}`).parent(); |
| 73 | let targetDisplayType = 'flex'; |
| 74 | let displayname; |
| 75 | |
| 76 | if (samplerName === 'json_schema') { |
| 77 | relatedDOMElement = $('#json_schema_block'); |
| 78 | targetDisplayType = 'block'; |
| 79 | displayname = 'JSON Schema Block'; |
| 80 | } |
| 81 | |
| 82 | if (samplerName === 'grammar_string') { |
| 83 | relatedDOMElement = $('#grammar_block_ooba'); |
| 84 | targetDisplayType = 'block'; |
| 85 | displayname = 'Grammar Block'; |
| 86 | } |
| 87 | |
| 88 | if (samplerName === 'guidance_scale') { |
| 89 | relatedDOMElement = $('#cfg_block_ooba'); |
| 90 | targetDisplayType = 'block'; |
| 91 | displayname = 'CFG Block'; |
| 92 | } |
| 93 | |
| 94 | if (samplerName === 'mirostat_mode') { |
| 95 | relatedDOMElement = $('#mirostat_block_ooba'); |
| 96 | targetDisplayType = 'block'; |
| 97 | displayname = 'Mirostat Block'; |
| 98 | } |
| 99 | |
| 100 | if (samplerName === 'dry_multiplier') { |
| 101 | relatedDOMElement = $('#dryBlock'); |
| 102 | targetDisplayType = 'block'; |
| 103 | displayname = 'DRY Rep Pen Block'; |
| 104 | } |
| 105 | |
| 106 | if (samplerName === 'xtc_probability') { |
| 107 | relatedDOMElement = $('#xtc_block'); |
| 108 | targetDisplayType = 'block'; |
| 109 | displayname = 'XTC Block'; |
| 110 | } |
| 111 | |
| 112 | if (samplerName === 'dynatemp') { |
| 113 | relatedDOMElement = $('#dynatemp_block_ooba'); |
| 114 | targetDisplayType = 'block'; |
| 115 | displayname = 'DynaTemp Block'; |
| 116 | } |
| 117 | |
| 118 | if (samplerName === 'banned_tokens') { |
| 119 | relatedDOMElement = $('#banned_tokens_block_ooba'); |
| 120 | targetDisplayType = 'block'; |
| 121 | } |
| 122 | |
| 123 | if (samplerName === 'sampler_order') { //this is for kcpp sampler order |
| 124 | relatedDOMElement = $('#sampler_order_block_kcpp'); |
| 125 | displayname = 'KCPP Sampler Order Block'; |
| 126 | } |
| 127 | |
| 128 | if (samplerName === 'samplers') { //this is for lcpp sampler order |
| 129 | relatedDOMElement = $('#sampler_order_block_lcpp'); |
| 130 | displayname = 'LCPP Sampler Order Block'; |
| 131 | } |
| 132 | |
| 133 | if (samplerName === 'sampler_priority') { //this is for ooba's sampler priority |
| 134 | relatedDOMElement = $('#sampler_priority_block_ooba'); |
| 135 | displayname = 'Ooba Sampler Priority Block'; |
| 136 | } |
| 137 | |
| 138 | if (samplerName === 'samplers_priorities') { //this is for aphrodite's sampler priority |
| 139 | relatedDOMElement = $('#sampler_priority_block_aphrodite'); |
| 140 | displayname = 'Aphrodite Sampler Priority Block'; |
| 141 | } |
| 142 | |
| 143 | if (samplerName === 'penalty_alpha') { //contrastive search only has one sampler, does it need its own block? |
| 144 | relatedDOMElement = $('#contrastiveSearchBlock'); |
| 145 | displayname = 'Contrast Search Block'; |
| 146 | } |
| 147 | |
| 148 | if (samplerName === 'num_beams') { // num_beams is the killswitch for Beam Search |
| 149 | relatedDOMElement = $('#beamSearchBlock'); |
| 150 | targetDisplayType = 'block'; |
| 151 | displayname = 'Beam Search Block'; |
| 152 | } |
| 153 | |
| 154 | if (samplerName === 'smoothing_factor') { // num_beams is the killswitch for Beam Search |
| 155 | relatedDOMElement = $('#smoothingBlock'); |
| 156 | targetDisplayType = 'block'; |
| 157 | displayname = 'Smoothing Block'; |
| 158 | } |
| 159 | |
| 160 | return { relatedDOMElement, targetDisplayType, displayname }; |
| 161 | } |
| 162 | |
| 163 | function setSamplerListListeners() { |
| 164 | // Goal 2: hide unchecked samplers from DOM |
| 165 | let listContainer = $('#apiSamplersList'); |
| 166 | listContainer.find('input').off('change').on('change', async function () { |
| 167 | const samplerName = this.name.replace('_checkbox', ''); |
| 168 | const { relatedDOMElement, targetDisplayType } = getRelatedDOMElement(samplerName); |
| 169 | |
| 170 | // Get the current state of the custom data attribute |
| 171 | const previousState = relatedDOMElement.data(SELECT_SAMPLER.DATA); |
| 172 | const isChecked = $(this).prop('checked'); |
| 173 | const popupInputLabel = $(this).parent().find('.sampler_name'); |
| 174 | |
| 175 | if (isChecked === false) { |
| 176 | if (previousState === SELECT_SAMPLER.SHOWN) { |
| 177 | console.log('saw previously custom shown sampler => new state:', isChecked, samplerName); |
| 178 | relatedDOMElement.removeData(SELECT_SAMPLER.DATA); |
| 179 | popupInputLabel.removeAttr('style'); |
| 180 | } else { |
| 181 | console.log('saw previous untouched sampler => new state:', isChecked, samplerName); |
| 182 | relatedDOMElement.data(SELECT_SAMPLER.DATA, SELECT_SAMPLER.HIDDEN); |
| 183 | popupInputLabel.attr('style', forcedOffColoring); |
| 184 | } |
| 185 | } else { |
| 186 | if (previousState === SELECT_SAMPLER.HIDDEN) { |
| 187 | console.log('saw previously custom hidden sampler => new state:', isChecked, samplerName); |
| 188 | relatedDOMElement.removeData(SELECT_SAMPLER.DATA); |
| 189 | popupInputLabel.removeAttr('style'); |
| 190 | } else { |
| 191 | console.log('saw previous untouched sampler => new state:', isChecked, samplerName); |
| 192 | relatedDOMElement.data(SELECT_SAMPLER.DATA, SELECT_SAMPLER.SHOWN); |
| 193 | popupInputLabel.attr('style', forcedOnColoring); |
| 194 | } |
| 195 | } |
| 196 | |
| 197 | await saveSettingsDebounced(); |
| 198 | |
| 199 | const shouldDisplay = isChecked ? targetDisplayType : 'none'; |
| 200 | relatedDOMElement.css('display', shouldDisplay); |
| 201 | |
| 202 | if (main_api === 'textgenerationwebui') setApiSamplersState(samplerName, shouldDisplay !== 'none'); |
| 203 | |
| 204 | console.log(samplerName, relatedDOMElement.data(SELECT_SAMPLER.DATA), shouldDisplay); |
| 205 | }); |
| 206 | } |
| 207 | |
| 208 | function isElementVisibleInDOM(element) { |
| 209 | while (element && element !== document.body) { |
| 210 | if (window.getComputedStyle(element).display === 'none') { |
| 211 | return false; |
| 212 | } |
| 213 | element = element.parentElement; |
| 214 | } |
| 215 | return true; |
| 216 | } |
| 217 | |
| 218 | |
| 219 | async function listSamplers(main_api, arrayOnly = false) { |
| 220 | let availableSamplers; |
| 221 | if (main_api === 'textgenerationwebui') { |
| 222 | availableSamplers = TGsamplerNames; |
| 223 | const valuesToRemove = new Set(['streaming', 'bypass_status_check', 'custom_model', 'generic_model', 'openrouter_allow_fallbacks', 'legacy_api', 'extensions']); |
| 224 | availableSamplers = availableSamplers.filter(sampler => !valuesToRemove.has(sampler)); |
| 225 | availableSamplers.sort(); |
| 226 | } |
| 227 | |
| 228 | if (arrayOnly) { |
| 229 | console.debug('returning full samplers array'); |
| 230 | return availableSamplers; |
| 231 | } |
| 232 | |
| 233 | const samplersActivatedManually = (main_api === 'textgenerationwebui') ? getActiveManualApiSamplers() : []; |
| 234 | const prioritizeManualSamplerSelect = (main_api === 'textgenerationwebui') ? isSamplerManualPriorityEnabled() : false; |
| 235 | |
| 236 | const samplersListHTML = availableSamplers.reduce((html, sampler) => { |
| 237 | let customColor; |
| 238 | let { relatedDOMElement, displayname } = getRelatedDOMElement(sampler); |
| 239 | |
| 240 | const isManuallyActivated = samplersActivatedManually.includes(sampler); |
| 241 | const displayModified = relatedDOMElement.data(SELECT_SAMPLER.DATA); |
| 242 | const isInDefaultState = !displayModified; |
| 243 | |
| 244 | const shouldBeChecked = () => { |
| 245 | let finalState = isElementVisibleInDOM(relatedDOMElement[0]); |
| 246 | |
| 247 | if (prioritizeManualSamplerSelect) { |
| 248 | finalState = isManuallyActivated; |
| 249 | } else if (!isInDefaultState) { |
| 250 | finalState = displayModified === SELECT_SAMPLER.SHOWN; |
| 251 | customColor = finalState ? forcedOnColoring : forcedOffColoring; |
| 252 | } |
| 253 | |
| 254 | return finalState; |
| 255 | }; |
| 256 | |
| 257 | console.log(sampler, relatedDOMElement.prop('id'), isInDefaultState, shouldBeChecked()); |
| 258 | |
| 259 | if (displayname === undefined) displayname = sampler; |
| 260 | if (main_api === 'textgenerationwebui') setApiSamplersState(sampler, shouldBeChecked()); |
| 261 | |
| 262 | return html + ` |
| 263 | <label class="sampler_view_list_item wide50p flex-container"> |
| 264 | <input type="checkbox" name="${sampler}_checkbox" ${shouldBeChecked() ? 'checked' : ''}> |
| 265 | <small class="sampler_name" style="${customColor}">${displayname}</small> |
| 266 | </label>`; |
| 267 | }, ''); |
| 268 | |
| 269 | return samplersListHTML; |
| 270 | } |
| 271 | |
| 272 | // Goal 3: make "sampler is hidden/disabled" status persistent (save settings) |
| 273 | // this runs on initial getSettings as well as after API changes |
| 274 | |
| 275 | export async function validateDisabledSamplers(redraw = false) { |
| 276 | const APISamplers = await listSamplers(main_api, true); |
| 277 | |
| 278 | if (!Array.isArray(APISamplers)) { |
| 279 | return; |
| 280 | } |
| 281 | |
| 282 | const samplersActivatedManually = (main_api === 'textgenerationwebui') ? getActiveManualApiSamplers() : []; |
| 283 | const prioritizeManualSamplerSelect = (main_api === 'textgenerationwebui') ? isSamplerManualPriorityEnabled() : false; |
| 284 | |
| 285 | for (const sampler of APISamplers) { |
| 286 | const { relatedDOMElement, targetDisplayType } = getRelatedDOMElement(sampler); |
| 287 | |
| 288 | if (prioritizeManualSamplerSelect) { |
| 289 | const isManuallyActivated = samplersActivatedManually.includes(sampler); |
| 290 | relatedDOMElement.css('display', isManuallyActivated ? targetDisplayType : 'none'); |
| 291 | } else { |
| 292 | const selectSamplerData = relatedDOMElement.data(SELECT_SAMPLER.DATA); |
| 293 | relatedDOMElement.css('display', selectSamplerData === SELECT_SAMPLER.SHOWN ? targetDisplayType : 'none'); |
| 294 | } |
| 295 | |
| 296 | relatedDOMElement.removeData(SELECT_SAMPLER.DATA); |
| 297 | } |
| 298 | |
| 299 | if (!prioritizeManualSamplerSelect && main_api === 'textgenerationwebui') { |
| 300 | showTGSamplerControls(); |
| 301 | } |
| 302 | |
| 303 | if (redraw) { |
| 304 | let samplersHTML = await listSamplers(main_api); |
| 305 | $('#apiSamplersList').empty().append(samplersHTML.toString()); |
| 306 | setSamplerListListeners(); |
| 307 | } |
| 308 | |
| 309 | await saveSettingsDebounced(); |
| 310 | } |
| 311 | |
| 312 | /** |
| 313 | * Initializes the configuration object for manually selected samplers. |
| 314 | * @returns void |
| 315 | */ |
| 316 | export async function loadApiSelectedSamplers() { |
| 317 | try { |
| 318 | console.debug('Text Completions: loading selected samplers'); |
| 319 | selectedSamplers = await textGenObjectStore.getItem('selectedSamplers') || {}; |
| 320 | } catch (error) { |
| 321 | console.log('Text Completions: unable to load selected samplers, using default samplers', error); |
| 322 | selectedSamplers = {}; |
| 323 | } |
| 324 | } |
| 325 | |
| 326 | /** |
| 327 | * Synchronizes the local forage instance with the selected samplers configuration object. |
| 328 | * @returns void |
| 329 | */ |
| 330 | export async function saveApiSelectedSamplers() { |
| 331 | try { |
| 332 | console.debug('Text Completions: saving selected samplers'); |
| 333 | await textGenObjectStore.setItem('selectedSamplers', selectedSamplers); |
| 334 | } catch (error) { |
| 335 | console.log('Text Completions: unable to save selected samplers', error); |
| 336 | } |
| 337 | } |
| 338 | |
| 339 | /** |
| 340 | * Resets the selected samplers configuration object from the local forage instance. |
| 341 | * @param {string?} tcApiType Name of the target API Type - It picks the currently active TC API type name by default |
| 342 | * @param {boolean} silent Suppresses the toastr message confirming that the data was deleted. |
| 343 | * @returns void |
| 344 | */ |
| 345 | export async function resetApiSelectedSamplers(tcApiType = '', silent = false) { |
| 346 | try { |
| 347 | if (!textgenerationwebui_settings?.type && !tcApiType) return; |
| 348 | if (!tcApiType) tcApiType = textgenerationwebui_settings.type; |
| 349 | if (!selectedSamplers[tcApiType]) return; |
| 350 | |
| 351 | console.debug('Text Completions: resetting selected samplers'); |
| 352 | delete selectedSamplers[tcApiType]; |
| 353 | await saveApiSelectedSamplers(); |
| 354 | if (!silent) toastr.success('Selected samplers cleared.'); |
| 355 | } catch (error) { |
| 356 | console.log('Text Completions: unable to reset selected preset samplers', error); |
| 357 | } |
| 358 | } |
| 359 | |
| 360 | /** |
| 361 | * Saves the visibility state for selected samplers into the configuration object. |
| 362 | * @param {string} samplerName Target sampler key name |
| 363 | * @param {string|boolean} state Visibility state of the target sampler |
| 364 | * @param {string?} tcApiType Name of the target API Type - It picks the currently active TC API type name by default |
| 365 | * @returns void |
| 366 | */ |
| 367 | export function setApiSamplersState(samplerName, state, tcApiType = '') { |
| 368 | if (!textgenerationwebui_settings?.type && !tcApiType) return; |
| 369 | if (!tcApiType) tcApiType = textgenerationwebui_settings.type; |
| 370 | if (!selectedSamplers[tcApiType]) selectedSamplers[tcApiType] = {}; |
| 371 | |
| 372 | const presetSamplers = selectedSamplers[tcApiType]; |
| 373 | presetSamplers[samplerName] = String(state) === 'true'; |
| 374 | } |
| 375 | |
| 376 | /** |
| 377 | * Returns the local forage object belonging to the active/selected TC API Type |
| 378 | * @param {string?} tcApiType Name of the target API Type - It picks the currently active TC API type name by default |
| 379 | * @returns {object} Full localforage object with manual selections |
| 380 | */ |
| 381 | export function getAllManualApiSamplers(tcApiType = '') { |
| 382 | if (!textgenerationwebui_settings?.type && !tcApiType) return {}; |
| 383 | if (!tcApiType) tcApiType = textgenerationwebui_settings.type; |
| 384 | if (!selectedSamplers[tcApiType]) selectedSamplers[tcApiType] = {}; |
| 385 | |
| 386 | return selectedSamplers[tcApiType]; |
| 387 | } |
| 388 | |
| 389 | /** |
| 390 | * Returns the key names of all the manually activated API Type samplers. |
| 391 | * @param {string?} tcApiType Name of the target API Type - It picks the currently active TC API type name by default |
| 392 | * @returns {string[]} Array of sampler key names |
| 393 | */ |
| 394 | export function getActiveManualApiSamplers(tcApiType = '') { |
| 395 | if (!textgenerationwebui_settings?.type && !tcApiType) return []; |
| 396 | if (!tcApiType) tcApiType = textgenerationwebui_settings.type; |
| 397 | if (!selectedSamplers[tcApiType]) selectedSamplers[tcApiType] = {}; |
| 398 | |
| 399 | try { |
| 400 | const presetSamplers = Object.entries(selectedSamplers[tcApiType]); |
| 401 | |
| 402 | return presetSamplers |
| 403 | .filter(([key, val]) => val === true && key !== 'st_manual_priority') |
| 404 | .map(([key, val]) => key); |
| 405 | } catch (error) { |
| 406 | console.log('Text Completions: unable to fetch active preset samplers', error); |
| 407 | return []; |
| 408 | } |
| 409 | } |
| 410 | |
| 411 | /** |
| 412 | * @param {string|boolean} state Target state of the feature |
| 413 | * @param {string?} tcApiType Name of the target API Type - It picks the currently active TC API type name by default |
| 414 | * @returns void |
| 415 | */ |
| 416 | export function toggleSamplerManualPriority(state = false, tcApiType = '') { |
| 417 | if (!textgenerationwebui_settings?.type && !tcApiType) return; |
| 418 | if (!tcApiType) tcApiType = textgenerationwebui_settings.type; |
| 419 | if (!selectedSamplers[tcApiType]) selectedSamplers[tcApiType] = {}; |
| 420 | |
| 421 | const presetSamplers = selectedSamplers[tcApiType]; |
| 422 | presetSamplers.st_manual_priority = String(state) === 'true'; |
| 423 | } |
| 424 | |
| 425 | /** |
| 426 | * @param {string?} tcApiType Name of the target API Type - It picks the currently active TC API type name by default |
| 427 | * @returns {boolean} |
| 428 | */ |
| 429 | export function isSamplerManualPriorityEnabled(tcApiType = '') { |
| 430 | if (!textgenerationwebui_settings?.type && !tcApiType) return false; |
| 431 | if (!tcApiType) tcApiType = textgenerationwebui_settings.type; |
| 432 | if (!selectedSamplers[tcApiType]) selectedSamplers[tcApiType] = {}; |
| 433 | |
| 434 | return selectedSamplers[tcApiType]?.st_manual_priority ?? false; |
| 435 | } |
| 436 | |
| 437 | export async function initCustomSelectedSamplers() { |
| 438 | await saveSettingsDebounced(); |
| 439 | $('#samplerSelectButton').off('click').on('click', showSamplerSelectPopup); |
| 440 | } |
| 441 | |
| 442 | // Goal 4: filter hidden samplers from API output |
| 443 | |
| 444 | // Goal 5: allow addition of custom samplers to be displayed |
| 445 | // Goal 6: send custom sampler values into prompt |