Add 'start-reply-with' to Connection Profiles (#3632) * Connection Profiles: Add support for 'start-reply-with' command and allow empty values for 'stop-strings' command * Add handling for empty profile values in makeFancyProfile function * Fix application of empty values * Handle undefined values * Improve argument validation * Replace || with && * I got it right this time, swear * Who wrote this?
Signed| @@ -16,12 +16,19 @@ import { t } from '../../i18n.js'; | |||
| 16 | 16 | ||
| 17 | const MODULE_NAME = 'connection-manager'; | 17 | const MODULE_NAME = 'connection-manager'; |
| 18 | const NONE = '<None>'; | 18 | const NONE = '<None>'; |
| 19 | const EMPTY = '<Empty>'; | ||
| 19 | 20 | ||
| 20 | const DEFAULT_SETTINGS = { | 21 | const DEFAULT_SETTINGS = { |
| 21 | profiles: [], | 22 | profiles: [], |
| 22 | selectedProfile: null, | 23 | selectedProfile: null, |
| 23 | }; | 24 | }; |
| 24 | 25 | ||
| 26 | // Commands that can record an empty value into the profile | ||
| 27 | const ALLOW_EMPTY = [ | ||
| 28 | 'stop-strings', | ||
| 29 | 'start-reply-with', | ||
| 30 | ]; | ||
| 31 | |||
| 25 | const CC_COMMANDS = [ | 32 | const CC_COMMANDS = [ |
| 26 | 'api', | 33 | 'api', |
| 27 | 'preset', | 34 | 'preset', |
| @@ -31,6 +38,7 @@ const CC_COMMANDS = [ | |||
| 31 | 'model', | 38 | 'model', |
| 32 | 'proxy', | 39 | 'proxy', |
| 33 | 'stop-strings', | 40 | 'stop-strings', |
| 41 | 'start-reply-with', | ||
| 34 | ]; | 42 | ]; |
| 35 | 43 | ||
| 36 | const TC_COMMANDS = [ | 44 | const TC_COMMANDS = [ |
| @@ -45,6 +53,7 @@ const TC_COMMANDS = [ | |||
| 45 | 'instruct-state', | 53 | 'instruct-state', |
| 46 | 'tokenizer', | 54 | 'tokenizer', |
| 47 | 'stop-strings', | 55 | 'stop-strings', |
| 56 | 'start-reply-with', | ||
| 48 | ]; | 57 | ]; |
| 49 | 58 | ||
| 50 | const FANCY_NAMES = { | 59 | const FANCY_NAMES = { |
| @@ -60,6 +69,7 @@ const FANCY_NAMES = { | |||
| 60 | 'context': 'Context Template', | 69 | 'context': 'Context Template', |
| 61 | 'tokenizer': 'Tokenizer', | 70 | 'tokenizer': 'Tokenizer', |
| 62 | 'stop-strings': 'Custom Stopping Strings', | 71 | 'stop-strings': 'Custom Stopping Strings', |
| 72 | 'start-reply-with': 'Start Reply With', | ||
| 63 | }; | 73 | }; |
| 64 | 74 | ||
| 65 | /** | 75 | /** |
| @@ -107,6 +117,7 @@ class ConnectionManagerSpinner { | |||
| 107 | /** | 117 | /** |
| 108 | * Get named arguments for the command callback. | 118 | * Get named arguments for the command callback. |
| 109 | * @param {object} [args] Additional named arguments | 119 | * @param {object} [args] Additional named arguments |
| 120 | * @param {string} [args.force] Whether to force setting the value | ||
| 110 | * @returns {object} Named arguments | 121 | * @returns {object} Named arguments |
| 111 | */ | 122 | */ |
| 112 | function getNamedArguments(args = {}) { | 123 | function getNamedArguments(args = {}) { |
| @@ -142,6 +153,7 @@ const profilesProvider = () => [ | |||
| 142 | * @property {string} [instruct-state] Instruct Mode | 153 | * @property {string} [instruct-state] Instruct Mode |
| 143 | * @property {string} [tokenizer] Tokenizer | 154 | * @property {string} [tokenizer] Tokenizer |
| 144 | * @property {string} [stop-strings] Custom Stopping Strings | 155 | * @property {string} [stop-strings] Custom Stopping Strings |
| 156 | * @property {string} [start-reply-with] Start Reply With | ||
| 145 | * @property {string[]} [exclude] Commands to exclude | 157 | * @property {string[]} [exclude] Commands to exclude |
| 146 | */ | 158 | */ |
| 147 | 159 | ||
| @@ -186,9 +198,10 @@ async function readProfileFromCommands(mode, profile, cleanUp = false) { | |||
| 186 | continue; | 198 | continue; |
| 187 | } | 199 | } |
| 188 | 200 | ||
| 201 | const allowEmpty = ALLOW_EMPTY.includes(command); | ||
| 189 | const args = getNamedArguments(); | 202 | const args = getNamedArguments(); |
| 190 | const result = await SlashCommandParser.commands[command].callback(args, ''); | 203 | const result = await SlashCommandParser.commands[command].callback(args, ''); |
| 191 | if (result) { | 204 | if (result || (allowEmpty && result === '')) { |
| 192 | profile[command] = result; | 205 | profile[command] = result; |
| 193 | continue; | 206 | continue; |
| 194 | } | 207 | } |
| @@ -309,7 +322,14 @@ async function deleteConnectionProfile() { | |||
| 309 | */ | 322 | */ |
| 310 | function makeFancyProfile(profile) { | 323 | function makeFancyProfile(profile) { |
| 311 | return Object.entries(FANCY_NAMES).reduce((acc, [key, value]) => { | 324 | return Object.entries(FANCY_NAMES).reduce((acc, [key, value]) => { |
| 312 | if (!profile[key]) return acc; | 325 | const allowEmpty = ALLOW_EMPTY.includes(key); |
| 326 | if (!profile[key]) { | ||
| 327 | if (profile[key] === '' && allowEmpty) { | ||
| 328 | acc[value] = EMPTY; | ||
| 329 | } | ||
| 330 | return acc; | ||
| 331 | } | ||
| 332 | |||
| 313 | acc[value] = profile[key]; | 333 | acc[value] = profile[key]; |
| 314 | return acc; | 334 | return acc; |
| 315 | }, {}); | 335 | }, {}); |
| @@ -339,11 +359,12 @@ async function applyConnectionProfile(profile) { | |||
| 339 | } | 359 | } |
| 340 | 360 | ||
| 341 | const argument = profile[command]; | 361 | const argument = profile[command]; |
| 342 | if (!argument) { | 362 | const allowEmpty = ALLOW_EMPTY.includes(command); |
| 363 | if (!argument && !(allowEmpty && argument === '')) { | ||
| 343 | continue; | 364 | continue; |
| 344 | } | 365 | } |
| 345 | try { | 366 | try { |
| 346 | const args = getNamedArguments(); | 367 | const args = getNamedArguments(allowEmpty ? { force: 'true' } : {}); |
| 347 | await SlashCommandParser.commands[command].callback(args, argument); | 368 | await SlashCommandParser.commands[command].callback(args, argument); |
| 348 | } catch (error) { | 369 | } catch (error) { |
| 349 | console.error(`Failed to execute command: ${command} ${argument}`, error); | 370 | console.error(`Failed to execute command: ${command} ${argument}`, error); |
| @@ -4134,16 +4134,27 @@ $(document).ready(() => { | |||
| 4134 | helpString: ` | 4134 | helpString: ` |
| 4135 | <div> | 4135 | <div> |
| 4136 | Sets a list of custom stopping strings. Gets the list if no value is provided. | 4136 | Sets a list of custom stopping strings. Gets the list if no value is provided. |
| 4137 | Use a "force" argument to force set an empty value. | ||
| 4137 | </div> | 4138 | </div> |
| 4138 | <div> | 4139 | <div> |
| 4139 | <strong>Examples:</strong> | 4140 | <strong>Examples:</strong> |
| 4140 | </div> | 4141 | </div> |
| 4141 | <ul> | 4142 | <ul> |
| 4143 | <li>Force set an empty value: <pre><code class="language-stscript">/stop-strings force="true" {{noop}}</code></pre></li> | ||
| 4142 | <li>Value must be a JSON-serialized array: <pre><code class="language-stscript">/stop-strings ["goodbye", "farewell"]</code></pre></li> | 4144 | <li>Value must be a JSON-serialized array: <pre><code class="language-stscript">/stop-strings ["goodbye", "farewell"]</code></pre></li> |
| 4143 | <li>Pipe characters must be escaped with a backslash: <pre><code class="language-stscript">/stop-strings ["left\\|right"]</code></pre></li> | 4145 | <li>Pipe characters must be escaped with a backslash: <pre><code class="language-stscript">/stop-strings ["left\\|right"]</code></pre></li> |
| 4144 | </ul> | 4146 | </ul> |
| 4145 | `, | 4147 | `, |
| 4146 | returns: ARGUMENT_TYPE.LIST, | 4148 | returns: ARGUMENT_TYPE.LIST, |
| 4149 | namedArgumentList: [ | ||
| 4150 | SlashCommandNamedArgument.fromProps({ | ||
| 4151 | name: 'force', | ||
| 4152 | description: 'force set a value if empty', | ||
| 4153 | typeList: [ARGUMENT_TYPE.BOOLEAN], | ||
| 4154 | defaultValue: 'false', | ||
| 4155 | enumList: commonEnumProviders.boolean('trueFalse')(), | ||
| 4156 | }), | ||
| 4157 | ], | ||
| 4147 | unnamedArgumentList: [ | 4158 | unnamedArgumentList: [ |
| 4148 | SlashCommandArgument.fromProps({ | 4159 | SlashCommandArgument.fromProps({ |
| 4149 | description: 'list of strings', | 4160 | description: 'list of strings', |
| @@ -4152,8 +4163,20 @@ $(document).ready(() => { | |||
| 4152 | isRequired: false, | 4163 | isRequired: false, |
| 4153 | }), | 4164 | }), |
| 4154 | ], | 4165 | ], |
| 4155 | callback: (_, value) => { | 4166 | callback: (args, value) => { |
| 4156 | if (String(value ?? '').trim()) { | 4167 | const force = isTrueBoolean(String(args?.force ?? false)); |
| 4168 | value = String(value ?? '').trim(); | ||
| 4169 | |||
| 4170 | // Skip processing if no value and not forced | ||
| 4171 | if (!force && !value) { | ||
| 4172 | return power_user.custom_stopping_strings; | ||
| 4173 | } | ||
| 4174 | |||
| 4175 | // Use empty array for forced empty value | ||
| 4176 | if (force && !value) { | ||
| 4177 | value = JSON.stringify([]); | ||
| 4178 | } | ||
| 4179 | |||
| 4157 | const parsedValue = ((x) => { try { return JSON.parse(x.toString()); } catch { return null; } })(value); | 4180 | const parsedValue = ((x) => { try { return JSON.parse(x.toString()); } catch { return null; } })(value); |
| 4158 | if (!parsedValue || !Array.isArray(parsedValue)) { | 4181 | if (!parsedValue || !Array.isArray(parsedValue)) { |
| 4159 | throw new Error('Invalid list format. The value must be a JSON-serialized array of strings.'); | 4182 | throw new Error('Invalid list format. The value must be a JSON-serialized array of strings.'); |
| @@ -4164,9 +4187,56 @@ $(document).ready(() => { | |||
| 4164 | power_user.custom_stopping_strings = JSON.stringify(parsedValue); | 4187 | power_user.custom_stopping_strings = JSON.stringify(parsedValue); |
| 4165 | $('#custom_stopping_strings').val(power_user.custom_stopping_strings); | 4188 | $('#custom_stopping_strings').val(power_user.custom_stopping_strings); |
| 4166 | saveSettingsDebounced(); | 4189 | saveSettingsDebounced(); |
| 4167 | } | ||
| 4168 | 4190 | ||
| 4169 | return power_user.custom_stopping_strings; | 4191 | return power_user.custom_stopping_strings; |
| 4170 | }, | 4192 | }, |
| 4171 | })); | 4193 | })); |
| 4194 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | ||
| 4195 | name: 'start-reply-with', | ||
| 4196 | helpString: ` | ||
| 4197 | <div> | ||
| 4198 | Sets a "Start Reply With". Gets the current value if no value is provided. | ||
| 4199 | Use a "force" argument to force set an empty value. | ||
| 4200 | </div> | ||
| 4201 | <div> | ||
| 4202 | <strong>Examples:</strong> | ||
| 4203 | </div> | ||
| 4204 | <ul> | ||
| 4205 | <li>Set the field value: <pre><code class="language-stscript">/start-reply-with Sure!</code></pre></li> | ||
| 4206 | <li>Force set an empty value: <pre><code class="language-stscript">/start-reply-with force="true" {{noop}}</code></pre></li> | ||
| 4207 | </ul> | ||
| 4208 | `, | ||
| 4209 | namedArgumentList: [ | ||
| 4210 | SlashCommandNamedArgument.fromProps({ | ||
| 4211 | name: 'force', | ||
| 4212 | description: 'force set a value if empty', | ||
| 4213 | typeList: [ARGUMENT_TYPE.BOOLEAN], | ||
| 4214 | defaultValue: 'false', | ||
| 4215 | enumList: commonEnumProviders.boolean('trueFalse')(), | ||
| 4216 | }), | ||
| 4217 | ], | ||
| 4218 | unnamedArgumentList:[ | ||
| 4219 | SlashCommandArgument.fromProps({ | ||
| 4220 | description: 'value', | ||
| 4221 | typeList: [ARGUMENT_TYPE.STRING], | ||
| 4222 | acceptsMultiple: false, | ||
| 4223 | isRequired: false, | ||
| 4224 | }), | ||
| 4225 | ], | ||
| 4226 | callback: (args, value) => { | ||
| 4227 | const force = isTrueBoolean(String(args?.force ?? false)); | ||
| 4228 | value = String(value ?? '').trim(); | ||
| 4229 | |||
| 4230 | // Skip processing if no value and not forced | ||
| 4231 | if (!force && !value) { | ||
| 4232 | return power_user.user_prompt_bias; | ||
| 4233 | } | ||
| 4234 | |||
| 4235 | power_user.user_prompt_bias = value; | ||
| 4236 | $('#start_reply_with').val(power_user.user_prompt_bias); | ||
| 4237 | saveSettingsDebounced(); | ||
| 4238 | |||
| 4239 | return power_user.user_prompt_bias; | ||
| 4240 | }, | ||
| 4241 | })); | ||
| 4172 | }); | 4242 | }); |