Merge branch 'staging' into disk-cache
| @@ -1,6 +1,7 @@ | |||
| 1 | name: 🔀 Pull Request Manager | 1 | name: 🔀 Pull Request Manager |
| 2 | 2 | ||
| 3 | on: | 3 | on: |
| 4 | workflow_dispatch: # Allow to manually call this workflow | ||
| 4 | pull_request_target: | 5 | pull_request_target: |
| 5 | types: [opened, synchronize, reopened, edited, labeled, unlabeled, closed] | 6 | types: [opened, synchronize, reopened, edited, labeled, unlabeled, closed] |
| 6 | pull_request_review_comment: | 7 | pull_request_review_comment: |
| @@ -17,6 +18,12 @@ jobs: | |||
| 17 | # Only needs to run when code is changed | 18 | # Only needs to run when code is changed |
| 18 | if: github.event.action == 'opened' || github.event.action == 'synchronize' | 19 | if: github.event.action == 'opened' || github.event.action == 'synchronize' |
| 19 | 20 | ||
| 21 | # Override permissions, linter likely needs write access to issues | ||
| 22 | permissions: | ||
| 23 | contents: read | ||
| 24 | issues: write | ||
| 25 | pull-requests: write | ||
| 26 | |||
| 20 | steps: | 27 | steps: |
| 21 | - name: Checkout Repository | 28 | - name: Checkout Repository |
| 22 | # Checkout | 29 | # Checkout |
| @@ -2069,8 +2069,9 @@ export function messageFormatting(mes, ch_name, isSystem, isUser, messageId, san | |||
| 2069 | } | 2069 | } |
| 2070 | 2070 | ||
| 2071 | // Prompt bias replacement should be applied on the raw message | 2071 | // Prompt bias replacement should be applied on the raw message |
| 2072 | if (!power_user.show_user_prompt_bias && ch_name && !isUser && !isSystem) { | 2072 | const replacedPromptBias = power_user.user_prompt_bias && substituteParams(power_user.user_prompt_bias); |
| 2073 | mes = mes.replaceAll(substituteParams(power_user.user_prompt_bias), ''); | 2073 | if (!power_user.show_user_prompt_bias && ch_name && !isUser && !isSystem && replacedPromptBias && mes.startsWith(replacedPromptBias)) { |
| 2074 | mes = mes.slice(replacedPromptBias.length); | ||
| 2074 | } | 2075 | } |
| 2075 | 2076 | ||
| 2076 | if (!isSystem) { | 2077 | if (!isSystem) { |
| @@ -3,10 +3,13 @@ import { extractMessageFromData, getGenerateUrl, getRequestHeaders } from '../sc | |||
| 3 | import { getTextGenServer } from './textgen-settings.js'; | 3 | import { getTextGenServer } from './textgen-settings.js'; |
| 4 | import { extractReasoningFromData } from './reasoning.js'; | 4 | import { extractReasoningFromData } from './reasoning.js'; |
| 5 | import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types } from './instruct-mode.js'; | 5 | import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types } from './instruct-mode.js'; |
| 6 | import { getStreamingReply, tryParseStreamingError } from './openai.js'; | ||
| 7 | import EventSourceStream from './sse-stream.js'; | ||
| 6 | 8 | ||
| 7 | // #region Type Definitions | 9 | // #region Type Definitions |
| 8 | /** | 10 | /** |
| 9 | * @typedef {Object} TextCompletionRequestBase | 11 | * @typedef {Object} TextCompletionRequestBase |
| 12 | * @property {boolean?} [stream=false] - Whether to stream the response | ||
| 10 | * @property {number} max_tokens - Maximum number of tokens to generate | 13 | * @property {number} max_tokens - Maximum number of tokens to generate |
| 11 | * @property {string} [model] - Optional model name | 14 | * @property {string} [model] - Optional model name |
| 12 | * @property {string} api_type - Type of API to use | 15 | * @property {string} api_type - Type of API to use |
| @@ -17,6 +20,7 @@ import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types | |||
| 17 | 20 | ||
| 18 | /** | 21 | /** |
| 19 | * @typedef {Object} TextCompletionPayloadBase | 22 | * @typedef {Object} TextCompletionPayloadBase |
| 23 | * @property {boolean?} [stream=false] - Whether to stream the response | ||
| 20 | * @property {string} prompt - The text prompt for completion | 24 | * @property {string} prompt - The text prompt for completion |
| 21 | * @property {number} max_tokens - Maximum number of tokens to generate | 25 | * @property {number} max_tokens - Maximum number of tokens to generate |
| 22 | * @property {number} max_new_tokens - Alias for max_tokens | 26 | * @property {number} max_new_tokens - Alias for max_tokens |
| @@ -36,6 +40,7 @@ import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types | |||
| 36 | 40 | ||
| 37 | /** | 41 | /** |
| 38 | * @typedef {Object} ChatCompletionPayloadBase | 42 | * @typedef {Object} ChatCompletionPayloadBase |
| 43 | * @property {boolean?} [stream=false] - Whether to stream the response | ||
| 39 | * @property {ChatCompletionMessage[]} messages - Array of chat messages | 44 | * @property {ChatCompletionMessage[]} messages - Array of chat messages |
| 40 | * @property {string} [model] - Optional model name to use for completion | 45 | * @property {string} [model] - Optional model name to use for completion |
| 41 | * @property {string} chat_completion_source - Source provider for chat completion | 46 | * @property {string} chat_completion_source - Source provider for chat completion |
| @@ -52,10 +57,19 @@ import { formatInstructModeChat, formatInstructModePrompt, names_behavior_types | |||
| 52 | * @property {string} reasoning - Extracted reasoning. | 57 | * @property {string} reasoning - Extracted reasoning. |
| 53 | */ | 58 | */ |
| 54 | 59 | ||
| 60 | /** | ||
| 61 | * @typedef {Object} StreamResponse | ||
| 62 | * @property {string} text - Generated text. | ||
| 63 | * @property {string[]} swipes - Generated swipes | ||
| 64 | * @property {Object} state - Generated state | ||
| 65 | * @property {string?} [state.reasoning] - Generated reasoning | ||
| 66 | * @property {string?} [state.image] - Generated image | ||
| 67 | */ | ||
| 68 | |||
| 55 | // #endregion | 69 | // #endregion |
| 56 | 70 | ||
| 57 | /** | 71 | /** |
| 58 | * Creates & sends a text completion request. Streaming is not supported. | 72 | * Creates & sends a text completion request. |
| 59 | */ | 73 | */ |
| 60 | export class TextCompletionService { | 74 | export class TextCompletionService { |
| 61 | static TYPE = 'textgenerationwebui'; | 75 | static TYPE = 'textgenerationwebui'; |
| @@ -64,9 +78,10 @@ export class TextCompletionService { | |||
| 64 | * @param {Record<string, any> & TextCompletionRequestBase & {prompt: string}} custom | 78 | * @param {Record<string, any> & TextCompletionRequestBase & {prompt: string}} custom |
| 65 | * @returns {TextCompletionPayload} | 79 | * @returns {TextCompletionPayload} |
| 66 | */ | 80 | */ |
| 67 | static createRequestData({ prompt, max_tokens, model, api_type, api_server, temperature, min_p, ...props }) { | 81 | static createRequestData({ stream = false, prompt, max_tokens, model, api_type, api_server, temperature, min_p, ...props }) { |
| 68 | const payload = { | 82 | const payload = { |
| 69 | ...props, | 83 | ...props, |
| 84 | stream, | ||
| 70 | prompt, | 85 | prompt, |
| 71 | max_tokens, | 86 | max_tokens, |
| 72 | max_new_tokens: max_tokens, | 87 | max_new_tokens: max_tokens, |
| @@ -75,7 +90,6 @@ export class TextCompletionService { | |||
| 75 | api_server: api_server ?? getTextGenServer(api_type), | 90 | api_server: api_server ?? getTextGenServer(api_type), |
| 76 | temperature, | 91 | temperature, |
| 77 | min_p, | 92 | min_p, |
| 78 | stream: false, | ||
| 79 | }; | 93 | }; |
| 80 | 94 | ||
| 81 | // Remove undefined values to avoid API errors | 95 | // Remove undefined values to avoid API errors |
| @@ -92,16 +106,18 @@ export class TextCompletionService { | |||
| 92 | * Sends a text completion request to the specified server | 106 | * Sends a text completion request to the specified server |
| 93 | * @param {TextCompletionPayload} data Request data | 107 | * @param {TextCompletionPayload} data Request data |
| 94 | * @param {boolean?} extractData Extract message from the response. Default true | 108 | * @param {boolean?} extractData Extract message from the response. Default true |
| 95 | * @returns {Promise<ExtractedData | any>} Extracted data or the raw response | 109 | * @param {AbortSignal?} signal |
| 110 | * @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator | ||
| 96 | * @throws {Error} | 111 | * @throws {Error} |
| 97 | */ | 112 | */ |
| 98 | static async sendRequest(data, extractData = true) { | 113 | static async sendRequest(data, extractData = true, signal = null) { |
| 114 | if (!data.stream) { | ||
| 99 | const response = await fetch(getGenerateUrl(this.TYPE), { | 115 | const response = await fetch(getGenerateUrl(this.TYPE), { |
| 100 | method: 'POST', | 116 | method: 'POST', |
| 101 | headers: getRequestHeaders(), | 117 | headers: getRequestHeaders(), |
| 102 | cache: 'no-cache', | 118 | cache: 'no-cache', |
| 103 | body: JSON.stringify(data), | 119 | body: JSON.stringify(data), |
| 104 | signal: new AbortController().signal, | 120 | signal: signal ?? new AbortController().signal, |
| 105 | }); | 121 | }); |
| 106 | 122 | ||
| 107 | const json = await response.json(); | 123 | const json = await response.json(); |
| @@ -123,6 +139,51 @@ export class TextCompletionService { | |||
| 123 | }; | 139 | }; |
| 124 | } | 140 | } |
| 125 | 141 | ||
| 142 | const response = await fetch('/api/backends/text-completions/generate', { | ||
| 143 | method: 'POST', | ||
| 144 | headers: getRequestHeaders(), | ||
| 145 | cache: 'no-cache', | ||
| 146 | body: JSON.stringify(data), | ||
| 147 | signal: signal ?? new AbortController().signal, | ||
| 148 | }); | ||
| 149 | |||
| 150 | if (!response.ok) { | ||
| 151 | const text = await response.text(); | ||
| 152 | tryParseStreamingError(response, text, { quiet: true }); | ||
| 153 | |||
| 154 | throw new Error(`Got response status ${response.status}`); | ||
| 155 | } | ||
| 156 | |||
| 157 | const eventStream = new EventSourceStream(); | ||
| 158 | response.body.pipeThrough(eventStream); | ||
| 159 | const reader = eventStream.readable.getReader(); | ||
| 160 | return async function* streamData() { | ||
| 161 | let text = ''; | ||
| 162 | const swipes = []; | ||
| 163 | const state = { reasoning: '' }; | ||
| 164 | while (true) { | ||
| 165 | const { done, value } = await reader.read(); | ||
| 166 | if (done) return; | ||
| 167 | if (value.data === '[DONE]') return; | ||
| 168 | |||
| 169 | tryParseStreamingError(response, value.data, { quiet: true }); | ||
| 170 | |||
| 171 | let data = JSON.parse(value.data); | ||
| 172 | |||
| 173 | if (data?.choices?.[0]?.index > 0) { | ||
| 174 | const swipeIndex = data.choices[0].index - 1; | ||
| 175 | swipes[swipeIndex] = (swipes[swipeIndex] || '') + data.choices[0].text; | ||
| 176 | } else { | ||
| 177 | const newText = data?.choices?.[0]?.text || data?.content || ''; | ||
| 178 | text += newText; | ||
| 179 | state.reasoning += data?.choices?.[0]?.reasoning ?? ''; | ||
| 180 | } | ||
| 181 | |||
| 182 | yield { text, swipes, state }; | ||
| 183 | } | ||
| 184 | }; | ||
| 185 | } | ||
| 186 | |||
| 126 | /** | 187 | /** |
| 127 | * Process and send a text completion request with optional preset & instruct | 188 | * Process and send a text completion request with optional preset & instruct |
| 128 | * @param {Record<string, any> & TextCompletionRequestBase & {prompt: (ChatCompletionMessage & {ignoreInstruct?: boolean})[] |string}} custom | 189 | * @param {Record<string, any> & TextCompletionRequestBase & {prompt: (ChatCompletionMessage & {ignoreInstruct?: boolean})[] |string}} custom |
| @@ -130,13 +191,15 @@ export class TextCompletionService { | |||
| 130 | * @param {string?} [options.presetName] - Name of the preset to use for generation settings | 191 | * @param {string?} [options.presetName] - Name of the preset to use for generation settings |
| 131 | * @param {string?} [options.instructName] - Name of instruct preset for message formatting | 192 | * @param {string?} [options.instructName] - Name of instruct preset for message formatting |
| 132 | * @param {boolean} extractData - Whether to extract structured data from response | 193 | * @param {boolean} extractData - Whether to extract structured data from response |
| 133 | * @returns {Promise<ExtractedData | any>} Extracted data or the raw response | 194 | * @param {AbortSignal?} [signal] |
| 195 | * @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator | ||
| 134 | * @throws {Error} | 196 | * @throws {Error} |
| 135 | */ | 197 | */ |
| 136 | static async processRequest( | 198 | static async processRequest( |
| 137 | custom, | 199 | custom, |
| 138 | options = {}, | 200 | options = {}, |
| 139 | extractData = true, | 201 | extractData = true, |
| 202 | signal = null, | ||
| 140 | ) { | 203 | ) { |
| 141 | const { presetName, instructName } = options; | 204 | const { presetName, instructName } = options; |
| 142 | let requestData = { ...custom }; | 205 | let requestData = { ...custom }; |
| @@ -220,7 +283,7 @@ export class TextCompletionService { | |||
| 220 | // @ts-ignore | 283 | // @ts-ignore |
| 221 | const data = this.createRequestData(requestData); | 284 | const data = this.createRequestData(requestData); |
| 222 | 285 | ||
| 223 | return await this.sendRequest(data, extractData); | 286 | return await this.sendRequest(data, extractData, signal); |
| 224 | } | 287 | } |
| 225 | 288 | ||
| 226 | /** | 289 | /** |
| @@ -256,7 +319,7 @@ export class TextCompletionService { | |||
| 256 | } | 319 | } |
| 257 | 320 | ||
| 258 | /** | 321 | /** |
| 259 | * Creates & sends a chat completion request. Streaming is not supported. | 322 | * Creates & sends a chat completion request. |
| 260 | */ | 323 | */ |
| 261 | export class ChatCompletionService { | 324 | export class ChatCompletionService { |
| 262 | static TYPE = 'openai'; | 325 | static TYPE = 'openai'; |
| @@ -265,16 +328,16 @@ export class ChatCompletionService { | |||
| 265 | * @param {ChatCompletionPayload} custom | 328 | * @param {ChatCompletionPayload} custom |
| 266 | * @returns {ChatCompletionPayload} | 329 | * @returns {ChatCompletionPayload} |
| 267 | */ | 330 | */ |
| 268 | static createRequestData({ messages, model, chat_completion_source, max_tokens, temperature, custom_url, ...props }) { | 331 | static createRequestData({ stream = false, messages, model, chat_completion_source, max_tokens, temperature, custom_url, ...props }) { |
| 269 | const payload = { | 332 | const payload = { |
| 270 | ...props, | 333 | ...props, |
| 334 | stream, | ||
| 271 | messages, | 335 | messages, |
| 272 | model, | 336 | model, |
| 273 | chat_completion_source, | 337 | chat_completion_source, |
| 274 | max_tokens, | 338 | max_tokens, |
| 275 | temperature, | 339 | temperature, |
| 276 | custom_url, | 340 | custom_url, |
| 277 | stream: false, | ||
| 278 | }; | 341 | }; |
| 279 | 342 | ||
| 280 | // Remove undefined values to avoid API errors | 343 | // Remove undefined values to avoid API errors |
| @@ -291,18 +354,20 @@ export class ChatCompletionService { | |||
| 291 | * Sends a chat completion request | 354 | * Sends a chat completion request |
| 292 | * @param {ChatCompletionPayload} data Request data | 355 | * @param {ChatCompletionPayload} data Request data |
| 293 | * @param {boolean?} extractData Extract message from the response. Default true | 356 | * @param {boolean?} extractData Extract message from the response. Default true |
| 294 | * @returns {Promise<ExtractedData | any>} Extracted data or the raw response | 357 | * @param {AbortSignal?} signal Abort signal |
| 358 | * @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator | ||
| 295 | * @throws {Error} | 359 | * @throws {Error} |
| 296 | */ | 360 | */ |
| 297 | static async sendRequest(data, extractData = true) { | 361 | static async sendRequest(data, extractData = true, signal = null) { |
| 298 | const response = await fetch('/api/backends/chat-completions/generate', { | 362 | const response = await fetch('/api/backends/chat-completions/generate', { |
| 299 | method: 'POST', | 363 | method: 'POST', |
| 300 | headers: getRequestHeaders(), | 364 | headers: getRequestHeaders(), |
| 301 | cache: 'no-cache', | 365 | cache: 'no-cache', |
| 302 | body: JSON.stringify(data), | 366 | body: JSON.stringify(data), |
| 303 | signal: new AbortController().signal, | 367 | signal: signal ?? new AbortController().signal, |
| 304 | }); | 368 | }); |
| 305 | 369 | ||
| 370 | if (!data.stream) { | ||
| 306 | const json = await response.json(); | 371 | const json = await response.json(); |
| 307 | if (!response.ok || json.error) { | 372 | if (!response.ok || json.error) { |
| 308 | throw json; | 373 | throw json; |
| @@ -322,16 +387,55 @@ export class ChatCompletionService { | |||
| 322 | }; | 387 | }; |
| 323 | } | 388 | } |
| 324 | 389 | ||
| 390 | if (!response.ok) { | ||
| 391 | const text = await response.text(); | ||
| 392 | tryParseStreamingError(response, text, { quiet: true }); | ||
| 393 | |||
| 394 | throw new Error(`Got response status ${response.status}`); | ||
| 395 | } | ||
| 396 | |||
| 397 | const eventStream = new EventSourceStream(); | ||
| 398 | response.body.pipeThrough(eventStream); | ||
| 399 | const reader = eventStream.readable.getReader(); | ||
| 400 | return async function* streamData() { | ||
| 401 | let text = ''; | ||
| 402 | const swipes = []; | ||
| 403 | const state = { reasoning: '', image: '' }; | ||
| 404 | while (true) { | ||
| 405 | const { done, value } = await reader.read(); | ||
| 406 | if (done) return; | ||
| 407 | const rawData = value.data; | ||
| 408 | if (rawData === '[DONE]') return; | ||
| 409 | tryParseStreamingError(response, rawData, { quiet: true }); | ||
| 410 | const parsed = JSON.parse(rawData); | ||
| 411 | |||
| 412 | const reply = getStreamingReply(parsed, state, { | ||
| 413 | chatCompletionSource: data.chat_completion_source, | ||
| 414 | overrideShowThoughts: true, | ||
| 415 | }); | ||
| 416 | if (Array.isArray(parsed?.choices) && parsed?.choices?.[0]?.index > 0) { | ||
| 417 | const swipeIndex = parsed.choices[0].index - 1; | ||
| 418 | swipes[swipeIndex] = (swipes[swipeIndex] || '') + reply; | ||
| 419 | } else { | ||
| 420 | text += reply; | ||
| 421 | } | ||
| 422 | |||
| 423 | yield { text, swipes: swipes, state }; | ||
| 424 | } | ||
| 425 | }; | ||
| 426 | } | ||
| 427 | |||
| 325 | /** | 428 | /** |
| 326 | * Process and send a chat completion request with optional preset | 429 | * Process and send a chat completion request with optional preset |
| 327 | * @param {ChatCompletionPayload} custom | 430 | * @param {ChatCompletionPayload} custom |
| 328 | * @param {Object} options - Configuration options | 431 | * @param {Object} options - Configuration options |
| 329 | * @param {string?} [options.presetName] - Name of the preset to use for generation settings | 432 | * @param {string?} [options.presetName] - Name of the preset to use for generation settings |
| 330 | * @param {boolean} extractData - Whether to extract structured data from response | 433 | * @param {boolean} [extractData=true] - Whether to extract structured data from response |
| 331 | * @returns {Promise<ExtractedData | any>} Extracted data or the raw response | 434 | * @param {AbortSignal?} [signal] - Abort signal |
| 435 | * @returns {Promise<ExtractedData | (() => AsyncGenerator<StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator | ||
| 332 | * @throws {Error} | 436 | * @throws {Error} |
| 333 | */ | 437 | */ |
| 334 | static async processRequest(custom, options, extractData = true) { | 438 | static async processRequest(custom, options, extractData = true, signal = null) { |
| 335 | const { presetName } = options; | 439 | const { presetName } = options; |
| 336 | let requestData = { ...custom }; | 440 | let requestData = { ...custom }; |
| 337 | 441 | ||
| @@ -354,7 +458,7 @@ export class ChatCompletionService { | |||
| 354 | 458 | ||
| 355 | const data = this.createRequestData(requestData); | 459 | const data = this.createRequestData(requestData); |
| 356 | 460 | ||
| 357 | return await this.sendRequest(data, extractData); | 461 | return await this.sendRequest(data, extractData, signal); |
| 358 | } | 462 | } |
| 359 | 463 | ||
| 360 | /** | 464 | /** |
| @@ -276,10 +276,12 @@ export async function getWebLlmContextSize() { | |||
| 276 | } | 276 | } |
| 277 | 277 | ||
| 278 | /** | 278 | /** |
| 279 | * It uses the profiles to send a generate request to the API. Doesn't support streaming. | 279 | * It uses the profiles to send a generate request to the API. |
| 280 | */ | 280 | */ |
| 281 | export class ConnectionManagerRequestService { | 281 | export class ConnectionManagerRequestService { |
| 282 | static defaultSendRequestParams = { | 282 | static defaultSendRequestParams = { |
| 283 | stream: false, | ||
| 284 | signal: null, | ||
| 283 | extractData: true, | 285 | extractData: true, |
| 284 | includePreset: true, | 286 | includePreset: true, |
| 285 | includeInstruct: true, | 287 | includeInstruct: true, |
| @@ -296,11 +298,11 @@ export class ConnectionManagerRequestService { | |||
| 296 | * @param {string} profileId | 298 | * @param {string} profileId |
| 297 | * @param {string | (import('../custom-request.js').ChatCompletionMessage & {ignoreInstruct?: boolean})[]} prompt | 299 | * @param {string | (import('../custom-request.js').ChatCompletionMessage & {ignoreInstruct?: boolean})[]} prompt |
| 298 | * @param {number} maxTokens | 300 | * @param {number} maxTokens |
| 299 | * @param {{extractData?: boolean, includePreset?: boolean, includeInstruct?: boolean}} custom - default values are true | 301 | * @param {{stream?: boolean, signal?: AbortSignal, extractData?: boolean, includePreset?: boolean, includeInstruct?: boolean}} custom - default values are true |
| 300 | * @returns {Promise<import('../custom-request.js').ExtractedData | any>} Extracted data or the raw response | 302 | * @returns {Promise<import('../custom-request.js').ExtractedData | (() => AsyncGenerator<import('../custom-request.js').StreamResponse>)>} If not streaming, returns extracted data; if streaming, returns a function that creates an AsyncGenerator |
| 301 | */ | 303 | */ |
| 302 | static async sendRequest(profileId, prompt, maxTokens, custom = this.defaultSendRequestParams) { | 304 | static async sendRequest(profileId, prompt, maxTokens, custom = this.defaultSendRequestParams) { |
| 303 | const { extractData, includePreset, includeInstruct } = { ...this.defaultSendRequestParams, ...custom }; | 305 | const { stream, signal, extractData, includePreset, includeInstruct } = { ...this.defaultSendRequestParams, ...custom }; |
| 304 | 306 | ||
| 305 | const context = SillyTavern.getContext(); | 307 | const context = SillyTavern.getContext(); |
| 306 | if (context.extensionSettings.disabledExtensions.includes('connection-manager')) { | 308 | if (context.extensionSettings.disabledExtensions.includes('connection-manager')) { |
| @@ -319,6 +321,7 @@ export class ConnectionManagerRequestService { | |||
| 319 | 321 | ||
| 320 | const messages = Array.isArray(prompt) ? prompt : [{ role: 'user', content: prompt }]; | 322 | const messages = Array.isArray(prompt) ? prompt : [{ role: 'user', content: prompt }]; |
| 321 | return await context.ChatCompletionService.processRequest({ | 323 | return await context.ChatCompletionService.processRequest({ |
| 324 | stream, | ||
| 322 | messages, | 325 | messages, |
| 323 | max_tokens: maxTokens, | 326 | max_tokens: maxTokens, |
| 324 | model: profile.model, | 327 | model: profile.model, |
| @@ -326,7 +329,7 @@ export class ConnectionManagerRequestService { | |||
| 326 | custom_url: profile['api-url'], | 329 | custom_url: profile['api-url'], |
| 327 | }, { | 330 | }, { |
| 328 | presetName: includePreset ? profile.preset : undefined, | 331 | presetName: includePreset ? profile.preset : undefined, |
| 329 | }, extractData); | 332 | }, extractData, signal); |
| 330 | } | 333 | } |
| 331 | case 'textgenerationwebui': { | 334 | case 'textgenerationwebui': { |
| 332 | if (!selectedApiMap.type) { | 335 | if (!selectedApiMap.type) { |
| @@ -334,6 +337,7 @@ export class ConnectionManagerRequestService { | |||
| 334 | } | 337 | } |
| 335 | 338 | ||
| 336 | return await context.TextCompletionService.processRequest({ | 339 | return await context.TextCompletionService.processRequest({ |
| 340 | stream, | ||
| 337 | prompt, | 341 | prompt, |
| 338 | max_tokens: maxTokens, | 342 | max_tokens: maxTokens, |
| 339 | model: profile.model, | 343 | model: profile.model, |
| @@ -342,7 +346,7 @@ export class ConnectionManagerRequestService { | |||
| 342 | }, { | 346 | }, { |
| 343 | instructName: includeInstruct ? profile.instruct : undefined, | 347 | instructName: includeInstruct ? profile.instruct : undefined, |
| 344 | presetName: includePreset ? profile.preset : undefined, | 348 | presetName: includePreset ? profile.preset : undefined, |
| 345 | }, extractData); | 349 | }, extractData, signal); |
| 346 | } | 350 | } |
| 347 | default: { | 351 | default: { |
| 348 | throw new Error(`Unknown API type ${selectedApiMap.selected}`); | 352 | throw new Error(`Unknown API type ${selectedApiMap.selected}`); |
| @@ -882,7 +882,7 @@ async function generateGroupWrapper(by_auto_mode, type = null, params = {}) { | |||
| 882 | activatedMembers = activateListOrder(enabledMembers); | 882 | activatedMembers = activateListOrder(enabledMembers); |
| 883 | } | 883 | } |
| 884 | else if (activationStrategy === group_activation_strategy.POOLED) { | 884 | else if (activationStrategy === group_activation_strategy.POOLED) { |
| 885 | activatedMembers = activatePooledOrder(enabledMembers, lastMessage); | 885 | activatedMembers = activatePooledOrder(enabledMembers, lastMessage, isUserInput); |
| 886 | } | 886 | } |
| 887 | else if (activationStrategy === group_activation_strategy.MANUAL && !isUserInput) { | 887 | else if (activationStrategy === group_activation_strategy.MANUAL && !isUserInput) { |
| 888 | activatedMembers = shuffle(enabledMembers).slice(0, 1).map(x => characters.findIndex(y => y.avatar === x)).filter(x => x !== -1); | 888 | activatedMembers = shuffle(enabledMembers).slice(0, 1).map(x => characters.findIndex(y => y.avatar === x)).filter(x => x !== -1); |
| @@ -1030,16 +1030,17 @@ function activateListOrder(members) { | |||
| 1030 | * Activate group members based on the last message. | 1030 | * Activate group members based on the last message. |
| 1031 | * @param {string[]} members List of member avatars | 1031 | * @param {string[]} members List of member avatars |
| 1032 | * @param {Object} lastMessage Last message | 1032 | * @param {Object} lastMessage Last message |
| 1033 | * @param {boolean} isUserInput Whether the user has input text | ||
| 1033 | * @returns {number[]} List of character ids | 1034 | * @returns {number[]} List of character ids |
| 1034 | */ | 1035 | */ |
| 1035 | function activatePooledOrder(members, lastMessage) { | 1036 | function activatePooledOrder(members, lastMessage, isUserInput) { |
| 1036 | /** @type {string} */ | 1037 | /** @type {string} */ |
| 1037 | let activatedMember = null; | 1038 | let activatedMember = null; |
| 1038 | /** @type {string[]} */ | 1039 | /** @type {string[]} */ |
| 1039 | const spokenSinceUser = []; | 1040 | const spokenSinceUser = []; |
| 1040 | 1041 | ||
| 1041 | for (const message of chat.slice().reverse()) { | 1042 | for (const message of chat.slice().reverse()) { |
| 1042 | if (message.is_user) { | 1043 | if (message.is_user || isUserInput) { |
| 1043 | break; | 1044 | break; |
| 1044 | } | 1045 | } |
| 1045 | 1046 | ||
| @@ -1444,8 +1444,10 @@ export async function prepareOpenAIMessages({ | |||
| 1444 | * Handles errors during streaming requests. | 1444 | * Handles errors during streaming requests. |
| 1445 | * @param {Response} response | 1445 | * @param {Response} response |
| 1446 | * @param {string} decoded - response text or decoded stream data | 1446 | * @param {string} decoded - response text or decoded stream data |
| 1447 | * @param {object} [options] | ||
| 1448 | * @param {boolean?} [options.quiet=false] Suppress toast messages | ||
| 1447 | */ | 1449 | */ |
| 1448 | function tryParseStreamingError(response, decoded) { | 1450 | export function tryParseStreamingError(response, decoded, { quiet = false } = {}) { |
| 1449 | try { | 1451 | try { |
| 1450 | const data = JSON.parse(decoded); | 1452 | const data = JSON.parse(decoded); |
| 1451 | 1453 | ||
| @@ -1453,19 +1455,19 @@ function tryParseStreamingError(response, decoded) { | |||
| 1453 | return; | 1455 | return; |
| 1454 | } | 1456 | } |
| 1455 | 1457 | ||
| 1456 | checkQuotaError(data); | 1458 | checkQuotaError(data, { quiet }); |
| 1457 | checkModerationError(data); | 1459 | checkModerationError(data, { quiet }); |
| 1458 | 1460 | ||
| 1459 | // these do not throw correctly (equiv to Error("[object Object]")) | 1461 | // these do not throw correctly (equiv to Error("[object Object]")) |
| 1460 | // if trying to fix "[object Object]" displayed to users, start here | 1462 | // if trying to fix "[object Object]" displayed to users, start here |
| 1461 | 1463 | ||
| 1462 | if (data.error) { | 1464 | if (data.error) { |
| 1463 | toastr.error(data.error.message || response.statusText, 'Chat Completion API'); | 1465 | !quiet && toastr.error(data.error.message || response.statusText, 'Chat Completion API'); |
| 1464 | throw new Error(data); | 1466 | throw new Error(data); |
| 1465 | } | 1467 | } |
| 1466 | 1468 | ||
| 1467 | if (data.message) { | 1469 | if (data.message) { |
| 1468 | toastr.error(data.message, 'Chat Completion API'); | 1470 | !quiet && toastr.error(data.message, 'Chat Completion API'); |
| 1469 | throw new Error(data); | 1471 | throw new Error(data); |
| 1470 | } | 1472 | } |
| 1471 | } | 1473 | } |
| @@ -1477,16 +1479,18 @@ function tryParseStreamingError(response, decoded) { | |||
| 1477 | /** | 1479 | /** |
| 1478 | * Checks if the response contains a quota error and displays a popup if it does. | 1480 | * Checks if the response contains a quota error and displays a popup if it does. |
| 1479 | * @param data | 1481 | * @param data |
| 1482 | * @param {object} [options] | ||
| 1483 | * @param {boolean?} [options.quiet=false] Suppress toast messages | ||
| 1480 | * @returns {void} | 1484 | * @returns {void} |
| 1481 | * @throws {object} - response JSON | 1485 | * @throws {object} - response JSON |
| 1482 | */ | 1486 | */ |
| 1483 | function checkQuotaError(data) { | 1487 | function checkQuotaError(data, { quiet = false } = {}) { |
| 1484 | if (!data) { | 1488 | if (!data) { |
| 1485 | return; | 1489 | return; |
| 1486 | } | 1490 | } |
| 1487 | 1491 | ||
| 1488 | if (data.quota_error) { | 1492 | if (data.quota_error) { |
| 1489 | renderTemplateAsync('quotaError').then((html) => Popup.show.text('Quota Error', html)); | 1493 | !quiet && renderTemplateAsync('quotaError').then((html) => Popup.show.text('Quota Error', html)); |
| 1490 | 1494 | ||
| 1491 | // this does not throw correctly (equiv to Error("[object Object]")) | 1495 | // this does not throw correctly (equiv to Error("[object Object]")) |
| 1492 | // if trying to fix "[object Object]" displayed to users, start here | 1496 | // if trying to fix "[object Object]" displayed to users, start here |
| @@ -1494,9 +1498,14 @@ function checkQuotaError(data) { | |||
| 1494 | } | 1498 | } |
| 1495 | } | 1499 | } |
| 1496 | 1500 | ||
| 1497 | function checkModerationError(data) { | 1501 | /** |
| 1502 | * @param {any} data | ||
| 1503 | * @param {object} [options] | ||
| 1504 | * @param {boolean?} [options.quiet=false] Suppress toast messages | ||
| 1505 | */ | ||
| 1506 | function checkModerationError(data, { quiet = false } = {}) { | ||
| 1498 | const moderationError = data?.error?.message?.includes('requires moderation'); | 1507 | const moderationError = data?.error?.message?.includes('requires moderation'); |
| 1499 | if (moderationError) { | 1508 | if (moderationError && !quiet) { |
| 1500 | const moderationReason = `Reasons: ${data?.error?.metadata?.reasons?.join(', ') ?? '(N/A)'}`; | 1509 | const moderationReason = `Reasons: ${data?.error?.metadata?.reasons?.join(', ') ?? '(N/A)'}`; |
| 1501 | const flaggedText = data?.error?.metadata?.flagged_input ?? '(N/A)'; | 1510 | const flaggedText = data?.error?.metadata?.flagged_input ?? '(N/A)'; |
| 1502 | toastr.info(flaggedText, moderationReason, { timeOut: 10000 }); | 1511 | toastr.info(flaggedText, moderationReason, { timeOut: 10000 }); |
| @@ -2255,37 +2264,43 @@ async function sendOpenAIRequest(type, messages, signal) { | |||
| 2255 | * Extracts the reply from the response data from a chat completions-like source | 2264 | * Extracts the reply from the response data from a chat completions-like source |
| 2256 | * @param {object} data Response data from the chat completions-like source | 2265 | * @param {object} data Response data from the chat completions-like source |
| 2257 | * @param {object} state Additional state to keep track of | 2266 | * @param {object} state Additional state to keep track of |
| 2267 | * @param {object} [options] Additional options | ||
| 2268 | * @param {string?} [options.chatCompletionSource] Chat completion source | ||
| 2269 | * @param {boolean?} [options.overrideShowThoughts] Override show thoughts | ||
| 2258 | * @returns {string} The reply extracted from the response data | 2270 | * @returns {string} The reply extracted from the response data |
| 2259 | */ | 2271 | */ |
| 2260 | function getStreamingReply(data, state) { | 2272 | export function getStreamingReply(data, state, { chatCompletionSource = null, overrideShowThoughts = null } = {}) { |
| 2261 | if (oai_settings.chat_completion_source === chat_completion_sources.CLAUDE) { | 2273 | const chat_completion_source = chatCompletionSource ?? oai_settings.chat_completion_source; |
| 2262 | if (oai_settings.show_thoughts) { | 2274 | const show_thoughts = overrideShowThoughts ?? oai_settings.show_thoughts; |
| 2275 | |||
| 2276 | if (chat_completion_source === chat_completion_sources.CLAUDE) { | ||
| 2277 | if (show_thoughts) { | ||
| 2263 | state.reasoning += data?.delta?.thinking || ''; | 2278 | state.reasoning += data?.delta?.thinking || ''; |
| 2264 | } | 2279 | } |
| 2265 | return data?.delta?.text || ''; | 2280 | return data?.delta?.text || ''; |
| 2266 | } else if (oai_settings.chat_completion_source === chat_completion_sources.MAKERSUITE) { | 2281 | } else if (chat_completion_source === chat_completion_sources.MAKERSUITE) { |
| 2267 | const inlineData = data?.candidates?.[0]?.content?.parts?.find(x => x.inlineData)?.inlineData; | 2282 | const inlineData = data?.candidates?.[0]?.content?.parts?.find(x => x.inlineData)?.inlineData; |
| 2268 | if (inlineData) { | 2283 | if (inlineData) { |
| 2269 | state.image = `data:${inlineData.mimeType};base64,${inlineData.data}`; | 2284 | state.image = `data:${inlineData.mimeType};base64,${inlineData.data}`; |
| 2270 | } | 2285 | } |
| 2271 | if (oai_settings.show_thoughts) { | 2286 | if (show_thoughts) { |
| 2272 | state.reasoning += (data?.candidates?.[0]?.content?.parts?.filter(x => x.thought)?.map(x => x.text)?.[0] || ''); | 2287 | state.reasoning += (data?.candidates?.[0]?.content?.parts?.filter(x => x.thought)?.map(x => x.text)?.[0] || ''); |
| 2273 | } | 2288 | } |
| 2274 | return data?.candidates?.[0]?.content?.parts?.filter(x => !x.thought)?.map(x => x.text)?.[0] || ''; | 2289 | return data?.candidates?.[0]?.content?.parts?.filter(x => !x.thought)?.map(x => x.text)?.[0] || ''; |
| 2275 | } else if (oai_settings.chat_completion_source === chat_completion_sources.COHERE) { | 2290 | } else if (chat_completion_source === chat_completion_sources.COHERE) { |
| 2276 | return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || ''; | 2291 | return data?.delta?.message?.content?.text || data?.delta?.message?.tool_plan || ''; |
| 2277 | } else if (oai_settings.chat_completion_source === chat_completion_sources.DEEPSEEK) { | 2292 | } else if (chat_completion_source === chat_completion_sources.DEEPSEEK) { |
| 2278 | if (oai_settings.show_thoughts) { | 2293 | if (show_thoughts) { |
| 2279 | state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || ''); | 2294 | state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content || ''); |
| 2280 | } | 2295 | } |
| 2281 | return data.choices?.[0]?.delta?.content || ''; | 2296 | return data.choices?.[0]?.delta?.content || ''; |
| 2282 | } else if (oai_settings.chat_completion_source === chat_completion_sources.OPENROUTER) { | 2297 | } else if (chat_completion_source === chat_completion_sources.OPENROUTER) { |
| 2283 | if (oai_settings.show_thoughts) { | 2298 | if (show_thoughts) { |
| 2284 | state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || ''); | 2299 | state.reasoning += (data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning || ''); |
| 2285 | } | 2300 | } |
| 2286 | return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? ''; | 2301 | return data.choices?.[0]?.delta?.content ?? data.choices?.[0]?.message?.content ?? data.choices?.[0]?.text ?? ''; |
| 2287 | } else if (oai_settings.chat_completion_source === chat_completion_sources.CUSTOM) { | 2302 | } else if (chat_completion_source === chat_completion_sources.CUSTOM) { |
| 2288 | if (oai_settings.show_thoughts) { | 2303 | if (show_thoughts) { |
| 2289 | state.reasoning += | 2304 | state.reasoning += |
| 2290 | data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ?? | 2305 | data.choices?.filter(x => x?.delta?.reasoning_content)?.[0]?.delta?.reasoning_content ?? |
| 2291 | data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning ?? | 2306 | data.choices?.filter(x => x?.delta?.reasoning)?.[0]?.delta?.reasoning ?? |
| @@ -69,7 +69,7 @@ import { SlashCommand } from './slash-commands/SlashCommand.js'; | |||
| 69 | import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js'; | 69 | import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js'; |
| 70 | import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashCommandNamedArgumentAssignment.js'; | 70 | import { SlashCommandNamedArgumentAssignment } from './slash-commands/SlashCommandNamedArgumentAssignment.js'; |
| 71 | import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; | 71 | import { SlashCommandEnumValue, enumTypes } from './slash-commands/SlashCommandEnumValue.js'; |
| 72 | import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; | 72 | import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; |
| 73 | import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; | 73 | import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCommonEnumsProvider.js'; |
| 74 | import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js'; | 74 | import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js'; |
| 75 | import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js'; | 75 | import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js'; |
| @@ -77,6 +77,7 @@ import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHel | |||
| 77 | import { accountStorage } from './util/AccountStorage.js'; | 77 | import { accountStorage } from './util/AccountStorage.js'; |
| 78 | import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js'; | 78 | import { SlashCommandDebugController } from './slash-commands/SlashCommandDebugController.js'; |
| 79 | import { SlashCommandScope } from './slash-commands/SlashCommandScope.js'; | 79 | import { SlashCommandScope } from './slash-commands/SlashCommandScope.js'; |
| 80 | import { t } from './i18n.js'; | ||
| 80 | export { | 81 | export { |
| 81 | executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand, | 82 | executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand, |
| 82 | }; | 83 | }; |
| @@ -1556,16 +1557,28 @@ export function initDefaultSlashCommands() { | |||
| 1556 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 1557 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 1557 | name: 'buttons', | 1558 | name: 'buttons', |
| 1558 | callback: buttonsCallback, | 1559 | callback: buttonsCallback, |
| 1559 | returns: 'clicked button label', | 1560 | returns: 'clicked button label (or array of labels if multiple is enabled)', |
| 1560 | namedArgumentList: [ | 1561 | namedArgumentList: [ |
| 1561 | new SlashCommandNamedArgument( | 1562 | SlashCommandNamedArgument.fromProps({ |
| 1562 | 'labels', 'button labels', [ARGUMENT_TYPE.LIST], true, | 1563 | name: 'labels', |
| 1563 | ), | 1564 | description: 'button labels', |
| 1565 | typeList: [ARGUMENT_TYPE.LIST], | ||
| 1566 | isRequired: true, | ||
| 1567 | }), | ||
| 1568 | SlashCommandNamedArgument.fromProps({ | ||
| 1569 | name: 'multiple', | ||
| 1570 | description: 'if enabled multiple buttons can be clicked/toggled, and all clicked buttons are returned as an array', | ||
| 1571 | typeList: [ARGUMENT_TYPE.BOOLEAN], | ||
| 1572 | enumList: commonEnumProviders.boolean('trueFalse')(), | ||
| 1573 | defaultValue: 'false', | ||
| 1574 | }), | ||
| 1564 | ], | 1575 | ], |
| 1565 | unnamedArgumentList: [ | 1576 | unnamedArgumentList: [ |
| 1566 | new SlashCommandArgument( | 1577 | SlashCommandArgument.fromProps({ |
| 1567 | 'text', [ARGUMENT_TYPE.STRING], true, | 1578 | description: 'text', |
| 1568 | ), | 1579 | typeList: [ARGUMENT_TYPE.STRING], |
| 1580 | isRequired: true, | ||
| 1581 | }), | ||
| 1569 | ], | 1582 | ], |
| 1570 | helpString: ` | 1583 | helpString: ` |
| 1571 | <div> | 1584 | <div> |
| @@ -2377,6 +2390,18 @@ async function trimTokensCallback(arg, value) { | |||
| 2377 | } | 2390 | } |
| 2378 | } | 2391 | } |
| 2379 | 2392 | ||
| 2393 | /** | ||
| 2394 | * Displays a popup with buttons based on provided labels and handles button interactions. | ||
| 2395 | * | ||
| 2396 | * @param {object} args - Named arguments for the command | ||
| 2397 | * @param {string} args.labels - JSON string of an array of button labels | ||
| 2398 | * @param {string} [args.multiple=false] - Flag indicating if multiple buttons can be toggled | ||
| 2399 | * @param {string} text - The text content to be displayed within the popup | ||
| 2400 | * | ||
| 2401 | * @returns {Promise<string>} - A promise that resolves to a string of the button labels selected | ||
| 2402 | * If 'multiple' is true, returns a JSON string array of labels. | ||
| 2403 | * If 'multiple' is false, returns a single label string. | ||
| 2404 | */ | ||
| 2380 | async function buttonsCallback(args, text) { | 2405 | async function buttonsCallback(args, text) { |
| 2381 | try { | 2406 | try { |
| 2382 | /** @type {string[]} */ | 2407 | /** @type {string[]} */ |
| @@ -2387,6 +2412,10 @@ async function buttonsCallback(args, text) { | |||
| 2387 | return ''; | 2412 | return ''; |
| 2388 | } | 2413 | } |
| 2389 | 2414 | ||
| 2415 | /** @type {Set<number>} */ | ||
| 2416 | const multipleToggledState = new Set(); | ||
| 2417 | const multiple = isTrueBoolean(args?.multiple); | ||
| 2418 | |||
| 2390 | // Map custom buttons to results. Start at 2 because 1 and 0 are reserved for ok and cancel | 2419 | // Map custom buttons to results. Start at 2 because 1 and 0 are reserved for ok and cancel |
| 2391 | const resultToButtonMap = new Map(buttons.map((button, index) => [index + 2, button])); | 2420 | const resultToButtonMap = new Map(buttons.map((button, index) => [index + 2, button])); |
| 2392 | 2421 | ||
| @@ -2404,11 +2433,24 @@ async function buttonsCallback(args, text) { | |||
| 2404 | 2433 | ||
| 2405 | for (const [result, button] of resultToButtonMap) { | 2434 | for (const [result, button] of resultToButtonMap) { |
| 2406 | const buttonElement = document.createElement('div'); | 2435 | const buttonElement = document.createElement('div'); |
| 2407 | buttonElement.classList.add('menu_button', 'result-control', 'wide100p'); | 2436 | buttonElement.classList.add('menu_button', 'wide100p'); |
| 2408 | buttonElement.dataset.result = String(result); | 2437 | |
| 2438 | if (multiple) { | ||
| 2439 | buttonElement.classList.add('toggleable'); | ||
| 2440 | buttonElement.dataset.toggleValue = String(result); | ||
| 2409 | buttonElement.addEventListener('click', async () => { | 2441 | buttonElement.addEventListener('click', async () => { |
| 2410 | await popup.complete(result); | 2442 | buttonElement.classList.toggle('toggled'); |
| 2443 | if (buttonElement.classList.contains('toggled')) { | ||
| 2444 | multipleToggledState.add(result); | ||
| 2445 | } else { | ||
| 2446 | multipleToggledState.delete(result); | ||
| 2447 | } | ||
| 2411 | }); | 2448 | }); |
| 2449 | } else { | ||
| 2450 | buttonElement.classList.add('result-control'); | ||
| 2451 | buttonElement.dataset.result = String(result); | ||
| 2452 | } | ||
| 2453 | |||
| 2412 | buttonElement.innerText = button; | 2454 | buttonElement.innerText = button; |
| 2413 | buttonContainer.appendChild(buttonElement); | 2455 | buttonContainer.appendChild(buttonElement); |
| 2414 | } | 2456 | } |
| @@ -2424,10 +2466,19 @@ async function buttonsCallback(args, text) { | |||
| 2424 | popupContainer.style.flexDirection = 'column'; | 2466 | popupContainer.style.flexDirection = 'column'; |
| 2425 | popupContainer.style.maxHeight = '80vh'; // Limit the overall height of the popup | 2467 | popupContainer.style.maxHeight = '80vh'; // Limit the overall height of the popup |
| 2426 | 2468 | ||
| 2427 | popup = new Popup(popupContainer, POPUP_TYPE.TEXT, '', { okButton: 'Cancel', allowVerticalScrolling: true }); | 2469 | popup = new Popup(popupContainer, POPUP_TYPE.TEXT, '', { okButton: multiple ? t`Ok` : t`Cancel`, allowVerticalScrolling: true }); |
| 2428 | popup.show() | 2470 | popup.show() |
| 2429 | .then((result => resolve(typeof result === 'number' ? resultToButtonMap.get(result) ?? '' : ''))) | 2471 | .then((result => resolve(getResult(result)))) |
| 2430 | .catch(() => resolve('')); | 2472 | .catch(() => resolve('')); |
| 2473 | |||
| 2474 | /** @returns {string} @param {string|number|boolean} result */ | ||
| 2475 | function getResult(result) { | ||
| 2476 | if (multiple) { | ||
| 2477 | const array = result === POPUP_RESULT.AFFIRMATIVE ? Array.from(multipleToggledState).map(r => resultToButtonMap.get(r) ?? '') : []; | ||
| 2478 | return JSON.stringify(array); | ||
| 2479 | } | ||
| 2480 | return typeof result === 'number' ? resultToButtonMap.get(result) ?? '' : ''; | ||
| 2481 | } | ||
| 2431 | }); | 2482 | }); |
| 2432 | } catch { | 2483 | } catch { |
| 2433 | return ''; | 2484 | return ''; |
| @@ -2911,6 +2911,35 @@ select option:not(:checked) { | |||
| 2911 | pointer-events: none; | 2911 | pointer-events: none; |
| 2912 | } | 2912 | } |
| 2913 | 2913 | ||
| 2914 | .menu_button.toggleable { | ||
| 2915 | padding-left: 20px; | ||
| 2916 | } | ||
| 2917 | |||
| 2918 | .menu_button.toggleable.toggled { | ||
| 2919 | border-color: var(--active); | ||
| 2920 | } | ||
| 2921 | |||
| 2922 | .menu_button.toggleable:not(.toggled) { | ||
| 2923 | filter: brightness(80%); | ||
| 2924 | } | ||
| 2925 | |||
| 2926 | .menu_button.toggleable::before { | ||
| 2927 | font-family: "Font Awesome 6 Free"; | ||
| 2928 | margin-left: 10px; | ||
| 2929 | position: absolute; | ||
| 2930 | left: 0; | ||
| 2931 | } | ||
| 2932 | |||
| 2933 | .menu_button.toggleable.toggled::before { | ||
| 2934 | content: "\f00c"; | ||
| 2935 | color: var(--active); | ||
| 2936 | } | ||
| 2937 | |||
| 2938 | .menu_button.toggleable:not(.toggled)::before { | ||
| 2939 | content: "\f00d"; | ||
| 2940 | color: var(--fullred); | ||
| 2941 | } | ||
| 2942 | |||
| 2914 | .fav_on { | 2943 | .fav_on { |
| 2915 | color: var(--golden) !important; | 2944 | color: var(--golden) !important; |
| 2916 | } | 2945 | } |
| @@ -2921,7 +2950,7 @@ select option:not(:checked) { | |||
| 2921 | } | 2950 | } |
| 2922 | 2951 | ||
| 2923 | .menu_button.togglable:not(.toggleEnabled) { | 2952 | .menu_button.togglable:not(.toggleEnabled) { |
| 2924 | color: red; | 2953 | color: var(--fullred); |
| 2925 | } | 2954 | } |
| 2926 | 2955 | ||
| 2927 | .displayBlock { | 2956 | .displayBlock { |