Refactor and JSDoc extensions.js
| @@ -12,3 +12,4 @@ access.log | ||
| 12 | 12 | /data |
| 13 | 13 | /cache |
| 14 | 14 | .DS_Store |
| 15 | +/public/scripts/extensions/third-party | |
| @@ -11,3 +11,4 @@ access.log | ||
| 11 | 11 | .github |
| 12 | 12 | .vscode |
| 13 | 13 | .git |
| 14 | +/public/scripts/extensions/third-party | |
| @@ -116,11 +116,6 @@ input.extension_missing[type="checkbox"] { | ||
| 116 | 116 | opacity: 0.5; |
| 117 | 117 | } |
| 118 | 118 | |
| 119 | -#extensions_list .disabled { | |
| 120 | - text-decoration: line-through; | |
| 121 | - color: lightgray; | |
| 122 | -} | |
| 123 | - | |
| 124 | 119 | .update-button { |
| 125 | 120 | margin-right: 10px; |
| 126 | 121 | display: inline-flex; |
| @@ -8,19 +8,16 @@ import { isSubsetOf, setValueByPath } from './utils.js'; | ||
| 8 | 8 | import { getContext } from './st-context.js'; |
| 9 | 9 | import { isAdmin } from './user.js'; |
| 10 | 10 | import { t } from './i18n.js'; |
| 11 | +import { debounce_timeout } from './constants.js'; | |
| 12 | + | |
| 11 | 13 | export { |
| 12 | 14 | getContext, |
| 13 | 15 | getApiUrl, |
| 14 | - loadExtensionSettings, | |
| 15 | - runGenerationInterceptors, | |
| 16 | - doExtrasFetch, | |
| 17 | - modules, | |
| 18 | - extension_settings, | |
| 19 | - ModuleWorkerWrapper, | |
| 20 | 16 | }; |
| 21 | 17 | |
| 22 | 18 | /** @type {string[]} */ |
| 23 | 19 | export let extensionNames = []; |
| 20 | + | |
| 24 | 21 | /** |
| 25 | 22 | * Holds the type of each extension. |
| 26 | 23 | * Don't use this directly, use getExtensionType instead! |
| @@ -28,13 +25,35 @@ export let extensionNames = []; | ||
| 28 | 25 | */ |
| 29 | 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 | 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 | 54 | let requiresReload = false; |
| 37 | 55 | let stateChanged = false; |
| 56 | +let saveMetadataTimeout = null; | |
| 38 | 57 | |
| 39 | 58 | export function saveMetadataDebounced() { |
| 40 | 59 | const context = getContext(); |
| @@ -59,9 +78,9 @@ export function saveMetadataDebounced() { | ||
| 59 | 78 | } |
| 60 | 79 | |
| 61 | 80 | console.debug('Saving metadata...'); |
| 62 | 81 | await newContext.saveMetadata(); |
| 63 | 82 | console.debug('Saved metadata...'); |
| 64 | 83 | }, 1000debounce_timeout.relaxed); |
| 65 | 84 | } |
| 66 | 85 | |
| 67 | 86 | /** |
| @@ -91,7 +110,7 @@ export function renderExtensionTemplateAsync(extensionName, templateId, template | ||
| 91 | 110 | } |
| 92 | 111 | |
| 93 | 112 | // Disables parallel updates |
| 94 | 113 | export class ModuleWorkerWrapper { |
| 95 | 114 | constructor(callback) { |
| 96 | 115 | this.isBusy = false; |
| 97 | 116 | this.callback = callback; |
| @@ -115,7 +134,7 @@ class ModuleWorkerWrapper { | ||
| 115 | 134 | } |
| 116 | 135 | } |
| 117 | 136 | |
| 118 | 137 | export const extension_settings = { |
| 119 | 138 | apiUrl: defaultUrl, |
| 120 | 139 | apiKey: '', |
| 121 | 140 | autoConnect: false, |
| @@ -180,12 +199,6 @@ const extension_settings = { | ||
| 180 | 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 | 202 | function showHideExtensionsMenu() { |
| 190 | 203 | // Get the number of menu items that are not hidden |
| 191 | 204 | const hasMenuItems = $('#extensionsMenu').children().filter((_, child) => $(child).css('display') !== 'none').length > 0; |
| @@ -212,7 +225,13 @@ function getExtensionType(externalId) { | ||
| 212 | 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 | 235 | if (!args) { |
| 217 | 236 | args = {}; |
| 218 | 237 | } |
| @@ -231,8 +250,7 @@ async function doExtrasFetch(endpoint, args) { | ||
| 231 | 250 | }); |
| 232 | 251 | } |
| 233 | 252 | |
| 234 | 253 | const response =return await fetch(endpoint, args); |
| 235 | - return response; | |
| 236 | 254 | } |
| 237 | 255 | |
| 238 | 256 | /** |
| @@ -267,6 +285,11 @@ function onEnableExtensionClick() { | ||
| 267 | 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 | 293 | export async function enableExtension(name, reload = true) { |
| 271 | 294 | extension_settings.disabledExtensions = extension_settings.disabledExtensions.filter(x => x !== name); |
| 272 | 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 | 309 | export async function disableExtension(name, reload = true) { |
| 282 | 310 | extension_settings.disabledExtensions.push(name); |
| 283 | 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 | 325 | async function getManifests(names) { |
| 293 | 326 | const obj = {}; |
| 294 | 327 | const promises = []; |
| @@ -316,6 +349,10 @@ async function getManifests(names) { | ||
| 316 | 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 | 356 | async function activateExtensions() { |
| 320 | 357 | const extensions = Object.entries(manifests).sort((a, b) => a[1].loading_order - b[1].loading_order); |
| 321 | 358 | const promises = []; |
| @@ -323,36 +360,25 @@ async function activateExtensions() { | ||
| 323 | 360 | for (let entry of extensions) { |
| 324 | 361 | const name = entry[0]; |
| 325 | 362 | const manifest = entry[1]; |
| 326 | - const elementExists = document.getElementById(name) !== null; | |
| 327 | 363 | |
| 328 | 364 | if (elementExists || activeExtensions.has(name)) { |
| 329 | 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)) { | |
| 334 | - try { | |
| 335 | 369 | const isDisabled = extension_settings.disabledExtensions.includes(name); |
| 336 | - const li = document.createElement('li'); | |
| 337 | 370 | |
| 338 | 371 | if (meetsModuleRequirements && !isDisabled) { |
| 372 | + try { | |
| 373 | + console.debug('Activating extension', name); | |
| 339 | 374 | const promise = Promise.all([addExtensionScript(name, manifest), addExtensionStyle(name, manifest)]); |
| 340 | 375 | await promise |
| 341 | 376 | .then(() => activeExtensions.add(name)) |
| 342 | 377 | .catch(err => console.log('Could not activate extension: ' +, name, err)); |
| 343 | 378 | promises.push(promise); |
| 344 | 379 | } |
| 345 | - else { | |
| 346 | - li.classList.add('disabled'); | |
| 347 | - } | |
| 348 | - | |
| 349 | - li.id = name; | |
| 350 | - li.innerText = manifest.display_name; | |
| 351 | - | |
| 352 | - $('#extensions_list').append(li); | |
| 353 | - } | |
| 354 | 380 | catch (error) { |
| 355 | 381 | console.error(`'Could not activate extension:', ${name}`); |
| 356 | 382 | console.error(error); |
| 357 | 383 | } |
| 358 | 384 | } |
| @@ -362,8 +388,8 @@ async function activateExtensions() { | ||
| 362 | 388 | } |
| 363 | 389 | |
| 364 | 390 | async function connectClickHandler() { |
| 365 | 391 | const baseUrl = String($('#extensions_url').val()); |
| 366 | 392 | extension_settings.apiUrl = String(baseUrl); |
| 367 | 393 | const testApiKey = $('#extensions_api_key').val(); |
| 368 | 394 | extension_settings.apiKey = String(testApiKey); |
| 369 | 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 | 457 | async function connectToApi(baseUrl) { |
| 442 | 458 | if (!baseUrl) { |
| 443 | 459 | return; |
| @@ -453,7 +469,7 @@ async function connectToApi(baseUrl) { | ||
| 453 | 469 | const data = await getExtensionsResult.json(); |
| 454 | 470 | modules = data.modules; |
| 455 | 471 | await activateExtensions(); |
| 456 | 472 | await eventSource.emit(event_types.EXTRAS_CONNECTED, modules); |
| 457 | 473 | } |
| 458 | 474 | |
| 459 | 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 | 486 | function updateStatus(success) { |
| 467 | 487 | connectedToApi = success; |
| 468 | 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 | 997 | * @param {boolean} versionChanged Is this a version change? |
| 978 | 998 | * @param {boolean} enableAutoUpdate Enable auto-update |
| 979 | 999 | */ |
| 980 | 1000 | export async function loadExtensionSettings(settings, versionChanged, enableAutoUpdate) { |
| 981 | 1001 | if (settings.extension_settings) { |
| 982 | 1002 | Object.assign(extension_settings, settings.extension_settings); |
| 983 | 1003 | } |
| @@ -1149,7 +1169,7 @@ async function autoUpdateExtensions(forceAll) { | ||
| 1149 | 1169 | * @param {number} contextSize Context size |
| 1150 | 1170 | * @returns {Promise<boolean>} True if generation should be aborted |
| 1151 | 1171 | */ |
| 1152 | 1172 | export async function runGenerationInterceptors(chat, contextSize) { |
| 1153 | 1173 | let aborted = false; |
| 1154 | 1174 | let exitImmediately = false; |
| 1155 | 1175 | |