Refactor and JSDoc extensions.js
| @@ -12,3 +12,4 @@ access.log | |||
| 12 | /data | 12 | /data |
| 13 | /cache | 13 | /cache |
| 14 | .DS_Store | 14 | .DS_Store |
| 15 | /public/scripts/extensions/third-party | ||
| @@ -11,3 +11,4 @@ access.log | |||
| 11 | .github | 11 | .github |
| 12 | .vscode | 12 | .vscode |
| 13 | .git | 13 | .git |
| 14 | /public/scripts/extensions/third-party | ||
| @@ -116,11 +116,6 @@ input.extension_missing[type="checkbox"] { | |||
| 116 | opacity: 0.5; | 116 | opacity: 0.5; |
| 117 | } | 117 | } |
| 118 | 118 | ||
| 119 | #extensions_list .disabled { | ||
| 120 | text-decoration: line-through; | ||
| 121 | color: lightgray; | ||
| 122 | } | ||
| 123 | |||
| 124 | .update-button { | 119 | .update-button { |
| 125 | margin-right: 10px; | 120 | margin-right: 10px; |
| 126 | display: inline-flex; | 121 | display: inline-flex; |
| @@ -8,19 +8,16 @@ import { isSubsetOf, setValueByPath } from './utils.js'; | |||
| 8 | import { getContext } from './st-context.js'; | 8 | import { getContext } from './st-context.js'; |
| 9 | import { isAdmin } from './user.js'; | 9 | import { isAdmin } from './user.js'; |
| 10 | import { t } from './i18n.js'; | 10 | import { t } from './i18n.js'; |
| 11 | import { debounce_timeout } from './constants.js'; | ||
| 12 | |||
| 11 | export { | 13 | export { |
| 12 | getContext, | 14 | getContext, |
| 13 | getApiUrl, | 15 | getApiUrl, |
| 14 | loadExtensionSettings, | ||
| 15 | runGenerationInterceptors, | ||
| 16 | doExtrasFetch, | ||
| 17 | modules, | ||
| 18 | extension_settings, | ||
| 19 | ModuleWorkerWrapper, | ||
| 20 | }; | 16 | }; |
| 21 | 17 | ||
| 22 | /** @type {string[]} */ | 18 | /** @type {string[]} */ |
| 23 | export let extensionNames = []; | 19 | export let extensionNames = []; |
| 20 | |||
| 24 | /** | 21 | /** |
| 25 | * Holds the type of each extension. | 22 | * Holds the type of each extension. |
| 26 | * Don't use this directly, use getExtensionType instead! | 23 | * Don't use this directly, use getExtensionType instead! |
| @@ -28,13 +25,35 @@ export let extensionNames = []; | |||
| 28 | */ | 25 | */ |
| 29 | export let extensionTypes = {}; | 26 | export let extensionTypes = {}; |
| 30 | 27 | ||
| 28 | /** | ||
| 29 | * A list of active modules provided by the Extras API. | ||
| 30 | * @type {string[]} | ||
| 31 | */ | ||
| 32 | export let modules = []; | ||
| 33 | |||
| 34 | /** | ||
| 35 | * A set of active extensions. | ||
| 36 | * @type {Set<string>} | ||
| 37 | */ | ||
| 38 | let activeExtensions = new Set(); | ||
| 39 | |||
| 40 | const getApiUrl = () => extension_settings.apiUrl; | ||
| 41 | let connectedToApi = false; | ||
| 42 | |||
| 43 | /** | ||
| 44 | * Holds manifest data for each extension. | ||
| 45 | * @type {Record<string, object>} | ||
| 46 | */ | ||
| 31 | let manifests = {}; | 47 | let manifests = {}; |
| 32 | const defaultUrl = 'http://localhost:5100'; | ||
| 33 | 48 | ||
| 34 | let saveMetadataTimeout = null; | 49 | /** |
| 50 | * Default URL for the Extras API. | ||
| 51 | */ | ||
| 52 | const defaultUrl = 'http://localhost:5100'; | ||
| 35 | 53 | ||
| 36 | let requiresReload = false; | 54 | let requiresReload = false; |
| 37 | let stateChanged = false; | 55 | let stateChanged = false; |
| 56 | let saveMetadataTimeout = null; | ||
| 38 | 57 | ||
| 39 | export function saveMetadataDebounced() { | 58 | export function saveMetadataDebounced() { |
| 40 | const context = getContext(); | 59 | const context = getContext(); |
| @@ -59,9 +78,9 @@ export function saveMetadataDebounced() { | |||
| 59 | } | 78 | } |
| 60 | 79 | ||
| 61 | console.debug('Saving metadata...'); | 80 | console.debug('Saving metadata...'); |
| 62 | newContext.saveMetadata(); | 81 | await newContext.saveMetadata(); |
| 63 | console.debug('Saved metadata...'); | 82 | console.debug('Saved metadata...'); |
| 64 | }, 1000); | 83 | }, debounce_timeout.relaxed); |
| 65 | } | 84 | } |
| 66 | 85 | ||
| 67 | /** | 86 | /** |
| @@ -91,7 +110,7 @@ export function renderExtensionTemplateAsync(extensionName, templateId, template | |||
| 91 | } | 110 | } |
| 92 | 111 | ||
| 93 | // Disables parallel updates | 112 | // Disables parallel updates |
| 94 | class ModuleWorkerWrapper { | 113 | export class ModuleWorkerWrapper { |
| 95 | constructor(callback) { | 114 | constructor(callback) { |
| 96 | this.isBusy = false; | 115 | this.isBusy = false; |
| 97 | this.callback = callback; | 116 | this.callback = callback; |
| @@ -115,7 +134,7 @@ class ModuleWorkerWrapper { | |||
| 115 | } | 134 | } |
| 116 | } | 135 | } |
| 117 | 136 | ||
| 118 | const extension_settings = { | 137 | export const extension_settings = { |
| 119 | apiUrl: defaultUrl, | 138 | apiUrl: defaultUrl, |
| 120 | apiKey: '', | 139 | apiKey: '', |
| 121 | autoConnect: false, | 140 | autoConnect: false, |
| @@ -180,12 +199,6 @@ const extension_settings = { | |||
| 180 | disabled_attachments: [], | 199 | disabled_attachments: [], |
| 181 | }; | 200 | }; |
| 182 | 201 | ||
| 183 | let modules = []; | ||
| 184 | let activeExtensions = new Set(); | ||
| 185 | |||
| 186 | const getApiUrl = () => extension_settings.apiUrl; | ||
| 187 | let connectedToApi = false; | ||
| 188 | |||
| 189 | function showHideExtensionsMenu() { | 202 | function showHideExtensionsMenu() { |
| 190 | // Get the number of menu items that are not hidden | 203 | // Get the number of menu items that are not hidden |
| 191 | const hasMenuItems = $('#extensionsMenu').children().filter((_, child) => $(child).css('display') !== 'none').length > 0; | 204 | const hasMenuItems = $('#extensionsMenu').children().filter((_, child) => $(child).css('display') !== 'none').length > 0; |
| @@ -212,7 +225,13 @@ function getExtensionType(externalId) { | |||
| 212 | return id ? extensionTypes[id] : ''; | 225 | return id ? extensionTypes[id] : ''; |
| 213 | } | 226 | } |
| 214 | 227 | ||
| 215 | async function doExtrasFetch(endpoint, args) { | 228 | /** |
| 229 | * Performs a fetch of the Extras API. | ||
| 230 | * @param {string|URL} endpoint Extras API endpoint | ||
| 231 | * @param {RequestInit} args Request arguments | ||
| 232 | * @returns {Promise<Response>} Response from the fetch | ||
| 233 | */ | ||
| 234 | export async function doExtrasFetch(endpoint, args = {}) { | ||
| 216 | if (!args) { | 235 | if (!args) { |
| 217 | args = {}; | 236 | args = {}; |
| 218 | } | 237 | } |
| @@ -231,8 +250,7 @@ async function doExtrasFetch(endpoint, args) { | |||
| 231 | }); | 250 | }); |
| 232 | } | 251 | } |
| 233 | 252 | ||
| 234 | const response = await fetch(endpoint, args); | 253 | return await fetch(endpoint, args); |
| 235 | return response; | ||
| 236 | } | 254 | } |
| 237 | 255 | ||
| 238 | /** | 256 | /** |
| @@ -267,6 +285,11 @@ function onEnableExtensionClick() { | |||
| 267 | enableExtension(name, false); | 285 | enableExtension(name, false); |
| 268 | } | 286 | } |
| 269 | 287 | ||
| 288 | /** | ||
| 289 | * Enables an extension by name. | ||
| 290 | * @param {string} name Extension name | ||
| 291 | * @param {boolean} [reload=true] If true, reload the page after enabling the extension | ||
| 292 | */ | ||
| 270 | export async function enableExtension(name, reload = true) { | 293 | export async function enableExtension(name, reload = true) { |
| 271 | extension_settings.disabledExtensions = extension_settings.disabledExtensions.filter(x => x !== name); | 294 | extension_settings.disabledExtensions = extension_settings.disabledExtensions.filter(x => x !== name); |
| 272 | stateChanged = true; | 295 | stateChanged = true; |
| @@ -278,6 +301,11 @@ export async function enableExtension(name, reload = true) { | |||
| 278 | } | 301 | } |
| 279 | } | 302 | } |
| 280 | 303 | ||
| 304 | /** | ||
| 305 | * Disables an extension by name. | ||
| 306 | * @param {string} name Extension name | ||
| 307 | * @param {boolean} [reload=true] If true, reload the page after disabling the extension | ||
| 308 | */ | ||
| 281 | export async function disableExtension(name, reload = true) { | 309 | export async function disableExtension(name, reload = true) { |
| 282 | extension_settings.disabledExtensions.push(name); | 310 | extension_settings.disabledExtensions.push(name); |
| 283 | stateChanged = true; | 311 | stateChanged = true; |
| @@ -289,6 +317,11 @@ export async function disableExtension(name, reload = true) { | |||
| 289 | } | 317 | } |
| 290 | } | 318 | } |
| 291 | 319 | ||
| 320 | /** | ||
| 321 | * Loads manifest.json files for extensions. | ||
| 322 | * @param {string[]} names Array of extension names | ||
| 323 | * @returns {Promise<Record<string, object>>} Object with extension names as keys and their manifests as values | ||
| 324 | */ | ||
| 292 | async function getManifests(names) { | 325 | async function getManifests(names) { |
| 293 | const obj = {}; | 326 | const obj = {}; |
| 294 | const promises = []; | 327 | const promises = []; |
| @@ -316,6 +349,10 @@ async function getManifests(names) { | |||
| 316 | return obj; | 349 | return obj; |
| 317 | } | 350 | } |
| 318 | 351 | ||
| 352 | /** | ||
| 353 | * Tries to activate all available extensions that are not already active. | ||
| 354 | * @returns {Promise<void>} | ||
| 355 | */ | ||
| 319 | async function activateExtensions() { | 356 | async function activateExtensions() { |
| 320 | const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order); | 357 | const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order); |
| 321 | const promises = []; | 358 | const promises = []; |
| @@ -323,36 +360,25 @@ async function activateExtensions() { | |||
| 323 | for (let entry of extensions) { | 360 | for (let entry of extensions) { |
| 324 | const name = entry[0]; | 361 | const name = entry[0]; |
| 325 | const manifest = entry[1]; | 362 | const manifest = entry[1]; |
| 326 | const elementExists = document.getElementById(name) !== null; | ||
| 327 | 363 | ||
| 328 | if (elementExists || activeExtensions.has(name)) { | 364 | if (activeExtensions.has(name)) { |
| 329 | continue; | 365 | continue; |
| 330 | } | 366 | } |
| 331 | 367 | ||
| 332 | // all required modules are active (offline extensions require none) | 368 | const meetsModuleRequirements = !Array.isArray(manifest.requires) || isSubsetOf(modules, manifest.requires); |
| 333 | if (isSubsetOf(modules, manifest.requires)) { | 369 | const isDisabled = extension_settings.disabledExtensions.includes(name); |
| 334 | try { | ||
| 335 | const isDisabled = extension_settings.disabledExtensions.includes(name); | ||
| 336 | const li = document.createElement('li'); | ||
| 337 | |||
| 338 | if (!isDisabled) { | ||
| 339 | const promise = Promise.all([addExtensionScript(name, manifest), addExtensionStyle(name, manifest)]); | ||
| 340 | await promise | ||
| 341 | .then(() => activeExtensions.add(name)) | ||
| 342 | .catch(err => console.log('Could not activate extension: ' + name, err)); | ||
| 343 | promises.push(promise); | ||
| 344 | } | ||
| 345 | else { | ||
| 346 | li.classList.add('disabled'); | ||
| 347 | } | ||
| 348 | |||
| 349 | li.id = name; | ||
| 350 | li.innerText = manifest.display_name; | ||
| 351 | 370 | ||
| 352 | $('#extensions_list').append(li); | 371 | if (meetsModuleRequirements && !isDisabled) { |
| 372 | try { | ||
| 373 | console.debug('Activating extension', name); | ||
| 374 | const promise = Promise.all([addExtensionScript(name, manifest), addExtensionStyle(name, manifest)]); | ||
| 375 | await promise | ||
| 376 | .then(() => activeExtensions.add(name)) | ||
| 377 | .catch(err => console.log('Could not activate extension', name, err)); | ||
| 378 | promises.push(promise); | ||
| 353 | } | 379 | } |
| 354 | catch (error) { | 380 | catch (error) { |
| 355 | console.error(`Could not activate extension: ${name}`); | 381 | console.error('Could not activate extension', name); |
| 356 | console.error(error); | 382 | console.error(error); |
| 357 | } | 383 | } |
| 358 | } | 384 | } |
| @@ -362,8 +388,8 @@ async function activateExtensions() { | |||
| 362 | } | 388 | } |
| 363 | 389 | ||
| 364 | async function connectClickHandler() { | 390 | async function connectClickHandler() { |
| 365 | const baseUrl = $('#extensions_url').val(); | 391 | const baseUrl = String($('#extensions_url').val()); |
| 366 | extension_settings.apiUrl = String(baseUrl); | 392 | extension_settings.apiUrl = baseUrl; |
| 367 | const testApiKey = $('#extensions_api_key').val(); | 393 | const testApiKey = $('#extensions_api_key').val(); |
| 368 | extension_settings.apiKey = String(testApiKey); | 394 | extension_settings.apiKey = String(testApiKey); |
| 369 | saveSettingsDebounced(); | 395 | saveSettingsDebounced(); |
| @@ -423,21 +449,11 @@ function notifyUpdatesInputHandler() { | |||
| 423 | } | 449 | } |
| 424 | } | 450 | } |
| 425 | 451 | ||
| 426 | /* $(document).on('click', function (e) { | 452 | /** |
| 427 | const target = $(e.target); | 453 | * Connects to the Extras API. |
| 428 | if (target.is(dropdown)) return; | 454 | * @param {string} baseUrl Extras API base URL |
| 429 | if (target.is(button) && dropdown.is(':hidden')) { | 455 | * @returns {Promise<void>} |
| 430 | dropdown.toggle(200); | 456 | */ |
| 431 | popper.update(); | ||
| 432 | } | ||
| 433 | if (target !== dropdown && | ||
| 434 | target !== button && | ||
| 435 | dropdown.is(":visible")) { | ||
| 436 | dropdown.hide(200); | ||
| 437 | } | ||
| 438 | }); | ||
| 439 | } */ | ||
| 440 | |||
| 441 | async function connectToApi(baseUrl) { | 457 | async function connectToApi(baseUrl) { |
| 442 | if (!baseUrl) { | 458 | if (!baseUrl) { |
| 443 | return; | 459 | return; |
| @@ -453,7 +469,7 @@ async function connectToApi(baseUrl) { | |||
| 453 | const data = await getExtensionsResult.json(); | 469 | const data = await getExtensionsResult.json(); |
| 454 | modules = data.modules; | 470 | modules = data.modules; |
| 455 | await activateExtensions(); | 471 | await activateExtensions(); |
| 456 | eventSource.emit(event_types.EXTRAS_CONNECTED, modules); | 472 | await eventSource.emit(event_types.EXTRAS_CONNECTED, modules); |
| 457 | } | 473 | } |
| 458 | 474 | ||
| 459 | updateStatus(getExtensionsResult.ok); | 475 | updateStatus(getExtensionsResult.ok); |
| @@ -463,6 +479,10 @@ async function connectToApi(baseUrl) { | |||
| 463 | } | 479 | } |
| 464 | } | 480 | } |
| 465 | 481 | ||
| 482 | /** | ||
| 483 | * Updates the status of Extras API connection. | ||
| 484 | * @param {boolean} success Whether the connection was successful | ||
| 485 | */ | ||
| 466 | function updateStatus(success) { | 486 | function updateStatus(success) { |
| 467 | connectedToApi = success; | 487 | connectedToApi = success; |
| 468 | const _text = success ? t`Connected to API` : t`Could not connect to API`; | 488 | const _text = success ? t`Connected to API` : t`Could not connect to API`; |
| @@ -977,7 +997,7 @@ export async function installExtension(url, global) { | |||
| 977 | * @param {boolean} versionChanged Is this a version change? | 997 | * @param {boolean} versionChanged Is this a version change? |
| 978 | * @param {boolean} enableAutoUpdate Enable auto-update | 998 | * @param {boolean} enableAutoUpdate Enable auto-update |
| 979 | */ | 999 | */ |
| 980 | async function loadExtensionSettings(settings, versionChanged, enableAutoUpdate) { | 1000 | export async function loadExtensionSettings(settings, versionChanged, enableAutoUpdate) { |
| 981 | if (settings.extension_settings) { | 1001 | if (settings.extension_settings) { |
| 982 | Object.assign(extension_settings, settings.extension_settings); | 1002 | Object.assign(extension_settings, settings.extension_settings); |
| 983 | } | 1003 | } |
| @@ -1149,7 +1169,7 @@ async function autoUpdateExtensions(forceAll) { | |||
| 1149 | * @param {number} contextSize Context size | 1169 | * @param {number} contextSize Context size |
| 1150 | * @returns {Promise<boolean>} True if generation should be aborted | 1170 | * @returns {Promise<boolean>} True if generation should be aborted |
| 1151 | */ | 1171 | */ |
| 1152 | async function runGenerationInterceptors(chat, contextSize) { | 1172 | export async function runGenerationInterceptors(chat, contextSize) { |
| 1153 | let aborted = false; | 1173 | let aborted = false; |
| 1154 | let exitImmediately = false; | 1174 | let exitImmediately = false; |
| 1155 | 1175 | ||