| 1 | import { |
| 2 | chat_metadata, |
| 3 | substituteParams, |
| 4 | this_chid, |
| 5 | eventSource, |
| 6 | event_types, |
| 7 | saveSettingsDebounced, |
| 8 | animation_duration, |
| 9 | } from '../script.js'; |
| 10 | import { extension_settings, saveMetadataDebounced } from './extensions.js'; |
| 11 | import { selected_group } from './group-chats.js'; |
| 12 | import { getCharaFilename, delay } from './utils.js'; |
| 13 | import { power_user } from './power-user.js'; |
| 14 | |
| 15 | const extensionName = 'cfg'; |
| 16 | const defaultSettings = { |
| 17 | global: { |
| 18 | 'guidance_scale': 1, |
| 19 | 'negative_prompt': '', |
| 20 | }, |
| 21 | chara: [], |
| 22 | }; |
| 23 | const settingType = { |
| 24 | guidance_scale: 0, |
| 25 | negative_prompt: 1, |
| 26 | positive_prompt: 2, |
| 27 | }; |
| 28 | |
| 29 | // Used for character and chat CFG values |
| 30 | function updateSettings() { |
| 31 | saveSettingsDebounced(); |
| 32 | loadSettings(); |
| 33 | } |
| 34 | |
| 35 | function setCharCfg(tempValue, setting) { |
| 36 | const avatarName = getCharaFilename(); |
| 37 | |
| 38 | // Assign temp object |
| 39 | let tempCharaCfg = { |
| 40 | name: avatarName, |
| 41 | }; |
| 42 | |
| 43 | switch (setting) { |
| 44 | case settingType.guidance_scale: |
| 45 | tempCharaCfg.guidance_scale = Number(tempValue); |
| 46 | break; |
| 47 | case settingType.negative_prompt: |
| 48 | tempCharaCfg.negative_prompt = tempValue; |
| 49 | break; |
| 50 | case settingType.positive_prompt: |
| 51 | tempCharaCfg.positive_prompt = tempValue; |
| 52 | break; |
| 53 | default: |
| 54 | return false; |
| 55 | } |
| 56 | |
| 57 | let existingCharaCfgIndex; |
| 58 | let existingCharaCfg; |
| 59 | |
| 60 | if (extension_settings.cfg.chara) { |
| 61 | existingCharaCfgIndex = extension_settings.cfg.chara.findIndex((e) => e.name === avatarName); |
| 62 | existingCharaCfg = extension_settings.cfg.chara[existingCharaCfgIndex]; |
| 63 | } |
| 64 | |
| 65 | if (extension_settings.cfg.chara && existingCharaCfg) { |
| 66 | const tempAssign = Object.assign(existingCharaCfg, tempCharaCfg); |
| 67 | |
| 68 | // If both values are default, remove the entry |
| 69 | if (!existingCharaCfg.useChara && |
| 70 | (tempAssign.guidance_scale ?? 1.00) === 1.00 && |
| 71 | (tempAssign.negative_prompt?.length ?? 0) === 0 && |
| 72 | (tempAssign.positive_prompt?.length ?? 0) === 0) { |
| 73 | extension_settings.cfg.chara.splice(existingCharaCfgIndex, 1); |
| 74 | } |
| 75 | } else if (avatarName && tempValue.length > 0) { |
| 76 | if (!extension_settings.cfg.chara) { |
| 77 | extension_settings.cfg.chara = []; |
| 78 | } |
| 79 | |
| 80 | extension_settings.cfg.chara.push(tempCharaCfg); |
| 81 | } else { |
| 82 | console.debug('Character CFG error: No avatar name key could be found.'); |
| 83 | |
| 84 | // Don't save settings if something went wrong |
| 85 | return false; |
| 86 | } |
| 87 | |
| 88 | updateSettings(); |
| 89 | |
| 90 | return true; |
| 91 | } |
| 92 | |
| 93 | function setChatCfg(tempValue, setting) { |
| 94 | switch (setting) { |
| 95 | case settingType.guidance_scale: |
| 96 | chat_metadata[metadataKeys.guidance_scale] = tempValue; |
| 97 | break; |
| 98 | case settingType.negative_prompt: |
| 99 | chat_metadata[metadataKeys.negative_prompt] = tempValue; |
| 100 | break; |
| 101 | case settingType.positive_prompt: |
| 102 | chat_metadata[metadataKeys.positive_prompt] = tempValue; |
| 103 | break; |
| 104 | default: |
| 105 | return false; |
| 106 | } |
| 107 | |
| 108 | saveMetadataDebounced(); |
| 109 | |
| 110 | return true; |
| 111 | } |
| 112 | |
| 113 | // TODO: Only change CFG when character is selected |
| 114 | function onCfgMenuItemClick() { |
| 115 | if (!selected_group && this_chid === undefined) { |
| 116 | toastr.warning('Select a character before trying to configure CFG', '', { timeOut: 2000 }); |
| 117 | return; |
| 118 | } |
| 119 | |
| 120 | //show CFG config if it's hidden |
| 121 | if ($('#cfgConfig').css('display') !== 'flex') { |
| 122 | $('#cfgConfig').addClass('resizing'); |
| 123 | $('#cfgConfig').css('display', 'flex'); |
| 124 | $('#cfgConfig').css('opacity', 0.0); |
| 125 | $('#cfgConfig').transition({ |
| 126 | opacity: 1.0, |
| 127 | duration: animation_duration, |
| 128 | }, async function () { |
| 129 | await delay(50); |
| 130 | $('#cfgConfig').removeClass('resizing'); |
| 131 | }); |
| 132 | |
| 133 | //auto-open the main AN inline drawer |
| 134 | if ($('#CFGBlockToggle') |
| 135 | .siblings('.inline-drawer-content') |
| 136 | .css('display') !== 'block') { |
| 137 | $('#floatingPrompt').addClass('resizing'); |
| 138 | $('#CFGBlockToggle').trigger('click'); |
| 139 | } |
| 140 | } else { |
| 141 | //hide AN if it's already displayed |
| 142 | $('#cfgConfig').addClass('resizing'); |
| 143 | $('#cfgConfig').transition({ |
| 144 | opacity: 0.0, |
| 145 | duration: animation_duration, |
| 146 | }, async function () { |
| 147 | await delay(50); |
| 148 | $('#cfgConfig').removeClass('resizing'); |
| 149 | }); |
| 150 | setTimeout(function () { |
| 151 | $('#cfgConfig').hide(); |
| 152 | }, animation_duration); |
| 153 | } |
| 154 | //duplicate options menu close handler from script.js |
| 155 | //because this listener takes priority |
| 156 | $('#options').stop().fadeOut(animation_duration); |
| 157 | } |
| 158 | |
| 159 | async function onChatChanged() { |
| 160 | loadSettings(); |
| 161 | await modifyCharaHtml(); |
| 162 | } |
| 163 | |
| 164 | // Rearrange the panel if a group chat is present |
| 165 | async function modifyCharaHtml() { |
| 166 | if (selected_group) { |
| 167 | $('#chara_cfg_container').hide(); |
| 168 | $('#groupchat_cfg_use_chara_container').show(); |
| 169 | } else { |
| 170 | $('#chara_cfg_container').show(); |
| 171 | $('#groupchat_cfg_use_chara_container').hide(); |
| 172 | // TODO: Remove chat checkbox here |
| 173 | } |
| 174 | } |
| 175 | |
| 176 | // Reloads chat-specific settings |
| 177 | function loadSettings() { |
| 178 | // Set chat CFG if it exists |
| 179 | $('#chat_cfg_guidance_scale').val(chat_metadata[metadataKeys.guidance_scale] ?? 1.0.toFixed(2)); |
| 180 | $('#chat_cfg_guidance_scale_counter').val(chat_metadata[metadataKeys.guidance_scale]?.toFixed(2) ?? 1.0.toFixed(2)); |
| 181 | $('#chat_cfg_negative_prompt').val(chat_metadata[metadataKeys.negative_prompt] ?? ''); |
| 182 | $('#chat_cfg_positive_prompt').val(chat_metadata[metadataKeys.positive_prompt] ?? ''); |
| 183 | $('#groupchat_cfg_use_chara').prop('checked', chat_metadata[metadataKeys.groupchat_individual_chars] ?? false); |
| 184 | if (chat_metadata[metadataKeys.prompt_combine]?.length > 0) { |
| 185 | chat_metadata[metadataKeys.prompt_combine].forEach((element) => { |
| 186 | $(`input[name="cfg_prompt_combine"][value="${element}"]`) |
| 187 | .prop('checked', true); |
| 188 | }); |
| 189 | } |
| 190 | |
| 191 | // Display the negative separator in quotes if not quoted already |
| 192 | let promptSeparatorDisplay = []; |
| 193 | const promptSeparator = chat_metadata[metadataKeys.prompt_separator]; |
| 194 | if (promptSeparator) { |
| 195 | promptSeparatorDisplay.push(promptSeparator); |
| 196 | if (!promptSeparator.startsWith('"')) { |
| 197 | promptSeparatorDisplay.unshift('"'); |
| 198 | } |
| 199 | |
| 200 | if (!promptSeparator.endsWith('"')) { |
| 201 | promptSeparatorDisplay.push('"'); |
| 202 | } |
| 203 | } |
| 204 | |
| 205 | $('#cfg_prompt_separator').val(promptSeparatorDisplay.length === 0 ? '' : promptSeparatorDisplay.join('')); |
| 206 | |
| 207 | $('#cfg_prompt_insertion_depth').val(chat_metadata[metadataKeys.prompt_insertion_depth] ?? 1); |
| 208 | |
| 209 | // Set character CFG if it exists |
| 210 | if (!selected_group) { |
| 211 | const charaCfg = extension_settings.cfg.chara.find((e) => e.name === getCharaFilename()); |
| 212 | $('#chara_cfg_guidance_scale').val(charaCfg?.guidance_scale ?? 1.00); |
| 213 | $('#chara_cfg_guidance_scale_counter').val(charaCfg?.guidance_scale?.toFixed(2) ?? 1.0.toFixed(2)); |
| 214 | $('#chara_cfg_negative_prompt').val(charaCfg?.negative_prompt ?? ''); |
| 215 | $('#chara_cfg_positive_prompt').val(charaCfg?.positive_prompt ?? ''); |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | // Load initial extension settings |
| 220 | async function initialLoadSettings() { |
| 221 | // Create the settings if they don't exist |
| 222 | extension_settings[extensionName] = extension_settings[extensionName] || {}; |
| 223 | if (Object.keys(extension_settings[extensionName]).length === 0) { |
| 224 | Object.assign(extension_settings[extensionName], defaultSettings); |
| 225 | saveSettingsDebounced(); |
| 226 | } |
| 227 | |
| 228 | // Set global CFG values on load |
| 229 | $('#global_cfg_guidance_scale').val(extension_settings.cfg.global.guidance_scale); |
| 230 | $('#global_cfg_guidance_scale_counter').val(extension_settings.cfg.global.guidance_scale.toFixed(2)); |
| 231 | $('#global_cfg_negative_prompt').val(extension_settings.cfg.global.negative_prompt); |
| 232 | $('#global_cfg_positive_prompt').val(extension_settings.cfg.global.positive_prompt); |
| 233 | } |
| 234 | |
| 235 | function migrateSettings() { |
| 236 | let performSettingsSave = false; |
| 237 | let performMetaSave = false; |
| 238 | |
| 239 | if (power_user.guidance_scale) { |
| 240 | extension_settings.cfg.global.guidance_scale = power_user.guidance_scale; |
| 241 | delete power_user.guidance_scale; |
| 242 | performSettingsSave = true; |
| 243 | } |
| 244 | |
| 245 | if (power_user.negative_prompt) { |
| 246 | extension_settings.cfg.global.negative_prompt = power_user.negative_prompt; |
| 247 | delete power_user.negative_prompt; |
| 248 | performSettingsSave = true; |
| 249 | } |
| 250 | |
| 251 | if (chat_metadata.cfg_negative_combine) { |
| 252 | chat_metadata[metadataKeys.prompt_combine] = chat_metadata.cfg_negative_combine; |
| 253 | chat_metadata.cfg_negative_combine = undefined; |
| 254 | performMetaSave = true; |
| 255 | } |
| 256 | |
| 257 | if (chat_metadata.cfg_negative_insertion_depth) { |
| 258 | chat_metadata[metadataKeys.prompt_insertion_depth] = chat_metadata.cfg_negative_insertion_depth; |
| 259 | chat_metadata.cfg_negative_insertion_depth = undefined; |
| 260 | performMetaSave = true; |
| 261 | } |
| 262 | |
| 263 | if (chat_metadata.cfg_negative_separator) { |
| 264 | chat_metadata[metadataKeys.prompt_separator] = chat_metadata.cfg_negative_separator; |
| 265 | chat_metadata.cfg_negative_separator = undefined; |
| 266 | performMetaSave = true; |
| 267 | } |
| 268 | |
| 269 | if (performSettingsSave) { |
| 270 | saveSettingsDebounced(); |
| 271 | } |
| 272 | |
| 273 | if (performMetaSave) { |
| 274 | saveMetadataDebounced(); |
| 275 | } |
| 276 | } |
| 277 | |
| 278 | // This function is called when the extension is loaded |
| 279 | export function initCfg() { |
| 280 | $('#CFGClose').on('click', function () { |
| 281 | $('#cfgConfig').transition({ |
| 282 | opacity: 0, |
| 283 | duration: animation_duration, |
| 284 | easing: 'ease-in-out', |
| 285 | }); |
| 286 | setTimeout(function () { $('#cfgConfig').hide(); }, animation_duration); |
| 287 | }); |
| 288 | |
| 289 | $('#chat_cfg_guidance_scale').on('input', function () { |
| 290 | const numberValue = Number($(this).val()); |
| 291 | const success = setChatCfg(numberValue, settingType.guidance_scale); |
| 292 | if (success) { |
| 293 | $('#chat_cfg_guidance_scale_counter').val(numberValue.toFixed(2)); |
| 294 | } |
| 295 | }); |
| 296 | |
| 297 | $('#chat_cfg_negative_prompt').on('input', function () { |
| 298 | setChatCfg($(this).val(), settingType.negative_prompt); |
| 299 | }); |
| 300 | |
| 301 | $('#chat_cfg_positive_prompt').on('input', function () { |
| 302 | setChatCfg($(this).val(), settingType.positive_prompt); |
| 303 | }); |
| 304 | |
| 305 | $('#chara_cfg_guidance_scale').on('input', function () { |
| 306 | const value = $(this).val(); |
| 307 | const success = setCharCfg(value, settingType.guidance_scale); |
| 308 | if (success) { |
| 309 | $('#chara_cfg_guidance_scale_counter').val(Number(value).toFixed(2)); |
| 310 | } |
| 311 | }); |
| 312 | |
| 313 | $('#chara_cfg_negative_prompt').on('input', function () { |
| 314 | setCharCfg($(this).val(), settingType.negative_prompt); |
| 315 | }); |
| 316 | |
| 317 | $('#chara_cfg_positive_prompt').on('input', function () { |
| 318 | setCharCfg($(this).val(), settingType.positive_prompt); |
| 319 | }); |
| 320 | |
| 321 | $('#global_cfg_guidance_scale').on('input', function () { |
| 322 | extension_settings.cfg.global.guidance_scale = Number($(this).val()); |
| 323 | $('#global_cfg_guidance_scale_counter').val(extension_settings.cfg.global.guidance_scale.toFixed(2)); |
| 324 | saveSettingsDebounced(); |
| 325 | }); |
| 326 | |
| 327 | $('#global_cfg_negative_prompt').on('input', function () { |
| 328 | extension_settings.cfg.global.negative_prompt = $(this).val(); |
| 329 | saveSettingsDebounced(); |
| 330 | }); |
| 331 | |
| 332 | $('#global_cfg_positive_prompt').on('input', function () { |
| 333 | extension_settings.cfg.global.positive_prompt = $(this).val(); |
| 334 | saveSettingsDebounced(); |
| 335 | }); |
| 336 | |
| 337 | $('input[name="cfg_prompt_combine"]').on('input', function () { |
| 338 | const values = $('#cfgConfig').find('input[name="cfg_prompt_combine"]') |
| 339 | .filter(':checked') |
| 340 | .map(function () { return Number($(this).val()); }) |
| 341 | .get() |
| 342 | .filter((e) => !Number.isNaN(e)) || []; |
| 343 | |
| 344 | chat_metadata[metadataKeys.prompt_combine] = values; |
| 345 | saveMetadataDebounced(); |
| 346 | }); |
| 347 | |
| 348 | $('#cfg_prompt_insertion_depth').on('input', function () { |
| 349 | chat_metadata[metadataKeys.prompt_insertion_depth] = Number($(this).val()); |
| 350 | saveMetadataDebounced(); |
| 351 | }); |
| 352 | |
| 353 | $('#cfg_prompt_separator').on('input', function () { |
| 354 | chat_metadata[metadataKeys.prompt_separator] = $(this).val(); |
| 355 | saveMetadataDebounced(); |
| 356 | }); |
| 357 | |
| 358 | $('#groupchat_cfg_use_chara').on('input', function () { |
| 359 | const checked = !!$(this).prop('checked'); |
| 360 | chat_metadata[metadataKeys.groupchat_individual_chars] = checked; |
| 361 | |
| 362 | if (checked) { |
| 363 | toastr.info('You can edit character CFG values in their respective character chats.'); |
| 364 | } |
| 365 | |
| 366 | saveMetadataDebounced(); |
| 367 | }); |
| 368 | |
| 369 | initialLoadSettings(); |
| 370 | |
| 371 | if (extension_settings.cfg) { |
| 372 | migrateSettings(); |
| 373 | } |
| 374 | |
| 375 | $('#option_toggle_CFG').on('click', onCfgMenuItemClick); |
| 376 | |
| 377 | // Hook events |
| 378 | eventSource.on(event_types.CHAT_CHANGED, async () => { |
| 379 | await onChatChanged(); |
| 380 | }); |
| 381 | } |
| 382 | |
| 383 | export const cfgType = { |
| 384 | chat: 0, |
| 385 | chara: 1, |
| 386 | global: 2, |
| 387 | }; |
| 388 | |
| 389 | export const metadataKeys = { |
| 390 | guidance_scale: 'cfg_guidance_scale', |
| 391 | negative_prompt: 'cfg_negative_prompt', |
| 392 | positive_prompt: 'cfg_positive_prompt', |
| 393 | prompt_combine: 'cfg_prompt_combine', |
| 394 | groupchat_individual_chars: 'cfg_groupchat_individual_chars', |
| 395 | prompt_insertion_depth: 'cfg_prompt_insertion_depth', |
| 396 | prompt_separator: 'cfg_prompt_separator', |
| 397 | }; |
| 398 | |
| 399 | // Gets the CFG guidance scale |
| 400 | // If the guidance scale is 1, ignore the CFG prompt(s) since it won't be used anyways |
| 401 | export function getGuidanceScale() { |
| 402 | if (!extension_settings.cfg) { |
| 403 | console.warn('CFG extension is not enabled. Skipping CFG guidance.'); |
| 404 | return; |
| 405 | } |
| 406 | |
| 407 | const charaCfg = extension_settings.cfg.chara?.find((e) => e.name === getCharaFilename(this_chid)); |
| 408 | const chatGuidanceScale = chat_metadata[metadataKeys.guidance_scale]; |
| 409 | const groupchatCharOverride = chat_metadata[metadataKeys.groupchat_individual_chars] ?? false; |
| 410 | |
| 411 | if (chatGuidanceScale && chatGuidanceScale !== 1 && !groupchatCharOverride) { |
| 412 | return { |
| 413 | type: cfgType.chat, |
| 414 | value: chatGuidanceScale, |
| 415 | }; |
| 416 | } |
| 417 | |
| 418 | if ((!selected_group && charaCfg || groupchatCharOverride) && charaCfg?.guidance_scale !== 1) { |
| 419 | return { |
| 420 | type: cfgType.chara, |
| 421 | value: charaCfg.guidance_scale, |
| 422 | }; |
| 423 | } |
| 424 | |
| 425 | if (extension_settings.cfg.global && extension_settings.cfg.global?.guidance_scale !== 1) { |
| 426 | return { |
| 427 | type: cfgType.global, |
| 428 | value: extension_settings.cfg.global.guidance_scale, |
| 429 | }; |
| 430 | } |
| 431 | } |
| 432 | |
| 433 | /** |
| 434 | * Gets the CFG prompt separator. |
| 435 | * @returns {string} The CFG prompt separator |
| 436 | */ |
| 437 | function getCustomSeparator() { |
| 438 | const defaultSeparator = '\n'; |
| 439 | |
| 440 | try { |
| 441 | if (chat_metadata[metadataKeys.prompt_separator]) { |
| 442 | return JSON.parse(chat_metadata[metadataKeys.prompt_separator]); |
| 443 | } |
| 444 | |
| 445 | return defaultSeparator; |
| 446 | } catch { |
| 447 | console.warn('Invalid JSON detected for prompt separator. Using default separator.'); |
| 448 | return defaultSeparator; |
| 449 | } |
| 450 | } |
| 451 | |
| 452 | /** |
| 453 | * Gets the CFG prompt based on the guidance scale. |
| 454 | * @param {{type: number, value: number}} guidanceScale The CFG guidance scale |
| 455 | * @param {boolean} isNegative Whether to get the negative prompt |
| 456 | * @param {boolean} quiet Whether to suppress console output |
| 457 | * @returns {{value: string, depth: number}} The CFG prompt and insertion depth |
| 458 | */ |
| 459 | export function getCfgPrompt(guidanceScale, isNegative, quiet = false) { |
| 460 | let splitCfgPrompt = []; |
| 461 | |
| 462 | const cfgPromptCombine = chat_metadata[metadataKeys.prompt_combine] ?? []; |
| 463 | if (guidanceScale.type === cfgType.chat || cfgPromptCombine.includes(cfgType.chat)) { |
| 464 | splitCfgPrompt.unshift( |
| 465 | substituteParams( |
| 466 | chat_metadata[isNegative ? metadataKeys.negative_prompt : metadataKeys.positive_prompt], |
| 467 | ), |
| 468 | ); |
| 469 | } |
| 470 | |
| 471 | const charaCfg = extension_settings.cfg.chara?.find((e) => e.name === getCharaFilename(this_chid)); |
| 472 | if (guidanceScale.type === cfgType.chara || cfgPromptCombine.includes(cfgType.chara)) { |
| 473 | splitCfgPrompt.unshift( |
| 474 | substituteParams( |
| 475 | isNegative ? charaCfg.negative_prompt : charaCfg.positive_prompt, |
| 476 | ), |
| 477 | ); |
| 478 | } |
| 479 | |
| 480 | if (guidanceScale.type === cfgType.global || cfgPromptCombine.includes(cfgType.global)) { |
| 481 | splitCfgPrompt.unshift( |
| 482 | substituteParams( |
| 483 | isNegative ? extension_settings.cfg.global.negative_prompt : extension_settings.cfg.global.positive_prompt, |
| 484 | ), |
| 485 | ); |
| 486 | } |
| 487 | |
| 488 | const customSeparator = getCustomSeparator(); |
| 489 | const combinedCfgPrompt = splitCfgPrompt.filter((e) => e.length > 0).join(customSeparator); |
| 490 | const insertionDepth = chat_metadata[metadataKeys.prompt_insertion_depth] ?? 1; |
| 491 | !quiet && console.log(`Setting CFG with guidance scale: ${guidanceScale.value}, negatives: ${combinedCfgPrompt}`); |
| 492 | |
| 493 | return { |
| 494 | value: combinedCfgPrompt, |
| 495 | depth: insertionDepth, |
| 496 | }; |
| 497 | } |