Merge branch 'SillyTavern:staging' into staging

f1720be0179b34d3b19b5a5944332948bf0c4f4f

Yokayo <52032299+Yokayo@users.noreply.github.com>

Signed
4 files changed, +671 -1Showing whitespace changes
public/scripts/extensions/tts/chatterbox.js+649 -0
@@ -0,0 +1,649 @@
1import { saveTtsProviderSettings } from './index.js';
2
3export { ChatterboxTtsProvider };
4
5class ChatterboxTtsProvider {
6 //########//
7 // Config //
8 //########//
9
10 settings = {};
11 constructor() {
12 // Initialize with default settings
13 this.settings = {
14 provider_endpoint: this.settings.provider_endpoint || 'http://localhost:8004',
15 voice_mode: this.settings.voice_mode || 'predefined',
16 predefined_voice: this.settings.predefined_voice || 'S1',
17 reference_voice: this.settings.reference_voice || '',
18 temperature: this.settings.temperature || 0.8,
19 exaggeration: this.settings.exaggeration || 0.5,
20 cfg_weight: this.settings.cfg_weight || 0.5,
21 seed: this.settings.seed || -1,
22 speed_factor: this.settings.speed_factor || 1.0,
23 language: this.settings.language || 'en',
24 split_text: this.settings.split_text || true,
25 chunk_size: this.settings.chunk_size || 120,
26 output_format: this.settings.output_format || 'wav',
27 voiceMap: this.settings.voiceMap || {},
28 };
29 }
30
31 ready = false;
32 voices = [];
33 separator = '. ';
34 audioElement = document.createElement('audio');
35
36 languageLabels = {
37 'English': 'en',
38 'Spanish': 'es',
39 'French': 'fr',
40 'German': 'de',
41 'Italian': 'it',
42 'Portuguese': 'pt',
43 'Polish': 'pl',
44 'Turkish': 'tr',
45 'Russian': 'ru',
46 'Dutch': 'nl',
47 'Czech': 'cs',
48 'Arabic': 'ar',
49 'Chinese': 'zh-cn',
50 'Japanese': 'ja',
51 'Korean': 'ko',
52 'Hindi': 'hi',
53 };
54
55 get settingsHtml() {
56 let html = `<div class="chatterbox-settings-container">
57 <div class="chatterbox-settings-header">
58 <h3>Chatterbox TTS Settings</h3>
59 <div class="status-indicator">
60 Status: <span id="chatterbox-status" class="offline">Offline</span>
61 </div>
62 </div>`;
63
64 // Server endpoint
65 html += `<div class="chatterbox-setting-row">
66 <label for="chatterbox-endpoint">Server Endpoint:</label>
67 <input id="chatterbox-endpoint" type="text" class="text_pole" value="${this.settings.provider_endpoint}" />
68 </div>`;
69
70 // Language selection
71 html += `<div class="chatterbox-setting-row">
72 <label for="chatterbox-language">Language:</label>
73 <select id="chatterbox-language">`;
74 for (let language in this.languageLabels) {
75 html += `<option value="${this.languageLabels[language]}" ${this.languageLabels[language] === this.settings.language ? 'selected' : ''}>${language}</option>`;
76 }
77 html += `</select>
78 </div>`;
79
80 // Generation parameters
81 html += `<div class="chatterbox-params-section">
82 <h4>Generation Parameters</h4>`;
83
84 // Temperature
85 html += `<div class="chatterbox-setting-row">
86 <label for="chatterbox-temperature">Temperature: <span id="chatterbox-temperature-value">${this.settings.temperature}</span></label>
87 <input id="chatterbox-temperature" type="range" min="0" max="1" step="0.1" value="${this.settings.temperature}" />
88 </div>`;
89
90 // Exaggeration
91 html += `<div class="chatterbox-setting-row">
92 <label for="chatterbox-exaggeration">Exaggeration: <span id="chatterbox-exaggeration-value">${this.settings.exaggeration}</span></label>
93 <input id="chatterbox-exaggeration" type="range" min="0" max="2" step="0.1" value="${this.settings.exaggeration}" />
94 </div>`;
95
96 // CFG Weight
97 html += `<div class="chatterbox-setting-row">
98 <label for="chatterbox-cfg-weight">CFG Weight: <span id="chatterbox-cfg-weight-value">${this.settings.cfg_weight}</span></label>
99 <input id="chatterbox-cfg-weight" type="range" min="0" max="1" step="0.1" value="${this.settings.cfg_weight}" />
100 </div>`;
101
102 // Speed Factor
103 html += `<div class="chatterbox-setting-row">
104 <label for="chatterbox-speed">Speed Factor: <span id="chatterbox-speed-value">${this.settings.speed_factor}</span></label>
105 <input id="chatterbox-speed" type="range" min="0.5" max="2" step="0.1" value="${this.settings.speed_factor}" />
106 </div>`;
107
108 // Seed
109 html += `<div class="chatterbox-setting-row">
110 <label for="chatterbox-seed">Seed (-1 for random):</label>
111 <input id="chatterbox-seed" class="text_pole" type="number" min="-1" value="${this.settings.seed}" />
112 </div>`;
113
114 // Text chunking
115 html += `<div class="chatterbox-setting-row">
116 <label class="checkbox_label">
117 <input type="checkbox" id="chatterbox-split-text" ${this.settings.split_text ? 'checked' : ''} />
118 Split long texts into chunks
119 </label>
120 </div>`;
121
122 // Chunk size
123 html += `<div class="chatterbox-setting-row" id="chunk-size-row" ${!this.settings.split_text ? 'style="display: none;"' : ''}>
124 <label for="chatterbox-chunk-size">Chunk Size:</label>
125 <input id="chatterbox-chunk-size" class="text_pole" type="number" min="50" max="500" value="${this.settings.chunk_size}" />
126 </div>`;
127
128 // Output format
129 html += `<div class="chatterbox-setting-row">
130 <label for="chatterbox-format">Output Format:</label>
131 <select id="chatterbox-format">
132 <option value="wav" ${this.settings.output_format === 'wav' ? 'selected' : ''}>WAV</option>
133 <option value="opus" ${this.settings.output_format === 'opus' ? 'selected' : ''}>Opus</option>
134 </select>
135 </div>`;
136
137 html += '</div>'; // End params section
138
139 // Footer with links
140 html += `<div class="chatterbox-footer">
141 <a href="${this.settings.provider_endpoint}" target="_blank">Chatterbox Web UI</a> |
142 <a href="https://github.com/devnen/Chatterbox-TTS-Server" target="_blank">Documentation</a>
143 </div>`;
144
145 html += '</div>'; // End container
146
147 // Add CSS styles
148 html += `<style>
149 .chatterbox-settings-container {
150 padding: 10px;
151 }
152 .chatterbox-settings-header {
153 display: flex;
154 justify-content: space-between;
155 align-items: center;
156 margin-bottom: 15px;
157 }
158 .chatterbox-settings-header h3 {
159 margin: 0;
160 }
161 .chatterbox-settings-container .status-indicator {
162 font-weight: bold;
163 }
164 #chatterbox-status.ready { color: #4CAF50; }
165 #chatterbox-status.offline { color: #f44336; }
166 #chatterbox-status.processing { color: #2196F3; }
167 .chatterbox-setting-row {
168 margin-bottom: 10px;
169 display: flex;
170 align-items: center;
171 gap: 10px;
172 }
173 .chatterbox-setting-row label {
174 flex: 0 0 150px;
175 }
176 .chatterbox-setting-row label.checkbox_label {
177 flex-basis: auto;
178 }
179 .chatterbox-setting-row input[type="text"],
180 .chatterbox-setting-row input[type="number"],
181 .chatterbox-setting-row select {
182 flex: 1;
183 }
184 .chatterbox-setting-row input[type="range"] {
185 flex: 1;
186 }
187 .chatterbox-params-section {
188 margin-top: 15px;
189 padding-top: 15px;
190 border-top: 1px solid #ccc;
191 }
192 .chatterbox-params-section h4 {
193 margin-top: 0;
194 margin-bottom: 10px;
195 }
196 .chatterbox-footer {
197 margin-top: 15px;
198 padding-top: 15px;
199 border-top: 1px solid #ccc;
200 text-align: center;
201 font-size: 0.9em;
202 }
203 </style>`;
204
205 return html;
206 }
207
208 //######################//
209 // Startup & Initialize //
210 //######################//
211
212 async loadSettings(settings) {
213 this.updateStatus('Offline');
214
215 if (Object.keys(settings).length === 0) {
216 console.info('Using default Chatterbox TTS Provider settings');
217 } else {
218 // Populate settings with provided values
219 for (const key in settings) {
220 if (key in this.settings) {
221 this.settings[key] = settings[key];
222 }
223 }
224 }
225
226 // Update UI elements
227 this.updateUIFromSettings();
228
229 console.debug('ChatterboxTTS: Settings loaded');
230
231 try {
232 // Check if TTS provider is ready
233 await this.checkReady();
234
235 if (this.ready) {
236 // Fetch all voice types for the voice map
237 await this.fetchTtsVoiceObjects();
238 this.updateStatus('Ready');
239 }
240
241 this.setupEventListeners();
242
243 } catch (error) {
244 console.error('Error loading Chatterbox settings:', error);
245 this.updateStatus('Offline');
246 }
247 }
248
249 updateUIFromSettings() {
250 $('#chatterbox-endpoint').val(this.settings.provider_endpoint);
251 $('#chatterbox-language').val(this.settings.language);
252 $('#chatterbox-temperature').val(this.settings.temperature);
253 $('#chatterbox-temperature-value').text(this.settings.temperature);
254 $('#chatterbox-exaggeration').val(this.settings.exaggeration);
255 $('#chatterbox-exaggeration-value').text(this.settings.exaggeration);
256 $('#chatterbox-cfg-weight').val(this.settings.cfg_weight);
257 $('#chatterbox-cfg-weight-value').text(this.settings.cfg_weight);
258 $('#chatterbox-speed').val(this.settings.speed_factor);
259 $('#chatterbox-speed-value').text(this.settings.speed_factor);
260 $('#chatterbox-seed').val(this.settings.seed);
261 $('#chatterbox-split-text').prop('checked', this.settings.split_text);
262 $('#chatterbox-chunk-size').val(this.settings.chunk_size);
263 $('#chatterbox-format').val(this.settings.output_format);
264
265 // Show/hide chunk size based on split text
266 if (this.settings.split_text) {
267 $('#chunk-size-row').show();
268 } else {
269 $('#chunk-size-row').hide();
270 }
271 }
272
273 //##############################//
274 // Check Server is Available //
275 //##############################//
276
277 async checkReady() {
278 try {
279 const response = await fetch(`${this.settings.provider_endpoint}/api/ui/initial-data`);
280
281 if (!response.ok) {
282 throw new Error(`HTTP Error Response: ${response.status} ${response.statusText}`);
283 }
284
285 const data = await response.json();
286
287 // Check if we got valid data
288 if (data) {
289 this.ready = true;
290 console.log('Chatterbox TTS service is ready.');
291 } else {
292 this.ready = false;
293 console.log('Chatterbox TTS service returned invalid data.');
294 }
295 } catch (error) {
296 console.error('Error checking Chatterbox TTS service readiness:', error);
297 this.ready = false;
298 }
299 }
300
301 //######################//
302 // Get Available Voices //
303 //######################//
304
305 async fetchTtsVoiceObjects() {
306 try {
307 // Always fetch predefined voices
308 const predefinedResponse = await fetch(`${this.settings.provider_endpoint}/get_predefined_voices`);
309 if (!predefinedResponse.ok) {
310 throw new Error(`HTTP ${predefinedResponse.status}: ${predefinedResponse.statusText}`);
311 }
312
313 const predefinedData = await predefinedResponse.json();
314
315 // Transform predefined voices
316 const predefinedVoices = predefinedData.map(voice => ({
317 name: voice.display_name,
318 voice_id: voice.voice_id || voice.filename,
319 preview_url: null,
320 lang: voice.language || 'en',
321 }));
322
323 // Always try to fetch reference voices
324 let referenceVoices = [];
325 try {
326 const refResponse = await fetch(`${this.settings.provider_endpoint}/get_reference_files`);
327 if (refResponse.ok) {
328 const refData = await refResponse.json();
329 referenceVoices = refData.map(filename => ({
330 name: `[Clone] ${filename}`,
331 voice_id: `ref_${filename}`,
332 preview_url: null,
333 lang: 'en',
334 }));
335 }
336 } catch (error) {
337 console.warn('Failed to fetch reference voices:', error);
338 }
339
340 // Combine all voices
341 this.voices = [...predefinedVoices, ...referenceVoices];
342
343 console.log(`Loaded ${this.voices.length} voices (${predefinedVoices.length} predefined, ${referenceVoices.length} reference)`);
344 return this.voices;
345 } catch (error) {
346 console.error('Error fetching Chatterbox voices:', error);
347 this.voices = [];
348 return [];
349 }
350 }
351
352 // Alias for internal use
353 async fetchVoices() {
354 return this.fetchTtsVoiceObjects();
355 }
356
357 //###########################//
358 // Setup Event Listeners //
359 //###########################//
360
361 setupEventListeners() {
362 // Server endpoint change
363 $('#chatterbox-endpoint').on('input', () => {
364 this.settings.provider_endpoint = $('#chatterbox-endpoint').val();
365 this.onSettingsChange();
366 });
367
368 // Language
369 $('#chatterbox-language').on('change', (e) => {
370 this.settings.language = e.target.value;
371 this.onSettingsChange();
372 });
373
374 // Parameter sliders
375 $('#chatterbox-temperature').on('input', (e) => {
376 this.settings.temperature = parseFloat(e.target.value);
377 $('#chatterbox-temperature-value').text(this.settings.temperature);
378 this.onSettingsChange();
379 });
380
381 $('#chatterbox-exaggeration').on('input', (e) => {
382 this.settings.exaggeration = parseFloat(e.target.value);
383 $('#chatterbox-exaggeration-value').text(this.settings.exaggeration);
384 this.onSettingsChange();
385 });
386
387 $('#chatterbox-cfg-weight').on('input', (e) => {
388 this.settings.cfg_weight = parseFloat(e.target.value);
389 $('#chatterbox-cfg-weight-value').text(this.settings.cfg_weight);
390 this.onSettingsChange();
391 });
392
393 $('#chatterbox-speed').on('input', (e) => {
394 this.settings.speed_factor = parseFloat(e.target.value);
395 $('#chatterbox-speed-value').text(this.settings.speed_factor);
396 this.onSettingsChange();
397 });
398
399 // Seed
400 $('#chatterbox-seed').on('change', (e) => {
401 this.settings.seed = parseInt(e.target.value);
402 this.onSettingsChange();
403 });
404
405 // Text splitting
406 $('#chatterbox-split-text').on('change', (e) => {
407 this.settings.split_text = e.target.checked;
408 if (e.target.checked) {
409 $('#chunk-size-row').show();
410 } else {
411 $('#chunk-size-row').hide();
412 }
413 this.onSettingsChange();
414 });
415
416 $('#chatterbox-chunk-size').on('change', (e) => {
417 this.settings.chunk_size = parseInt(e.target.value);
418 this.onSettingsChange();
419 });
420
421 // Output format
422 $('#chatterbox-format').on('change', (e) => {
423 this.settings.output_format = e.target.value;
424 this.onSettingsChange();
425 });
426 }
427
428 //#############################//
429 // Store ST interface settings //
430 //#############################//
431
432 onSettingsChange() {
433 // Save the updated settings
434 saveTtsProviderSettings();
435 }
436
437 //#########################//
438 // Handle Reload button //
439 //#########################//
440
441 async onRefreshClick() {
442 try {
443 this.updateStatus('Processing');
444 await this.checkReady();
445
446 if (this.ready) {
447 await this.fetchTtsVoiceObjects();
448 this.updateStatus('Ready');
449 } else {
450 this.updateStatus('Offline');
451 }
452 } catch (error) {
453 console.error('Error during refresh:', error);
454 this.updateStatus('Offline');
455 }
456 }
457
458 //##################//
459 // Preview Voice //
460 //##################//
461
462 async previewTtsVoice(voiceId) {
463 try {
464 this.updateStatus('Processing');
465
466 const previewText = 'Hello! This is a preview of the selected voice.';
467
468 // Determine if this is a reference voice
469 let isReferenceVoice = false;
470 let actualVoiceId = voiceId;
471
472 if (voiceId && voiceId.startsWith('ref_')) {
473 isReferenceVoice = true;
474 actualVoiceId = voiceId.substring(4); // Remove 'ref_' prefix
475 }
476
477 // Generate preview using the main TTS endpoint
478 const requestBody = {
479 text: previewText,
480 voice_mode: isReferenceVoice ? 'clone' : 'predefined',
481 temperature: this.settings.temperature,
482 exaggeration: this.settings.exaggeration,
483 cfg_weight: this.settings.cfg_weight,
484 seed: this.settings.seed >= 0 ? this.settings.seed : Math.floor(Math.random() * 2147483648), // Use random seed if -1
485 speed_factor: this.settings.speed_factor,
486 language: this.settings.language,
487 split_text: false, // Don't split for preview
488 output_format: this.settings.output_format,
489 };
490
491 // Add voice-specific parameters
492 if (isReferenceVoice) {
493 requestBody.reference_audio_filename = actualVoiceId;
494 } else {
495 requestBody.predefined_voice_id = actualVoiceId;
496 }
497
498 const response = await fetch(`${this.settings.provider_endpoint}/tts`, {
499 method: 'POST',
500 headers: {
501 'Content-Type': 'application/json',
502 },
503 body: JSON.stringify(requestBody),
504 });
505
506 if (!response.ok) {
507 throw new Error(`HTTP ${response.status}: ${response.statusText}`);
508 }
509
510 // Get the audio blob and play it
511 const audioBlob = await response.blob();
512 const audioUrl = URL.createObjectURL(audioBlob);
513
514 const audio = new Audio(audioUrl);
515 audio.addEventListener('ended', () => {
516 URL.revokeObjectURL(audioUrl);
517 this.updateStatus('Ready');
518 });
519
520 await audio.play();
521
522 } catch (error) {
523 console.error('Error previewing voice:', error);
524 this.updateStatus('Ready');
525 throw error;
526 }
527 }
528
529 //#####################//
530 // Get Voice Object //
531 //#####################//
532
533 async getVoice(voiceName) {
534 // Ensure voices are loaded
535 if (this.voices.length === 0) {
536 await this.fetchTtsVoiceObjects();
537 }
538
539 // Find the voice object by name or voice_id
540 let match = this.voices.find(voice =>
541 voice.name === voiceName ||
542 voice.voice_id === voiceName ||
543 voice.display_name === voiceName,
544 );
545
546 if (!match) {
547 console.warn(`Voice not found: ${voiceName}`);
548 // Check if it's a reference voice that wasn't in the list
549 if (voiceName && voiceName.startsWith('ref_')) {
550 const filename = voiceName.substring(4);
551 return {
552 name: `[Clone] ${filename}`,
553 voice_id: voiceName,
554 preview_url: null,
555 lang: 'en',
556 };
557 }
558 // Return a default voice object
559 return {
560 name: voiceName || 'Default',
561 voice_id: voiceName || this.settings.predefined_voice || 'S1',
562 preview_url: null,
563 lang: 'en',
564 };
565 }
566
567 return match;
568 }
569
570 //##################//
571 // Generate TTS //
572 //##################//
573
574 async generateTts(inputText, voiceId) {
575 try {
576 this.updateStatus('Processing');
577
578 // Determine if this is a reference voice
579 let isReferenceVoice = false;
580 let actualVoiceId = voiceId;
581
582 if (voiceId && voiceId.startsWith('ref_')) {
583 isReferenceVoice = true;
584 actualVoiceId = voiceId.substring(4); // Remove 'ref_' prefix
585 }
586
587 // Prepare the request body
588 const requestBody = {
589 text: inputText,
590 voice_mode: isReferenceVoice ? 'clone' : 'predefined',
591 temperature: this.settings.temperature,
592 exaggeration: this.settings.exaggeration,
593 cfg_weight: this.settings.cfg_weight,
594 seed: this.settings.seed >= 0 ? this.settings.seed : Math.floor(Math.random() * 2147483648), // Use random seed if -1
595 speed_factor: this.settings.speed_factor,
596 language: this.settings.language,
597 split_text: this.settings.split_text,
598 chunk_size: this.settings.chunk_size,
599 output_format: this.settings.output_format,
600 };
601
602 // Add voice-specific parameters
603 if (isReferenceVoice) {
604 requestBody.reference_audio_filename = actualVoiceId;
605 } else {
606 requestBody.predefined_voice_id = actualVoiceId || this.settings.predefined_voice;
607 }
608
609 console.log('Generating TTS with params:', requestBody);
610
611 const response = await fetch(`${this.settings.provider_endpoint}/tts`, {
612 method: 'POST',
613 headers: {
614 'Content-Type': 'application/json',
615 'Cache-Control': 'no-cache',
616 },
617 body: JSON.stringify(requestBody),
618 });
619
620 if (!response.ok) {
621 const errorText = await response.text();
622 console.error('TTS generation error:', errorText);
623 throw new Error(`HTTP ${response.status}: ${errorText}`);
624 }
625
626 this.updateStatus('Ready');
627
628 // Return the response directly - SillyTavern expects a Response object
629 return response;
630
631 } catch (error) {
632 console.error('Error in generateTts:', error);
633 this.updateStatus('Ready');
634 throw error;
635 }
636 }
637
638 //######################//
639 // Update Status //
640 //######################//
641
642 updateStatus(status) {
643 const statusElement = document.getElementById('chatterbox-status');
644 if (statusElement) {
645 statusElement.textContent = status;
646 statusElement.className = status.toLowerCase();
647 }
648 }
649}
public/scripts/extensions/tts/index.js+2 -0
@@ -27,6 +27,7 @@ import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashComm
27import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';27import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
28import { POPUP_TYPE, callGenericPopup } from '../../popup.js';28import { POPUP_TYPE, callGenericPopup } from '../../popup.js';
29import { GoogleTranslateTtsProvider } from './google-translate.js';29import { GoogleTranslateTtsProvider } from './google-translate.js';
30import { ChatterboxTtsProvider } from './chatterbox.js';
30import { KokoroTtsProvider } from './kokoro.js';31import { KokoroTtsProvider } from './kokoro.js';
31import { TtsWebuiProvider } from './tts-webui.js';32import { TtsWebuiProvider } from './tts-webui.js';
3233
@@ -89,6 +90,7 @@ export function getPreviewString(lang) {
89const ttsProviders = {90const ttsProviders = {
90 AllTalk: AllTalkTtsProvider,91 AllTalk: AllTalkTtsProvider,
91 Azure: AzureTtsProvider,92 Azure: AzureTtsProvider,
93 Chatterbox: ChatterboxTtsProvider,
92 Coqui: CoquiTtsProvider,94 Coqui: CoquiTtsProvider,
93 'CosyVoice (Unofficial)': CosyVoiceProvider,95 'CosyVoice (Unofficial)': CosyVoiceProvider,
94 Edge: EdgeTtsProvider,96 Edge: EdgeTtsProvider,
public/scripts/slash-commands.js+16 -1
@@ -54,7 +54,7 @@ import { getMessageTimeStamp, isMobile } from './RossAscends-mods.js';
54import { hideChatMessageRange } from './chats.js';54import { hideChatMessageRange } from './chats.js';
55import { getContext, saveMetadataDebounced } from './extensions.js';55import { getContext, saveMetadataDebounced } from './extensions.js';
56import { getRegexedString, regex_placement } from './extensions/regex/engine.js';56import { getRegexedString, regex_placement } from './extensions/regex/engine.js';
57import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js';57import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group, getGroupMembers } from './group-chats.js';
58import { chat_completion_sources, oai_settings, promptManager } from './openai.js';58import { chat_completion_sources, oai_settings, promptManager } from './openai.js';
59import { user_avatar } from './personas.js';59import { user_avatar } from './personas.js';
60import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';60import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js';
@@ -944,6 +944,12 @@ export function initDefaultSlashCommands() {
944 `,944 `,
945 }));945 }));
946 SlashCommandParser.addCommandObject(SlashCommand.fromProps({946 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
947 name: 'member-count',
948 callback: countGroupMemberCallback,
949 aliases: ['countmember', 'membercount'],
950 helpString: 'Returns the total number of group members in the group chat list.',
951 }));
952 SlashCommandParser.addCommandObject(SlashCommand.fromProps({
947 name: 'delswipe',953 name: 'delswipe',
948 callback: deleteSwipeCallback,954 callback: deleteSwipeCallback,
949 returns: 'the new, currently selected swipe id',955 returns: 'the new, currently selected swipe id',
@@ -3546,6 +3552,15 @@ async function peekCallback(_, arg) {
3546 return '';3552 return '';
3547}3553}
35483554
3555async function countGroupMemberCallback() {
3556 if (!selected_group) {
3557 toastr.warning('Cannot run /member-count command outside of a group chat.');
3558 return '';
3559 }
3560
3561 return getGroupMembers(selected_group).length;
3562}
3563
3549async function removeGroupMemberCallback(_, arg) {3564async function removeGroupMemberCallback(_, arg) {
3550 if (!selected_group) {3565 if (!selected_group) {
3551 toastr.warning('Cannot run /member-remove command outside of a group chat.');3566 toastr.warning('Cannot run /member-remove command outside of a group chat.');
public/style.css+4 -0
@@ -715,6 +715,10 @@ hr {
715 opacity: 0.2;715 opacity: 0.2;
716}716}
717717
718#chat hr {
719 opacity: 0.4;
720}
721
718#bg1,722#bg1,
719#bg_custom {723#bg_custom {
720 background-repeat: no-repeat;724 background-repeat: no-repeat;