2024-09-07 20:33:31 +02:00
import { event _types , eventSource , main _api , saveSettingsDebounced } from '../../../script.js' ;
2024-09-06 23:23:42 +02:00
import { extension _settings , renderExtensionTemplateAsync } from '../../extensions.js' ;
import { callGenericPopup , Popup , POPUP _TYPE } from '../../popup.js' ;
import { executeSlashCommandsWithOptions } from '../../slash-commands.js' ;
2024-09-07 12:58:46 +02:00
import { SlashCommand } from '../../slash-commands/SlashCommand.js' ;
2024-09-07 20:33:31 +02:00
import { ARGUMENT _TYPE , SlashCommandArgument , SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js' ;
import { commonEnumProviders , enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js' ;
2024-09-07 12:58:46 +02:00
import { enumTypes , SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js' ;
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js' ;
2024-09-07 20:33:31 +02:00
import { collapseSpaces , getUniqueName , isFalseBoolean , uuidv4 } from '../../utils.js' ;
2024-09-06 23:23:42 +02:00
const MODULE _NAME = 'connection-manager' ;
2024-09-07 12:58:46 +02:00
const NONE = '<None>' ;
2024-09-06 23:23:42 +02:00
const DEFAULT _SETTINGS = {
profiles : [ ] ,
selectedProfile : null ,
} ;
const COMMON _COMMANDS = [
'api' ,
'preset' ,
2024-09-07 12:24:56 +02:00
'api-url' ,
2024-09-06 23:23:42 +02:00
'model' ,
] ;
const CC _COMMANDS = [
... COMMON _COMMANDS ,
'proxy' ,
] ;
const TC _COMMANDS = [
... COMMON _COMMANDS ,
'instruct' ,
'context' ,
'instruct-state' ,
'tokenizer' ,
] ;
const FANCY _NAMES = {
'api' : 'API' ,
2024-09-07 12:24:56 +02:00
'api-url' : 'Server URL' ,
2024-09-06 23:23:42 +02:00
'preset' : 'Settings Preset' ,
'model' : 'Model' ,
'proxy' : 'Proxy Preset' ,
'instruct-state' : 'Instruct Mode' ,
'instruct' : 'Instruct Template' ,
'context' : 'Context Template' ,
'tokenizer' : 'Tokenizer' ,
} ;
2024-09-07 22:53:22 +02:00
/ * *
* A wrapper for the connection manager spinner .
* /
class ConnectionManagerSpinner {
/ * *
* @ type { AbortController [ ] }
* /
static abortControllers = [ ] ;
/** @type {HTMLElement} */
spinnerElement ;
/** @type {AbortController} */
abortController = new AbortController ( ) ;
constructor ( ) {
// @ts-ignore
this . spinnerElement = document . getElementById ( 'connection_profile_spinner' ) ;
this . abortController = new AbortController ( ) ;
}
start ( ) {
ConnectionManagerSpinner . abortControllers . push ( this . abortController ) ;
this . spinnerElement . classList . remove ( 'hidden' ) ;
}
stop ( ) {
this . spinnerElement . classList . add ( 'hidden' ) ;
}
isAborted ( ) {
return this . abortController . signal . aborted ;
}
static abort ( ) {
for ( const controller of ConnectionManagerSpinner . abortControllers ) {
controller . abort ( ) ;
}
ConnectionManagerSpinner . abortControllers = [ ] ;
}
}
2024-09-07 12:58:46 +02:00
/** @type {() => SlashCommandEnumValue[]} */
const profilesProvider = ( ) => [
2024-09-07 20:33:31 +02:00
new SlashCommandEnumValue ( NONE ) ,
2024-09-07 20:08:37 +02:00
... extension _settings . connectionManager . profiles . map ( p => new SlashCommandEnumValue ( p . name , null , enumTypes . name , enumIcons . server ) ) ,
2024-09-07 12:58:46 +02:00
] ;
2024-09-06 23:23:42 +02:00
/ * *
* @ typedef { Object } ConnectionProfile
* @ property { string } id Unique identifier
* @ property { string } mode Mode of the connection profile
* @ property { string } [ name ] Name of the connection profile
* @ property { string } [ api ] API
* @ property { string } [ preset ] Settings Preset
* @ property { string } [ model ] Model
* @ property { string } [ proxy ] Proxy Preset
* @ property { string } [ instruct ] Instruct Template
* @ property { string } [ context ] Context Template
* @ property { string } [ instruct - state ] Instruct Mode
* @ property { string } [ tokenizer ] Tokenizer
* /
const escapeArgument = ( a ) => a . replace ( /"/g , '\\"' ) . replace ( /\|/g , '\\|' ) ;
2024-09-07 16:13:32 +02:00
/ * *
* Finds the best match for the search value .
* @ param { string } value Search value
* @ returns { ConnectionProfile | null } Best match or null
* /
function findProfileByName ( value ) {
// Try to find exact match
const profile = extension _settings . connectionManager . profiles . find ( p => p . name === value ) ;
if ( profile ) {
return profile ;
}
// Try to find fuzzy match
const fuse = new Fuse ( extension _settings . connectionManager . profiles , { keys : [ 'name' ] } ) ;
const results = fuse . search ( value ) ;
if ( results . length === 0 ) {
return null ;
}
const bestMatch = results [ 0 ] ;
return bestMatch . item ;
}
2024-09-06 23:23:42 +02:00
/ * *
* Reads the connection profile from the commands .
* @ param { string } mode Mode of the connection profile
* @ param { ConnectionProfile } profile Connection profile
* @ param { boolean } [ cleanUp ] Whether to clean up the profile
* /
async function readProfileFromCommands ( mode , profile , cleanUp = false ) {
const commands = mode === 'cc' ? CC _COMMANDS : TC _COMMANDS ;
const opposingCommands = mode === 'cc' ? TC _COMMANDS : CC _COMMANDS ;
for ( const command of commands ) {
const commandText = ` / ${ command } quiet=true ` ;
try {
const result = await executeSlashCommandsWithOptions ( commandText , { handleParserErrors : false , handleExecutionErrors : false } ) ;
if ( result . pipe ) {
profile [ command ] = result . pipe ;
continue ;
}
} catch ( error ) {
console . warn ( ` Failed to execute command: ${ commandText } ` , error ) ;
}
}
if ( cleanUp ) {
for ( const command of opposingCommands ) {
if ( commands . includes ( command ) ) {
continue ;
}
delete profile [ command ] ;
}
}
}
/ * *
* Creates a new connection profile .
2024-09-07 13:56:30 +02:00
* @ param { string } [ forceName ] Name of the connection profile
2024-09-06 23:23:42 +02:00
* @ returns { Promise < ConnectionProfile > } Created connection profile
* /
2024-09-07 13:56:30 +02:00
async function createConnectionProfile ( forceName = null ) {
2024-09-06 23:23:42 +02:00
const mode = main _api === 'openai' ? 'cc' : 'tc' ;
2024-09-07 00:12:53 +02:00
const id = uuidv4 ( ) ;
2024-09-06 23:23:42 +02:00
const profile = {
id ,
mode ,
} ;
await readProfileFromCommands ( mode , profile ) ;
const profileForDisplay = makeFancyProfile ( profile ) ;
const template = await renderExtensionTemplateAsync ( MODULE _NAME , 'profile' , { profile : profileForDisplay } ) ;
2024-09-07 20:21:46 +02:00
const isNameTaken = ( n ) => extension _settings . connectionManager . profiles . some ( p => p . name === n ) ;
const suggestedName = getUniqueName ( collapseSpaces ( ` ${ profile . api ? ? '' } ${ profile . model ? ? '' } - ${ profile . preset ? ? '' } ` ) , isNameTaken ) ;
2024-09-07 13:56:30 +02:00
const name = forceName ? ? await callGenericPopup ( template , POPUP _TYPE . INPUT , suggestedName , { rows : 2 } ) ;
2024-09-06 23:23:42 +02:00
if ( ! name ) {
2024-09-06 23:27:52 +02:00
return null ;
2024-09-06 23:23:42 +02:00
}
2024-09-07 20:33:31 +02:00
if ( isNameTaken ( name ) || name === NONE ) {
2024-09-07 20:21:46 +02:00
toastr . error ( 'A profile with the same name already exists.' ) ;
return null ;
}
2024-09-06 23:23:42 +02:00
profile . name = name ;
return profile ;
}
/ * *
* Deletes the selected connection profile .
* @ returns { Promise < void > }
* /
async function deleteConnectionProfile ( ) {
const selectedProfile = extension _settings . connectionManager . selectedProfile ;
if ( ! selectedProfile ) {
return ;
}
const index = extension _settings . connectionManager . profiles . findIndex ( p => p . id === selectedProfile ) ;
if ( index === - 1 ) {
return ;
}
2024-09-07 00:18:35 +02:00
const name = extension _settings . connectionManager . profiles [ index ] . name ;
const confirm = await Popup . show . confirm ( 'Are you sure you want to delete the selected profile?' , name ) ;
2024-09-06 23:23:42 +02:00
if ( ! confirm ) {
return ;
}
extension _settings . connectionManager . profiles . splice ( index , 1 ) ;
extension _settings . connectionManager . selectedProfile = null ;
saveSettingsDebounced ( ) ;
}
/ * *
* Formats the connection profile for display .
* @ param { ConnectionProfile } profile Connection profile
* @ returns { Object } Fancy profile
* /
function makeFancyProfile ( profile ) {
return Object . entries ( FANCY _NAMES ) . reduce ( ( acc , [ key , value ] ) => {
if ( ! profile [ key ] ) return acc ;
acc [ value ] = profile [ key ] ;
return acc ;
} , { } ) ;
}
/ * *
* Applies the connection profile .
* @ param { ConnectionProfile } profile Connection profile
* @ returns { Promise < void > }
* /
async function applyConnectionProfile ( profile ) {
if ( ! profile ) {
return ;
}
2024-09-07 22:53:22 +02:00
// Abort any ongoing profile application
ConnectionManagerSpinner . abort ( ) ;
2024-09-06 23:23:42 +02:00
const mode = profile . mode ;
const commands = mode === 'cc' ? CC _COMMANDS : TC _COMMANDS ;
2024-09-07 22:53:22 +02:00
const spinner = new ConnectionManagerSpinner ( ) ;
spinner . start ( ) ;
2024-09-06 23:23:42 +02:00
for ( const command of commands ) {
2024-09-07 22:53:22 +02:00
if ( spinner . isAborted ( ) ) {
throw new Error ( 'Profile application aborted' ) ;
}
2024-09-06 23:23:42 +02:00
const argument = profile [ command ] ;
if ( ! argument ) {
continue ;
}
const commandText = ` / ${ command } quiet=true ${ escapeArgument ( argument ) } ` ;
try {
await executeSlashCommandsWithOptions ( commandText , { handleParserErrors : false , handleExecutionErrors : false } ) ;
} catch ( error ) {
2024-09-07 22:53:22 +02:00
console . error ( ` Failed to execute command: ${ commandText } ` , error ) ;
2024-09-06 23:23:42 +02:00
}
}
2024-09-07 22:53:22 +02:00
spinner . stop ( ) ;
2024-09-06 23:23:42 +02:00
}
/ * *
* Updates the selected connection profile .
2024-09-06 23:30:47 +02:00
* @ param { ConnectionProfile } profile Connection profile
2024-09-06 23:23:42 +02:00
* @ returns { Promise < void > }
* /
2024-09-06 23:30:47 +02:00
async function updateConnectionProfile ( profile ) {
2024-09-06 23:23:42 +02:00
profile . mode = main _api === 'openai' ? 'cc' : 'tc' ;
await readProfileFromCommands ( profile . mode , profile , true ) ;
}
/ * *
* Renders the connection profile details .
* @ param { HTMLSelectElement } profiles Select element containing connection profiles
* /
function renderConnectionProfiles ( profiles ) {
profiles . innerHTML = '' ;
const noneOption = document . createElement ( 'option' ) ;
noneOption . value = '' ;
2024-09-07 12:58:46 +02:00
noneOption . textContent = NONE ;
2024-09-06 23:23:42 +02:00
noneOption . selected = ! extension _settings . connectionManager . selectedProfile ;
profiles . appendChild ( noneOption ) ;
for ( const profile of extension _settings . connectionManager . profiles ) {
const option = document . createElement ( 'option' ) ;
option . value = profile . id ;
option . textContent = profile . name ;
option . selected = profile . id === extension _settings . connectionManager . selectedProfile ;
profiles . appendChild ( option ) ;
}
}
/ * *
* Renders the content of the details element .
* @ param { HTMLDetailsElement } details Details element
* @ param { HTMLElement } detailsContent Content element of the details
* /
async function renderDetailsContent ( details , detailsContent ) {
detailsContent . innerHTML = '' ;
if ( details . open ) {
const selectedProfile = extension _settings . connectionManager . selectedProfile ;
const profile = extension _settings . connectionManager . profiles . find ( p => p . id === selectedProfile ) ;
if ( profile ) {
const profileForDisplay = makeFancyProfile ( profile ) ;
const template = await renderExtensionTemplateAsync ( MODULE _NAME , 'view' , { profile : profileForDisplay } ) ;
detailsContent . innerHTML = template ;
} else {
detailsContent . textContent = 'No profile selected' ;
}
}
}
( async function ( ) {
extension _settings . connectionManager = extension _settings . connectionManager || structuredClone ( DEFAULT _SETTINGS ) ;
for ( const key of Object . keys ( DEFAULT _SETTINGS ) ) {
if ( extension _settings . connectionManager [ key ] === undefined ) {
extension _settings . connectionManager [ key ] = DEFAULT _SETTINGS [ key ] ;
}
}
const container = document . getElementById ( 'rm_api_block' ) ;
const settings = await renderExtensionTemplateAsync ( MODULE _NAME , 'settings' ) ;
container . insertAdjacentHTML ( 'afterbegin' , settings ) ;
/** @type {HTMLSelectElement} */
// @ts-ignore
const profiles = document . getElementById ( 'connection_profiles' ) ;
renderConnectionProfiles ( profiles ) ;
profiles . addEventListener ( 'change' , async function ( ) {
const selectedProfile = profiles . selectedOptions [ 0 ] ;
if ( ! selectedProfile ) {
2024-09-07 20:33:31 +02:00
// Safety net for preventing the command getting stuck
await eventSource . emit ( event _types . CONNECTION _PROFILE _LOADED , NONE ) ;
2024-09-06 23:23:42 +02:00
return ;
}
const profileId = selectedProfile . value ;
extension _settings . connectionManager . selectedProfile = profileId ;
saveSettingsDebounced ( ) ;
await renderDetailsContent ( details , detailsContent ) ;
2024-09-07 20:08:37 +02:00
// None option selected
if ( ! profileId ) {
2024-09-07 20:33:31 +02:00
await eventSource . emit ( event _types . CONNECTION _PROFILE _LOADED , NONE ) ;
2024-09-07 20:08:37 +02:00
return ;
}
2024-09-06 23:23:42 +02:00
const profile = extension _settings . connectionManager . profiles . find ( p => p . id === profileId ) ;
if ( ! profile ) {
console . log ( ` Profile not found: ${ profileId } ` ) ;
return ;
}
await applyConnectionProfile ( profile ) ;
2024-09-07 20:33:31 +02:00
await eventSource . emit ( event _types . CONNECTION _PROFILE _LOADED , profile . name ) ;
2024-09-06 23:23:42 +02:00
} ) ;
const reloadButton = document . getElementById ( 'reload_connection_profile' ) ;
reloadButton . addEventListener ( 'click' , async ( ) => {
const selectedProfile = extension _settings . connectionManager . selectedProfile ;
const profile = extension _settings . connectionManager . profiles . find ( p => p . id === selectedProfile ) ;
if ( ! profile ) {
console . log ( 'No profile selected' ) ;
return ;
}
await applyConnectionProfile ( profile ) ;
await renderDetailsContent ( details , detailsContent ) ;
2024-09-07 20:33:31 +02:00
await eventSource . emit ( event _types . CONNECTION _PROFILE _LOADED , profile . name ) ;
2024-09-06 23:23:42 +02:00
toastr . success ( 'Connection profile reloaded' , '' , { timeOut : 1500 } ) ;
} ) ;
const createButton = document . getElementById ( 'create_connection_profile' ) ;
createButton . addEventListener ( 'click' , async ( ) => {
const profile = await createConnectionProfile ( ) ;
2024-09-06 23:27:52 +02:00
if ( ! profile ) {
return ;
}
2024-09-06 23:23:42 +02:00
extension _settings . connectionManager . profiles . push ( profile ) ;
extension _settings . connectionManager . selectedProfile = profile . id ;
saveSettingsDebounced ( ) ;
renderConnectionProfiles ( profiles ) ;
await renderDetailsContent ( details , detailsContent ) ;
2024-09-07 20:33:31 +02:00
await eventSource . emit ( event _types . CONNECTION _PROFILE _LOADED , profile . name ) ;
2024-09-06 23:23:42 +02:00
} ) ;
const updateButton = document . getElementById ( 'update_connection_profile' ) ;
updateButton . addEventListener ( 'click' , async ( ) => {
2024-09-06 23:30:47 +02:00
const selectedProfile = extension _settings . connectionManager . selectedProfile ;
const profile = extension _settings . connectionManager . profiles . find ( p => p . id === selectedProfile ) ;
if ( ! profile ) {
console . log ( 'No profile selected' ) ;
return ;
}
await updateConnectionProfile ( profile ) ;
2024-09-06 23:23:42 +02:00
await renderDetailsContent ( details , detailsContent ) ;
saveSettingsDebounced ( ) ;
2024-09-07 20:33:31 +02:00
await eventSource . emit ( event _types . CONNECTION _PROFILE _LOADED , profile . name ) ;
2024-09-06 23:23:42 +02:00
toastr . success ( 'Connection profile updated' , '' , { timeOut : 1500 } ) ;
} ) ;
const deleteButton = document . getElementById ( 'delete_connection_profile' ) ;
deleteButton . addEventListener ( 'click' , async ( ) => {
await deleteConnectionProfile ( ) ;
renderConnectionProfiles ( profiles ) ;
await renderDetailsContent ( details , detailsContent ) ;
2024-09-07 20:33:31 +02:00
await eventSource . emit ( event _types . CONNECTION _PROFILE _LOADED , NONE ) ;
2024-09-06 23:23:42 +02:00
} ) ;
/** @type {HTMLDetailsElement} */
// @ts-ignore
const details = document . getElementById ( 'connection_profile_details' ) ;
const detailsContent = document . getElementById ( 'connection_profile_details_content' ) ;
details . addEventListener ( 'toggle' , ( ) => renderDetailsContent ( details , detailsContent ) ) ;
2024-09-07 12:58:46 +02:00
SlashCommandParser . addCommandObject ( SlashCommand . fromProps ( {
name : 'profile' ,
helpString : 'Switch to a connection profile or return the name of the current profile in no argument is provided. Use <code><None></code> to switch to no profile.' ,
2024-09-07 13:56:30 +02:00
returns : 'name of the profile' ,
2024-09-07 12:58:46 +02:00
unnamedArgumentList : [
SlashCommandArgument . fromProps ( {
description : 'Name of the connection profile' ,
enumProvider : profilesProvider ,
isRequired : false ,
} ) ,
] ,
2024-09-07 20:33:31 +02:00
namedArgumentList : [
SlashCommandNamedArgument . fromProps ( {
name : 'await' ,
description : 'Wait for the connection profile to be applied before returning.' ,
isRequired : false ,
typeList : [ ARGUMENT _TYPE . BOOLEAN ] ,
defaultValue : 'true' ,
enumList : commonEnumProviders . boolean ( 'trueFalse' ) ( ) ,
} ) ,
] ,
callback : async ( args , value ) => {
2024-09-07 12:58:46 +02:00
if ( ! value || typeof value !== 'string' ) {
const selectedProfile = extension _settings . connectionManager . selectedProfile ;
const profile = extension _settings . connectionManager . profiles . find ( p => p . id === selectedProfile ) ;
if ( ! profile ) {
return NONE ;
}
return profile . name ;
}
if ( value === NONE ) {
profiles . selectedIndex = 0 ;
profiles . dispatchEvent ( new Event ( 'change' ) ) ;
return NONE ;
}
2024-09-07 16:13:32 +02:00
const profile = findProfileByName ( value ) ;
2024-09-07 12:58:46 +02:00
2024-09-07 16:13:32 +02:00
if ( ! profile ) {
2024-09-07 12:58:46 +02:00
return '' ;
}
2024-09-07 20:33:31 +02:00
const shouldAwait = ! isFalseBoolean ( String ( args ? . await ) ) ;
const awaitPromise = new Promise ( ( resolve ) => eventSource . once ( event _types . CONNECTION _PROFILE _LOADED , resolve ) ) ;
2024-09-07 16:13:32 +02:00
profiles . selectedIndex = Array . from ( profiles . options ) . findIndex ( o => o . value === profile . id ) ;
2024-09-07 12:58:46 +02:00
profiles . dispatchEvent ( new Event ( 'change' ) ) ;
2024-09-07 16:13:32 +02:00
2024-09-07 20:33:31 +02:00
if ( shouldAwait ) {
await awaitPromise ;
}
2024-09-07 19:32:58 +02:00
return profile . name ;
2024-09-07 12:58:46 +02:00
} ,
} ) ) ;
2024-09-07 13:56:30 +02:00
SlashCommandParser . addCommandObject ( SlashCommand . fromProps ( {
name : 'profile-list' ,
helpString : 'List all connection profile names.' ,
returns : 'list of profile names' ,
callback : ( ) => JSON . stringify ( extension _settings . connectionManager . profiles . map ( p => p . name ) ) ,
} ) ) ;
SlashCommandParser . addCommandObject ( SlashCommand . fromProps ( {
name : 'profile-create' ,
returns : 'name of the new profile' ,
helpString : 'Create a new connection profile using the current settings.' ,
unnamedArgumentList : [
SlashCommandArgument . fromProps ( {
description : 'name of the new connection profile' ,
isRequired : true ,
typeList : [ ARGUMENT _TYPE . STRING ] ,
} ) ,
] ,
callback : async ( _args , name ) => {
if ( ! name || typeof name !== 'string' ) {
toastr . warning ( 'Please provide a name for the new connection profile.' ) ;
return '' ;
}
const profile = await createConnectionProfile ( name ) ;
if ( ! profile ) {
return '' ;
}
extension _settings . connectionManager . profiles . push ( profile ) ;
extension _settings . connectionManager . selectedProfile = profile . id ;
saveSettingsDebounced ( ) ;
renderConnectionProfiles ( profiles ) ;
await renderDetailsContent ( details , detailsContent ) ;
return profile . name ;
} ,
} ) ) ;
SlashCommandParser . addCommandObject ( SlashCommand . fromProps ( {
name : 'profile-update' ,
helpString : 'Update the selected connection profile.' ,
callback : async ( ) => {
const selectedProfile = extension _settings . connectionManager . selectedProfile ;
const profile = extension _settings . connectionManager . profiles . find ( p => p . id === selectedProfile ) ;
if ( ! profile ) {
2024-09-07 20:08:37 +02:00
toastr . warning ( 'No profile selected.' ) ;
2024-09-07 13:56:30 +02:00
return '' ;
}
await updateConnectionProfile ( profile ) ;
await renderDetailsContent ( details , detailsContent ) ;
saveSettingsDebounced ( ) ;
2024-09-07 20:08:37 +02:00
return profile . name ;
2024-09-07 13:56:30 +02:00
} ,
} ) ) ;
2024-09-07 16:13:32 +02:00
SlashCommandParser . addCommandObject ( SlashCommand . fromProps ( {
name : 'profile-get' ,
helpString : 'Get the details of the connection profile. Returns the selected profile if no argument is provided.' ,
returns : 'object of the selected profile' ,
unnamedArgumentList : [
SlashCommandArgument . fromProps ( {
description : 'Name of the connection profile' ,
enumProvider : profilesProvider ,
isRequired : false ,
} ) ,
] ,
callback : async ( _args , value ) => {
if ( ! value || typeof value !== 'string' ) {
const selectedProfile = extension _settings . connectionManager . selectedProfile ;
const profile = extension _settings . connectionManager . profiles . find ( p => p . id === selectedProfile ) ;
if ( ! profile ) {
return '' ;
}
return JSON . stringify ( profile ) ;
}
const profile = findProfileByName ( value ) ;
if ( ! profile ) {
return '' ;
}
return JSON . stringify ( profile ) ;
} ,
} ) ) ;
2024-09-06 23:23:42 +02:00
} ) ( ) ;