Connection Manager: quick-switch profile button in the chat bar Reviewed-by: auto-review

c0a14545a4616d74737976c6257fa95ac2a97504

permissionBRICK <permissionBRICK@gmail.com>

2 files changed, +162 -2Ignore whitespace
public/scripts/extensions/connection-manager/index.js+136 -2
@@ -1,6 +1,6 @@
11import { DOMPurify, Fuse, Popper } from '../../../lib.js';
22
33import { activateSendButtons, animation_duration, deactivateSendButtons, event_types, eventSource, main_api, online_status, saveSettingsDebounced } from '../../../script.js';
44import { extension_settings, getContext, renderExtensionTemplateAsync } from '../../extensions.js';
55import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from '../../popup.js';
66import { SlashCommand } from '../../slash-commands/SlashCommand.js';
@@ -695,6 +695,138 @@ async function generateStreamCallback(args, value) {
695695 }
696696}
697697
698+/**
699+ * Adds a quick-switch connection profile button to the bottom-left of the chat bar.
700+ * Clicking it opens a small popup listing all saved connection profiles; clicking a
701+ * profile switches to it by driving the Connection Manager's #connection_profiles select.
702+ */
703+function addQuickSwitchButton() {
704+ const leftSendForm = document.getElementById('leftSendForm');
705+ if (!leftSendForm) {
706+ console.warn('[Connection Manager] #leftSendForm not found, skipping quick-switch button');
707+ return;
708+ }
709+
710+ // Avoid adding the button twice (e.g. on extension re-activation)
711+ if (document.getElementById('connection_profile_switcher')) {
712+ return;
713+ }
714+
715+ const button = document.createElement('div');
716+ button.id = 'connection_profile_switcher';
717+ button.className = 'fa-solid fa-plug interactable';
718+ button.tabIndex = 0;
719+ button.title = t`Quick-switch connection profile`;
720+ button.setAttribute('data-i18n', '[title]Quick-switch connection profile');
721+ leftSendForm.appendChild(button);
722+
723+ const menu = document.createElement('div');
724+ menu.id = 'connection_profile_switcher_menu';
725+ menu.style.display = 'none';
726+ const list = document.createElement('ul');
727+ list.id = 'connection_profile_switcher_list';
728+ list.className = 'list-group';
729+ menu.appendChild(list);
730+ document.body.appendChild(menu);
731+
732+ const $menu = $(menu);
733+ const popper = Popper.createPopper(button, menu, {
734+ placement: 'top-start',
735+ });
736+
737+ /**
738+ * Rebuilds the list of profiles from the current settings.
739+ */
740+ function rebuildList() {
741+ list.innerHTML = '';
742+ const profiles = extension_settings.connectionManager.profiles;
743+ const selectedProfile = extension_settings.connectionManager.selectedProfile;
744+
745+ if (!Array.isArray(profiles) || profiles.length === 0) {
746+ const emptyItem = document.createElement('li');
747+ emptyItem.className = 'list-group-item disabled';
748+ emptyItem.textContent = t`No connection profiles saved`;
749+ emptyItem.setAttribute('data-i18n', 'No connection profiles saved');
750+ list.appendChild(emptyItem);
751+ return;
752+ }
753+
754+ const sortedProfiles = profiles.slice().sort((a, b) => a.name.localeCompare(b.name));
755+ for (const profile of sortedProfiles) {
756+ const item = document.createElement('li');
757+ item.className = 'list-group-item interactable';
758+ item.dataset.profileId = profile.id;
759+ if (profile.id === selectedProfile) {
760+ item.classList.add('selected');
761+ }
762+ const icon = document.createElement('i');
763+ icon.className = profile.id === selectedProfile ? 'fa-fw fa-solid fa-check' : 'fa-fw fa-solid';
764+ const label = document.createElement('span');
765+ label.textContent = profile.name;
766+ item.appendChild(icon);
767+ item.appendChild(label);
768+ list.appendChild(item);
769+ }
770+ }
771+
772+ button.addEventListener('click', (e) => {
773+ e.preventDefault();
774+ if ($menu.is(':visible')) {
775+ $menu.fadeOut(animation_duration);
776+ return;
777+ }
778+ rebuildList();
779+ $menu.fadeIn(animation_duration);
780+ popper.update();
781+ });
782+
783+ // Switch profile when a list item is clicked (event delegation).
784+ list.addEventListener('click', (e) => {
785+ const item = e.target instanceof Element ? e.target.closest('li[data-profile-id]') : null;
786+ if (!item) {
787+ return;
788+ }
789+ const profileId = item.dataset.profileId;
790+ $menu.fadeOut(animation_duration);
791+ if (!profileId) {
792+ return;
793+ }
794+ /** @type {HTMLSelectElement} */
795+ // @ts-ignore
796+ const select = document.getElementById('connection_profiles');
797+ if (!select) {
798+ console.warn('[Connection Manager] #connection_profiles select not found');
799+ return;
800+ }
801+ if (!Array.from(select.options).some(o => o.value === profileId)) {
802+ console.warn(`[Connection Manager] Profile option not found in select: ${profileId}`);
803+ return;
804+ }
805+ select.value = profileId;
806+ select.dispatchEvent(new Event('change'));
807+ });
808+
809+ // Close the menu when clicking outside of it or the button.
810+ document.addEventListener('click', (e) => {
811+ if (!$menu.is(':visible')) {
812+ return;
813+ }
814+ const target = e.target;
815+ if (target instanceof Node && (menu.contains(target) || button.contains(target) || button === target)) {
816+ return;
817+ }
818+ $menu.fadeOut(animation_duration);
819+ });
820+
821+ // Keep the highlighted item up to date while the menu is open.
822+ eventSource.on(event_types.CONNECTION_PROFILE_LOADED, () => {
823+ if ($menu.is(':visible')) {
824+ rebuildList();
825+ popper.update();
826+ }
827+ });
828+}
829+
698830export async function init() {
699831 extension_settings.connectionManager = extension_settings.connectionManager || structuredClone(DEFAULT_SETTINGS);
700832
@@ -713,6 +845,8 @@ export async function init() {
713845 const profiles = document.getElementById('connection_profiles');
714846 renderConnectionProfiles(profiles);
715847
848+ addQuickSwitchButton();
849+
716850 function toggleProfileSpecificButtons() {
717851 const profileId = extension_settings.connectionManager.selectedProfile;
718852 const profileSpecificButtons = ['update_connection_profile', 'reload_connection_profile', 'delete_connection_profile'];
public/scripts/extensions/connection-manager/style.css+26 -0
@@ -9,3 +9,29 @@
99#connection_profile_spinner {
1010 margin-left: 5px;
1111}
12+
13+#connection_profile_switcher {
14+ font-size: var(--bottomFormIconSize);
15+}
16+
17+#connection_profile_switcher_menu {
18+ z-index: 30000;
19+ backdrop-filter: blur(var(--SmartThemeBlurStrength));
20+ -webkit-backdrop-filter: blur(var(--SmartThemeBlurStrength));
21+}
22+
23+#connection_profile_switcher_list {
24+ max-height: calc(100vh - calc(2 * var(--bottomFormBlockSize) + 10px));
25+ max-height: calc(100dvh - calc(2 * var(--bottomFormBlockSize) + 10px));
26+ overflow-y: auto;
27+ min-width: 150px;
28+}
29+
30+#connection_profile_switcher_list .list-group-item.selected {
31+ opacity: 1;
32+}
33+
34+#connection_profile_switcher_list .list-group-item.disabled {
35+ cursor: default;
36+ opacity: 0.5;
37+}