mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-16 03:42:20 +02:00
Squash Odysseus development history
This commit is contained in:
+450
-78
@@ -1,15 +1,16 @@
|
||||
// compare/panes.js — pane lifecycle, actions, layout
|
||||
import state from './state.js';
|
||||
import { _persistSelections } from './models.js';
|
||||
import { buildVoteBar } from './vote.js';
|
||||
import { buildVoteBar } from './vote.js?v=20260828resendcaldrag1';
|
||||
import {
|
||||
ICON_REROLL, ICON_COPY, ICON_EXPAND, ICON_COLLAPSE, ICON_CLOSE,
|
||||
ICON_PLAY, ICON_CODE, SEND_SVG,
|
||||
} from './icons.js';
|
||||
ICON_PLAY, ICON_CODE, SEND_SVG, ICON_DICE,
|
||||
} from './icons.js?v=20260908compareprompts1';
|
||||
import { _clearProbeWaves } from './probe.js';
|
||||
import Storage from '../storage.js';
|
||||
import uiModule from '../ui.js';
|
||||
import uiModule from '../ui.js?v=20260908weekhoverfix1';
|
||||
import spinnerModule from '../spinner.js';
|
||||
import { bindMenuDismiss } from '../escMenuStack.js';
|
||||
|
||||
var escapeHtml = uiModule.esc;
|
||||
|
||||
@@ -32,6 +33,342 @@ function registerPaneActions({ setSendBtn, deactivate, streamToPane, renderSearc
|
||||
/** Slot label: A/B/C in parallel mode, 1/2/3 in sequential. */
|
||||
function _slotChar(i) { return state._parallel ? String.fromCharCode(65 + i) : String(i + 1); }
|
||||
|
||||
/** Keep Shuffle compact until a larger comparison makes it useful as a
|
||||
* direct action. It returns to the kebab after being used. */
|
||||
function syncShuffleButtonPlacement(showDirect = state._selectedModels.length > 2) {
|
||||
const shuffleBtn = document.getElementById('compare-shuffle-btn');
|
||||
const moreMenu = document.querySelector('.compare-more-menu');
|
||||
const moreWrap = document.querySelector('.compare-more-wrap');
|
||||
const headerActions = moreWrap?.parentElement;
|
||||
if (!shuffleBtn || !moreMenu || !moreWrap || !headerActions) return;
|
||||
if (showDirect) {
|
||||
if (shuffleBtn.parentElement !== headerActions) headerActions.insertBefore(shuffleBtn, moreWrap);
|
||||
} else if (shuffleBtn.parentElement !== moreMenu) {
|
||||
moreMenu.appendChild(shuffleBtn);
|
||||
}
|
||||
}
|
||||
|
||||
function _paneModeBadgeHtml(paneIdx) {
|
||||
const mode = String(state._compareMode || 'chat');
|
||||
if (mode !== 'search') return '';
|
||||
const label = ({ agent: 'Agent', search: 'Search', research: 'Research' }[mode] || 'Chat');
|
||||
const detail = ({ agent: 'tools', search: 'web', research: 'sources' }[mode] || 'plain');
|
||||
return '<span class="pane-mode-badge pane-mode-' + escapeHtml(mode) + '" title="' + escapeHtml(label + ' mode') + '">' +
|
||||
'<span class="pane-mode-dot" aria-hidden="true"></span>' +
|
||||
'<span class="pane-mode-label">' + escapeHtml(label) + '</span>' +
|
||||
(mode === 'agent' ? '' : '<span class="pane-mode-detail">' + escapeHtml(detail) + '</span>') +
|
||||
'</span>';
|
||||
}
|
||||
|
||||
const PANE_SETTINGS_ICON = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.6 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06A2 2 0 1 1 7.04 4.3l.06.06A1.65 1.65 0 0 0 8.92 4a1.65 1.65 0 0 0 1-1.51V2a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82 1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1Z"/></svg>';
|
||||
|
||||
function paneSettingsButtonHtml(paneIdx) {
|
||||
return '<button type="button" class="pane-action-btn pane-settings-btn" data-action="settings" data-pane="' + Number(paneIdx) + '" title="Inference settings" aria-label="Inference settings" aria-haspopup="dialog" aria-expanded="false"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg></button>';
|
||||
}
|
||||
|
||||
function _paneModelRequiresThinking(paneIdx) {
|
||||
const modelId = String(state._selectedModels[paneIdx]?.model || '').toLowerCase().split(':', 1)[0];
|
||||
return modelId === 'x-ai/grok-4.5' || modelId === 'grok-4.5';
|
||||
}
|
||||
|
||||
async function _savePaneGenerationSettings(paneIdx, change) {
|
||||
const sid = state._paneSessionIds[paneIdx];
|
||||
if (!sid) return false;
|
||||
const current = state._paneGenerationSettings[paneIdx] || {};
|
||||
const next = {
|
||||
thinking_mode: current.thinking_mode || '',
|
||||
temperature_override: current.temperature_override ?? null,
|
||||
max_tokens_override: current.max_tokens_override ?? null,
|
||||
...change,
|
||||
};
|
||||
try {
|
||||
const res = await fetch(`${state.API_BASE}/api/session/${encodeURIComponent(sid)}/generation-settings`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify(next),
|
||||
});
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
state._paneGenerationSettings[paneIdx] = { ...next, ...await res.json() };
|
||||
return true;
|
||||
} catch (err) {
|
||||
uiModule.showError(`Could not save pane settings: ${err.message || err}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function togglePaneSettings(paneIdx, anchorBtn) {
|
||||
const existing = document.querySelector('.compare-pane-settings-popup');
|
||||
if (existing) {
|
||||
const same = existing.dataset.pane === String(paneIdx);
|
||||
if (typeof existing._dismiss === 'function') existing._dismiss(); else existing.remove();
|
||||
if (same) return;
|
||||
}
|
||||
const settings = state._paneGenerationSettings[paneIdx] || {
|
||||
thinking_mode: '', temperature_override: null, max_tokens_override: null,
|
||||
};
|
||||
const popup = document.createElement('div');
|
||||
popup.className = 'chat-context-popup compare-pane-settings-popup';
|
||||
popup.dataset.pane = String(paneIdx);
|
||||
popup.setAttribute('role', 'dialog');
|
||||
popup.setAttribute('aria-label', 'Inference settings');
|
||||
popup.innerHTML = '<div class="chat-context-popup-title">Inference settings</div>';
|
||||
|
||||
const thinkingRequired = _paneModelRequiresThinking(paneIdx);
|
||||
const thinkingOn = thinkingRequired || settings.thinking_mode === 'on';
|
||||
const thinkingRow = document.createElement('div');
|
||||
thinkingRow.className = 'chat-context-toggle-row';
|
||||
thinkingRow.innerHTML = '<div class="chat-context-toggle-copy"><span>Thinking</span><span class="chat-context-toggle-state">' + (thinkingRequired ? 'Required' : (thinkingOn ? 'On' : 'Off')) + '</span></div>';
|
||||
const thinkingToggle = document.createElement('button');
|
||||
thinkingToggle.type = 'button';
|
||||
thinkingToggle.className = `chat-context-toggle${thinkingOn ? ' active' : ''}`;
|
||||
thinkingToggle.setAttribute('role', 'switch');
|
||||
thinkingToggle.setAttribute('aria-checked', thinkingOn ? 'true' : 'false');
|
||||
thinkingToggle.disabled = thinkingRequired;
|
||||
if (thinkingRequired) thinkingToggle.title = 'This model requires reasoning';
|
||||
thinkingToggle.addEventListener('click', async () => {
|
||||
if (thinkingRequired) return;
|
||||
const next = !thinkingToggle.classList.contains('active');
|
||||
thinkingToggle.disabled = true;
|
||||
if (await _savePaneGenerationSettings(paneIdx, { thinking_mode: next ? 'on' : 'off' })) {
|
||||
thinkingToggle.classList.toggle('active', next);
|
||||
thinkingToggle.setAttribute('aria-checked', next ? 'true' : 'false');
|
||||
thinkingRow.querySelector('.chat-context-toggle-state').textContent = next ? 'On' : 'Off';
|
||||
}
|
||||
thinkingToggle.disabled = false;
|
||||
});
|
||||
thinkingRow.appendChild(thinkingToggle);
|
||||
popup.appendChild(thinkingRow);
|
||||
|
||||
const addSlider = (label, value, min, max, step, formatter, key, normalize) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'chat-context-threshold-row';
|
||||
row.innerHTML = '<div class="chat-context-threshold-top"><span>' + label + '</span><span>' + formatter(value) + '</span></div>';
|
||||
const input = document.createElement('input');
|
||||
Object.assign(input, { type: 'range', min: String(min), max: String(max), step: String(step), value: String(value), className: 'chat-context-threshold-slider preset-range' });
|
||||
input.addEventListener('input', () => { row.querySelector('.chat-context-threshold-top span:last-child').textContent = formatter(Number(input.value)); });
|
||||
input.addEventListener('change', async () => {
|
||||
input.disabled = true;
|
||||
await _savePaneGenerationSettings(paneIdx, { [key]: normalize(Number(input.value)) });
|
||||
input.disabled = false;
|
||||
});
|
||||
row.appendChild(input);
|
||||
popup.appendChild(row);
|
||||
};
|
||||
addSlider('Temperature', settings.temperature_override ?? 1, 0, 2, 0.1, value => Number(value).toFixed(1), 'temperature_override', value => value);
|
||||
addSlider('Max tokens', settings.max_tokens_override ?? 8448, 256, 8448, 256, value => value > 8192 ? 'No limit' : Number(value).toLocaleString(), 'max_tokens_override', value => value > 8192 ? null : value);
|
||||
|
||||
document.body.appendChild(popup);
|
||||
const rect = anchorBtn.getBoundingClientRect();
|
||||
const margin = 8;
|
||||
const popupRect = popup.getBoundingClientRect();
|
||||
popup.style.left = Math.max(margin, Math.min(rect.left, window.innerWidth - popupRect.width - margin)) + 'px';
|
||||
popup.style.top = Math.max(margin, Math.min(rect.bottom + 5, window.innerHeight - popupRect.height - margin)) + 'px';
|
||||
anchorBtn.setAttribute('aria-expanded', 'true');
|
||||
bindMenuDismiss(popup, () => {
|
||||
popup.remove();
|
||||
anchorBtn.setAttribute('aria-expanded', 'false');
|
||||
}, event => !popup.contains(event.target) && event.target !== anchorBtn);
|
||||
}
|
||||
|
||||
function _isMobileCompare() {
|
||||
return window.matchMedia('(max-width: 768px)').matches;
|
||||
}
|
||||
|
||||
function _mobilePaneLabel(index) {
|
||||
if (state._blindMode) return 'Model ' + _slotChar(index);
|
||||
return state._selectedModels[index]?.name || 'Model ' + (index + 1);
|
||||
}
|
||||
|
||||
/** Keep the phone tab strip and its single visible pane in sync. */
|
||||
function refreshMobilePaneTabs(preferredIndex = state._activeMobilePane) {
|
||||
const tabs = document.querySelector('.compare-mobile-tabs');
|
||||
const grid = document.querySelector('.compare-grid');
|
||||
if (!tabs || !grid) return;
|
||||
|
||||
const panes = Array.from(grid.querySelectorAll(':scope > .compare-pane'));
|
||||
if (!panes.length) {
|
||||
tabs.replaceChildren();
|
||||
return;
|
||||
}
|
||||
|
||||
state._activeMobilePane = Math.max(0, Math.min(Number(preferredIndex) || 0, panes.length - 1));
|
||||
const retained = new Set();
|
||||
panes.forEach((pane, index) => {
|
||||
const paneId = 'cmp-pane-' + index;
|
||||
const tabId = 'cmp-mobile-tab-' + index;
|
||||
pane.id = paneId;
|
||||
pane.setAttribute('role', 'tabpanel');
|
||||
pane.setAttribute('aria-labelledby', tabId);
|
||||
|
||||
let tab = tabs.querySelector(`[data-pane="${index}"]`);
|
||||
if (!tab) {
|
||||
tab = document.createElement('button');
|
||||
tab.type = 'button';
|
||||
tab.className = 'compare-mobile-tab';
|
||||
tab.setAttribute('role', 'tab');
|
||||
tab.innerHTML = '<span class="compare-mobile-tab-slot"></span><span class="compare-mobile-tab-label"></span><span class="compare-mobile-tab-state" aria-hidden="true"></span>';
|
||||
}
|
||||
retained.add(tab);
|
||||
tab.id = tabId;
|
||||
tab.dataset.pane = String(index);
|
||||
tab.setAttribute('aria-controls', paneId);
|
||||
tab.querySelector('.compare-mobile-tab-slot').textContent = _slotChar(index);
|
||||
tab.querySelector('.compare-mobile-tab-label').textContent = _mobilePaneLabel(index);
|
||||
tab.classList.toggle('is-streaming', pane.classList.contains('is-streaming'));
|
||||
tab.classList.toggle('is-awaiting-input', pane.classList.contains('is-awaiting-input'));
|
||||
tab.classList.toggle('is-done', pane.classList.contains('is-done'));
|
||||
tab.classList.toggle('is-failed', pane.classList.contains('is-failed'));
|
||||
tabs.appendChild(tab);
|
||||
});
|
||||
|
||||
tabs.querySelectorAll('.compare-mobile-tab').forEach(tab => {
|
||||
if (!retained.has(tab)) tab.remove();
|
||||
});
|
||||
|
||||
let addButton = tabs.querySelector('.compare-mobile-add');
|
||||
if (!addButton && typeof tabs._onAddPane === 'function') {
|
||||
addButton = document.createElement('button');
|
||||
addButton.type = 'button';
|
||||
addButton.className = 'compare-mobile-add';
|
||||
addButton.title = 'Add model';
|
||||
addButton.setAttribute('aria-label', 'Add model');
|
||||
addButton.innerHTML = '<span aria-hidden="true">+</span><span>Add</span>';
|
||||
addButton.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
tabs._onAddPane(addButton);
|
||||
});
|
||||
}
|
||||
if (addButton) {
|
||||
addButton.disabled = !!state._streaming;
|
||||
addButton.style.display = panes.length >= 8 ? 'none' : '';
|
||||
tabs.appendChild(addButton);
|
||||
}
|
||||
|
||||
const mobile = _isMobileCompare();
|
||||
panes.forEach((pane, index) => {
|
||||
const active = index === state._activeMobilePane;
|
||||
pane.classList.toggle('compare-pane-mobile-active', active);
|
||||
if (mobile) pane.setAttribute('aria-hidden', active ? 'false' : 'true');
|
||||
else pane.removeAttribute('aria-hidden');
|
||||
});
|
||||
tabs.querySelectorAll('.compare-mobile-tab').forEach((tab, index) => {
|
||||
const active = index === state._activeMobilePane;
|
||||
tab.classList.toggle('active', active);
|
||||
tab.setAttribute('aria-selected', active ? 'true' : 'false');
|
||||
tab.tabIndex = active ? 0 : -1;
|
||||
});
|
||||
}
|
||||
|
||||
function activateMobilePane(index, { focus = false } = {}) {
|
||||
state._activeMobilePane = index;
|
||||
refreshMobilePaneTabs(index);
|
||||
const tab = document.querySelector(`.compare-mobile-tab[data-pane="${state._activeMobilePane}"]`);
|
||||
if (tab && _isMobileCompare()) {
|
||||
tab.scrollIntoView({ block: 'nearest', inline: 'nearest', behavior: 'smooth' });
|
||||
if (focus) tab.focus();
|
||||
}
|
||||
}
|
||||
|
||||
/** Mount a horizontally scrollable tab row without unmounting inactive streams. */
|
||||
function mountMobilePaneTabs(container, grid, onAddPane) {
|
||||
const tabs = document.createElement('div');
|
||||
tabs.className = 'compare-mobile-tabs';
|
||||
tabs.setAttribute('role', 'tablist');
|
||||
tabs.setAttribute('aria-label', 'Compared models');
|
||||
tabs._onAddPane = onAddPane;
|
||||
container.insertBefore(tabs, grid);
|
||||
|
||||
const onClick = (event) => {
|
||||
const tab = event.target.closest('.compare-mobile-tab');
|
||||
if (tab) activateMobilePane(Number(tab.dataset.pane));
|
||||
};
|
||||
const onKeyDown = (event) => {
|
||||
if (!event.target.closest('.compare-mobile-tab')) return;
|
||||
if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return;
|
||||
event.preventDefault();
|
||||
const last = Math.max(0, state._selectedModels.length - 1);
|
||||
let next = state._activeMobilePane;
|
||||
if (event.key === 'ArrowLeft') next = Math.max(0, next - 1);
|
||||
if (event.key === 'ArrowRight') next = Math.min(last, next + 1);
|
||||
if (event.key === 'Home') next = 0;
|
||||
if (event.key === 'End') next = last;
|
||||
activateMobilePane(next, { focus: true });
|
||||
};
|
||||
const onResize = () => refreshMobilePaneTabs();
|
||||
tabs.addEventListener('click', onClick);
|
||||
tabs.addEventListener('keydown', onKeyDown);
|
||||
window.addEventListener('resize', onResize);
|
||||
|
||||
// On phones, swipe across the response card to move to the adjacent model.
|
||||
// Keep the gesture vertical-scroll friendly and don't steal touches from
|
||||
// buttons, links, selects, or editable response content.
|
||||
let swipeStart = null;
|
||||
const onTouchStart = (event) => {
|
||||
if (!_isMobileCompare() || event.touches.length !== 1) {
|
||||
swipeStart = null;
|
||||
return;
|
||||
}
|
||||
const target = event.target instanceof Element ? event.target : null;
|
||||
if (target?.closest('button, a, select, input, textarea, [contenteditable="true"]')) {
|
||||
swipeStart = null;
|
||||
return;
|
||||
}
|
||||
const touch = event.touches[0];
|
||||
swipeStart = { x: touch.clientX, y: touch.clientY };
|
||||
};
|
||||
const onTouchEnd = (event) => {
|
||||
if (!swipeStart || !_isMobileCompare() || event.changedTouches.length !== 1) return;
|
||||
const touch = event.changedTouches[0];
|
||||
const dx = touch.clientX - swipeStart.x;
|
||||
const dy = touch.clientY - swipeStart.y;
|
||||
swipeStart = null;
|
||||
if (Math.abs(dx) < 48 || Math.abs(dx) < Math.abs(dy) * 1.25) return;
|
||||
const last = Math.max(0, state._selectedModels.length - 1);
|
||||
const next = dx < 0
|
||||
? Math.min(last, state._activeMobilePane + 1)
|
||||
: Math.max(0, state._activeMobilePane - 1);
|
||||
if (next !== state._activeMobilePane) activateMobilePane(next);
|
||||
};
|
||||
grid.addEventListener('touchstart', onTouchStart, { passive: true });
|
||||
grid.addEventListener('touchend', onTouchEnd, { passive: true });
|
||||
|
||||
const observer = new MutationObserver(() => refreshMobilePaneTabs());
|
||||
observer.observe(grid, {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
characterData: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['class', 'data-pane'],
|
||||
});
|
||||
tabs._cleanup = () => {
|
||||
observer.disconnect();
|
||||
window.removeEventListener('resize', onResize);
|
||||
grid.removeEventListener('touchstart', onTouchStart);
|
||||
grid.removeEventListener('touchend', onTouchEnd);
|
||||
};
|
||||
refreshMobilePaneTabs(0);
|
||||
return tabs;
|
||||
}
|
||||
|
||||
function _showShuffleNotice() {
|
||||
const grid = document.querySelector('.compare-grid');
|
||||
if (!grid) return;
|
||||
grid.querySelector('.compare-shuffle-notice')?.remove();
|
||||
const notice = document.createElement('div');
|
||||
notice.className = 'compare-shuffle-notice';
|
||||
notice.setAttribute('role', 'status');
|
||||
notice.setAttribute('aria-live', 'polite');
|
||||
notice.innerHTML = '<span class="compare-shuffle-notice-icon" aria-hidden="true">' + ICON_DICE + '</span><span>Shuffling</span>';
|
||||
grid.appendChild(notice);
|
||||
requestAnimationFrame(() => notice.classList.add('show'));
|
||||
setTimeout(() => {
|
||||
notice.classList.remove('show');
|
||||
notice.addEventListener('transitionend', () => notice.remove(), { once: true });
|
||||
setTimeout(() => notice.remove(), 220);
|
||||
}, 1060);
|
||||
}
|
||||
|
||||
// ── Stop / reroll ──
|
||||
|
||||
function stopAll() {
|
||||
@@ -43,6 +380,9 @@ function stopAll() {
|
||||
document.querySelectorAll('#compare-shuffle-btn, #compare-check-btn, #compare-add-btn').forEach(b => {
|
||||
b.disabled = false; b.style.opacity = '0.7'; b.style.pointerEvents = '';
|
||||
});
|
||||
document.querySelectorAll('.compare-pane').forEach(pane => {
|
||||
pane.classList.remove('is-streaming', 'is-awaiting-input');
|
||||
});
|
||||
}
|
||||
|
||||
function stopPane(paneIdx) {
|
||||
@@ -54,6 +394,8 @@ function stopPane(paneIdx) {
|
||||
// Hide stop button, show reroll
|
||||
const pane = document.querySelector(`.compare-pane[data-pane="${paneIdx}"]`);
|
||||
if (pane) {
|
||||
pane.classList.remove('is-streaming', 'is-awaiting-input', 'is-done');
|
||||
pane.classList.add('is-failed');
|
||||
const stopBtn = pane.querySelector('.pane-stop-btn');
|
||||
if (stopBtn) stopBtn.style.display = 'none';
|
||||
pane.querySelectorAll('.pane-needs-response').forEach(b => b.style.display = '');
|
||||
@@ -97,6 +439,8 @@ async function rerollPane(paneIdx, overrideTimeout) {
|
||||
if (badge) { badge.textContent = ''; badge.style.color = ''; }
|
||||
const timer = document.getElementById('cmp-timer-' + paneIdx);
|
||||
if (timer) timer.textContent = '';
|
||||
const summary = document.getElementById('cmp-summary-' + paneIdx);
|
||||
if (summary) { summary.textContent = ''; summary.title = ''; }
|
||||
|
||||
// Search mode: re-query the search provider
|
||||
if (state._compareMode === 'search') {
|
||||
@@ -282,10 +626,11 @@ async function _addPane(anchorBtn) {
|
||||
|
||||
// Toggle existing dropdown
|
||||
const existing = document.querySelector('.add-pane-dropdown');
|
||||
if (existing) { existing.remove(); return; }
|
||||
if (existing) { if (typeof existing._dismiss === 'function') existing._dismiss(); else existing.remove(); return; }
|
||||
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = 'add-pane-dropdown';
|
||||
let closeMenu = () => dropdown.remove();
|
||||
|
||||
// Search input for large model lists
|
||||
if (filtered.length >= 5) {
|
||||
@@ -326,7 +671,7 @@ async function _addPane(anchorBtn) {
|
||||
|
||||
item.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
dropdown.remove();
|
||||
closeMenu();
|
||||
await _createAndAppendPane(m);
|
||||
});
|
||||
dropdown.appendChild(item);
|
||||
@@ -337,6 +682,8 @@ async function _addPane(anchorBtn) {
|
||||
// chat-container is wider than the viewport.
|
||||
const btnRect = anchorBtn.getBoundingClientRect();
|
||||
dropdown.style.position = 'fixed';
|
||||
dropdown.style.right = 'auto';
|
||||
dropdown.style.bottom = 'auto';
|
||||
const vw = window.innerWidth;
|
||||
const vh = window.innerHeight;
|
||||
const margin = 8;
|
||||
@@ -351,16 +698,18 @@ async function _addPane(anchorBtn) {
|
||||
const ddRect = dropdown.getBoundingClientRect();
|
||||
const ddW = ddRect.width;
|
||||
const ddH = ddRect.height;
|
||||
// Horizontal: align dropdown's right edge with the button's, then
|
||||
// clamp so the dropdown stays within [margin, vw - margin].
|
||||
// Align the dropdown's right edge with the button, then clamp so it stays
|
||||
// within the viewport. This keeps the picker attached to the Add flap
|
||||
// instead of opening at an unrelated edge of the compare surface.
|
||||
let left = btnRect.right - ddW;
|
||||
if (left + ddW > vw - margin) left = vw - margin - ddW;
|
||||
if (left < margin) left = margin;
|
||||
left = Math.max(margin, Math.min(left, vw - margin - ddW));
|
||||
// Vertical: drop below the button if there's room, otherwise above.
|
||||
const spaceBelow = vh - btnRect.bottom;
|
||||
const spaceAbove = btnRect.top;
|
||||
let top;
|
||||
if (spaceBelow >= ddH + margin || spaceBelow >= spaceAbove) {
|
||||
if (anchorBtn.classList.contains('compare-add-flap')) {
|
||||
top = Math.max(margin, Math.min(btnRect.bottom + 6, vh - margin - ddH));
|
||||
} else if (spaceBelow >= ddH + margin || spaceBelow >= spaceAbove) {
|
||||
top = Math.min(btnRect.bottom + 4, vh - margin - Math.min(ddH, vh - margin * 2));
|
||||
} else {
|
||||
top = Math.max(margin, btnRect.top - 4 - ddH);
|
||||
@@ -371,15 +720,8 @@ async function _addPane(anchorBtn) {
|
||||
dropdown.style.bottom = 'auto';
|
||||
dropdown.style.maxHeight = Math.min(ddH, vh - margin * 2) + 'px';
|
||||
|
||||
// Close on outside click
|
||||
const close = (e) => {
|
||||
if (!dropdown.contains(e.target) && e.target !== anchorBtn) {
|
||||
dropdown.remove();
|
||||
document.removeEventListener('click', close);
|
||||
}
|
||||
};
|
||||
setTimeout(() => document.addEventListener('click', close), 0);
|
||||
}
|
||||
// Close on outside click or Escape (the latter via the registry).
|
||||
closeMenu = bindMenuDismiss(dropdown, () => dropdown.remove(), (e) => !dropdown.contains(e.target) && e.target !== anchorBtn);}
|
||||
|
||||
/** Create a new pane for the given model and append it to the compare grid. */
|
||||
async function _createAndAppendPane(m) {
|
||||
@@ -387,7 +729,8 @@ async function _createAndAppendPane(m) {
|
||||
|
||||
// Create session
|
||||
const fd = new FormData();
|
||||
fd.append('name', '[CMP] ' + m.name);
|
||||
// Blind mode: neutral slot name only — never leak the model (issue #1285).
|
||||
fd.append('name', '[CMP] ' + (state._blindMode ? 'Model ' + _slotChar(i) : m.name));
|
||||
fd.append('endpoint_url', m.url || '');
|
||||
fd.append('model', m.id || '');
|
||||
if (m.endpointId) {
|
||||
@@ -401,6 +744,7 @@ async function _createAndAppendPane(m) {
|
||||
// Update arrays
|
||||
state._selectedModels.push({ model: m.id, endpoint: m.url, endpointId: m.endpointId, name: m.name, endpointName: m.endpointName || '' });
|
||||
state._paneSessionIds.push(data.id);
|
||||
state._paneGenerationSettings.push({ thinking_mode: '', temperature_override: null, max_tokens_override: null });
|
||||
state._paneMetrics.push(null);
|
||||
state._abortControllers.push(null);
|
||||
_persistSelections();
|
||||
@@ -413,16 +757,17 @@ async function _createAndAppendPane(m) {
|
||||
pane.dataset.pane = String(i);
|
||||
pane.innerHTML =
|
||||
'<div class="pane-header">' +
|
||||
'<button class="pane-title pane-title-btn" id="cmp-title-' + i + '" data-pane="' + i + '" type="button">' + escapeHtml(label) + ' <span class="pane-title-caret">▾</span></button>' +
|
||||
'<span class="pane-timer" id="cmp-timer-' + i + '"></span>' +
|
||||
'<span class="pane-finish-badge" id="cmp-badge-' + i + '"></span>' +
|
||||
'<div class="pane-actions">' +
|
||||
'<button class="pane-action-btn pane-preview-btn" data-action="preview" data-pane="' + i + '" id="cmp-preview-' + i + '" title="Run preview" style="display:none;">' + ICON_PLAY + '</button>' +
|
||||
'<button class="pane-action-btn" data-action="reroll" data-pane="' + i + '" title="Re-roll">' + ICON_REROLL + '</button>' +
|
||||
'<button class="pane-action-btn" data-action="copy" data-pane="' + i + '" title="Copy">' + ICON_COPY + '</button>' +
|
||||
'<div class="pane-header-row pane-header-primary"><button class="pane-title pane-title-btn" id="cmp-title-' + i + '" data-pane="' + i + '" type="button">' + escapeHtml(label) + ' <span class="pane-title-caret">▾</span></button><div class="pane-primary-actions">' +
|
||||
'<button class="pane-action-btn" data-action="expand" data-pane="' + i + '" title="Expand">' + ICON_EXPAND + '</button>' +
|
||||
'<button class="pane-action-btn pane-close-btn" data-action="close" data-pane="' + i + '" title="Remove pane">' + ICON_CLOSE + '</button>' +
|
||||
'</div>' +
|
||||
paneSettingsButtonHtml(i) +
|
||||
'<button class="close-btn pane-close-btn" data-action="close" data-pane="' + i + '" title="Remove pane"></button></div></div>' +
|
||||
'<div class="pane-header-row pane-header-secondary"><div class="pane-stats">' + _paneModeBadgeHtml(i) +
|
||||
'<span class="pane-timer" id="cmp-timer-' + i + '"></span><span class="pane-summary" id="cmp-summary-' + i + '" role="button" tabindex="0" aria-label="Show response metrics"></span><span class="pane-finish-badge" id="cmp-badge-' + i + '"></span></div>' +
|
||||
'<div class="pane-actions">' +
|
||||
'<button class="pane-action-btn pane-stop-btn" data-action="stop" data-pane="' + i + '" title="Stop" style="display:none;"><svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg></button>' +
|
||||
'<button class="pane-action-btn pane-preview-btn" data-action="preview" data-pane="' + i + '" id="cmp-preview-' + i + '" title="Run preview" style="display:none;">' + ICON_PLAY + '</button>' +
|
||||
'<button class="pane-action-btn pane-needs-response" data-action="reroll" data-pane="' + i + '" title="Re-roll" style="display:none;">' + ICON_REROLL + '</button>' +
|
||||
'<button class="pane-action-btn pane-needs-response" data-action="copy" data-pane="' + i + '" title="Copy" style="display:none;">' + ICON_COPY + '</button></div></div>' +
|
||||
'</div>' +
|
||||
'<div class="chat-history" id="cmp-history-' + i + '"></div>' +
|
||||
'<iframe class="compare-pane-iframe" id="cmp-iframe-' + i + '" sandbox="allow-scripts" style="display:none;"></iframe>' +
|
||||
@@ -440,6 +785,14 @@ async function _createAndAppendPane(m) {
|
||||
// Update grid columns
|
||||
const n = state._selectedModels.length;
|
||||
grid.dataset.cols = String(Math.min(n, 4));
|
||||
syncShuffleButtonPlacement(n > 2);
|
||||
refreshMobilePaneTabs(i);
|
||||
if (_isMobileCompare()) {
|
||||
activateMobilePane(i);
|
||||
requestAnimationFrame(() => {
|
||||
document.querySelector('.compare-mobile-add')?.scrollIntoView({ block: 'nearest', inline: 'end', behavior: 'smooth' });
|
||||
});
|
||||
}
|
||||
|
||||
// Update header label
|
||||
const headerSpan = document.querySelector('.compare-active > div:first-child span');
|
||||
@@ -452,19 +805,6 @@ async function _createAndAppendPane(m) {
|
||||
// Rebuild vote bar
|
||||
buildVoteBar(n);
|
||||
|
||||
// Prompt to shuffle in blind mode — tooltip bubble next to Shuffle button
|
||||
if (state._blindMode && n > 2) {
|
||||
const shuffleBtn = document.getElementById('compare-shuffle-btn');
|
||||
if (shuffleBtn) {
|
||||
const bubble = document.createElement('div');
|
||||
bubble.style.cssText = 'position:absolute;top:100%;right:0;margin-top:6px;background:var(--panel);border:1px solid var(--border);border-radius:6px;padding:5px 10px;font-size:11px;white-space:nowrap;z-index:10000;box-shadow:0 4px 12px rgba(0,0,0,0.25);pointer-events:none;opacity:0;transition:opacity 0.2s;';
|
||||
bubble.textContent = 'Shuffle models?';
|
||||
shuffleBtn.style.position = 'relative';
|
||||
shuffleBtn.appendChild(bubble);
|
||||
requestAnimationFrame(() => { bubble.style.opacity = '1'; });
|
||||
setTimeout(() => { bubble.style.opacity = '0'; setTimeout(() => bubble.remove(), 200); }, 4000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Remove a pane from the compare grid. If only 1 remains, exit compare mode. */
|
||||
@@ -483,6 +823,7 @@ function _removePane(paneIdx) {
|
||||
// Remove from arrays
|
||||
state._selectedModels.splice(paneIdx, 1);
|
||||
state._paneSessionIds.splice(paneIdx, 1);
|
||||
state._paneGenerationSettings.splice(paneIdx, 1);
|
||||
state._paneMetrics.splice(paneIdx, 1);
|
||||
state._abortControllers.splice(paneIdx, 1);
|
||||
_persistSelections();
|
||||
@@ -499,6 +840,7 @@ function _removePane(paneIdx) {
|
||||
grid.querySelectorAll('.compare-pane').forEach(p => p.remove());
|
||||
|
||||
const n = state._selectedModels.length;
|
||||
syncShuffleButtonPlacement(n > 2);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const label = state._blindMode ? 'Model ' + _slotChar(i) : state._selectedModels[i].name;
|
||||
const pane = document.createElement('div');
|
||||
@@ -506,17 +848,17 @@ function _removePane(paneIdx) {
|
||||
pane.dataset.pane = String(i);
|
||||
pane.innerHTML =
|
||||
'<div class="pane-header">' +
|
||||
'<button class="pane-title pane-title-btn" id="cmp-title-' + i + '" data-pane="' + i + '" type="button">' + escapeHtml(label) + ' <span class="pane-title-caret">▾</span></button>' +
|
||||
'<span class="pane-timer" id="cmp-timer-' + i + '"></span>' +
|
||||
'<span class="pane-finish-badge" id="cmp-badge-' + i + '"></span>' +
|
||||
'<div class="pane-header-row pane-header-primary"><button class="pane-title pane-title-btn" id="cmp-title-' + i + '" data-pane="' + i + '" type="button">' + escapeHtml(label) + ' <span class="pane-title-caret">▾</span></button><div class="pane-primary-actions">' +
|
||||
'<button class="pane-action-btn" data-action="expand" data-pane="' + i + '" title="Expand">' + ICON_EXPAND + '</button>' +
|
||||
paneSettingsButtonHtml(i) +
|
||||
'<button class="close-btn pane-close-btn" data-action="close" data-pane="' + i + '" title="Remove pane"></button></div></div>' +
|
||||
'<div class="pane-header-row pane-header-secondary"><div class="pane-stats">' + _paneModeBadgeHtml(i) +
|
||||
'<span class="pane-timer" id="cmp-timer-' + i + '"></span><span class="pane-summary" id="cmp-summary-' + i + '" role="button" tabindex="0" aria-label="Show response metrics"></span><span class="pane-finish-badge" id="cmp-badge-' + i + '"></span></div>' +
|
||||
'<div class="pane-actions">' +
|
||||
'<button class="pane-action-btn pane-stop-btn" data-action="stop" data-pane="' + i + '" title="Stop" style="display:none;"><svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg></button>' +
|
||||
'<button class="pane-action-btn pane-preview-btn" data-action="preview" data-pane="' + i + '" id="cmp-preview-' + i + '" title="Run preview" style="display:none;">' + ICON_PLAY + '</button>' +
|
||||
'<button class="pane-action-btn pane-needs-response" data-action="reroll" data-pane="' + i + '" title="Re-roll" style="display:none;">' + ICON_REROLL + '</button>' +
|
||||
'<button class="pane-action-btn pane-needs-response" data-action="copy" data-pane="' + i + '" title="Copy" style="display:none;">' + ICON_COPY + '</button>' +
|
||||
'<button class="pane-action-btn" data-action="expand" data-pane="' + i + '" title="Expand">' + ICON_EXPAND + '</button>' +
|
||||
'<button class="pane-action-btn pane-close-btn" data-action="close" data-pane="' + i + '" title="Remove pane">' + ICON_CLOSE + '</button>' +
|
||||
'</div>' +
|
||||
'<button class="pane-action-btn pane-needs-response" data-action="copy" data-pane="' + i + '" title="Copy" style="display:none;">' + ICON_COPY + '</button></div></div>' +
|
||||
'</div>' +
|
||||
'<div class="chat-history" id="cmp-history-' + i + '"></div>' +
|
||||
'<iframe class="compare-pane-iframe" id="cmp-iframe-' + i + '" sandbox="allow-scripts" style="display:none;"></iframe>' +
|
||||
@@ -542,16 +884,20 @@ function _removePane(paneIdx) {
|
||||
|
||||
// Rebuild vote bar
|
||||
buildVoteBar(n);
|
||||
refreshMobilePaneTabs(Math.min(paneIdx, n - 1));
|
||||
}
|
||||
|
||||
/** Show a dropdown under the pane title to swap the model for that pane. */
|
||||
function _showModelSwapDropdown(paneIdx, titleBtn) {
|
||||
// Don't allow swaps while streaming
|
||||
if (state._streaming) return;
|
||||
if (state._streaming) {
|
||||
uiModule.showToast('Stop the response or wait for it to finish before changing models.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove any existing dropdown
|
||||
const existing = document.querySelector('.pane-model-dropdown');
|
||||
if (existing) { existing.remove(); return; }
|
||||
if (existing) { if (typeof existing._dismiss === 'function') existing._dismiss(); else existing.remove(); return; }
|
||||
|
||||
const _effectiveType = (state._compareMode === 'agent' || state._compareMode === 'research') ? 'chat' : state._compareMode;
|
||||
const filtered = state._cachedModels.filter(m => m.type === _effectiveType);
|
||||
@@ -559,6 +905,7 @@ function _showModelSwapDropdown(paneIdx, titleBtn) {
|
||||
|
||||
const dropdown = document.createElement('div');
|
||||
dropdown.className = 'pane-model-dropdown';
|
||||
let closeMenu = () => dropdown.remove();
|
||||
|
||||
filtered.forEach(m => {
|
||||
const item = document.createElement('button');
|
||||
@@ -573,34 +920,40 @@ function _showModelSwapDropdown(paneIdx, titleBtn) {
|
||||
}
|
||||
item.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
dropdown.remove();
|
||||
closeMenu();
|
||||
|
||||
// Update the model for this pane and persist
|
||||
state._selectedModels[paneIdx] = {
|
||||
model: m.id, endpoint: m.url, endpointId: m.endpointId, name: m.name,
|
||||
};
|
||||
_persistSelections();
|
||||
if (window._updateCheckBtnState) window._updateCheckBtnState();
|
||||
|
||||
// Delete old session, create new one
|
||||
const oldSid = state._paneSessionIds[paneIdx];
|
||||
if (oldSid) {
|
||||
fetch(`${state.API_BASE}/api/session/${oldSid}`, { method: 'DELETE' }).catch(() => {});
|
||||
}
|
||||
const fd = new FormData();
|
||||
fd.append('name', '[CMP] ' + m.name);
|
||||
// Blind mode: neutral slot name only — never leak the model (issue #1285).
|
||||
fd.append('name', '[CMP] ' + (state._blindMode ? 'Model ' + _slotChar(paneIdx) : m.name));
|
||||
fd.append('endpoint_url', m.url || '');
|
||||
fd.append('model', m.id || '');
|
||||
if (m.endpointId) {
|
||||
fd.append('endpoint_id', m.endpointId);
|
||||
fd.append('skip_validation', 'true');
|
||||
}
|
||||
let newSessionId = '';
|
||||
try {
|
||||
const res = await fetch(`${state.API_BASE}/api/session`, { method: 'POST', body: fd });
|
||||
if (!res.ok) throw new Error('HTTP ' + res.status);
|
||||
const data = await res.json();
|
||||
state._paneSessionIds[paneIdx] = data.id;
|
||||
newSessionId = data.id || '';
|
||||
if (!newSessionId) throw new Error('Missing session id');
|
||||
} catch (err) {
|
||||
console.error('Failed to create session for swapped model:', err);
|
||||
if (uiModule?.showError) uiModule.showError('Failed to swap compare model: ' + (err?.message || 'unknown'));
|
||||
return;
|
||||
}
|
||||
|
||||
const oldSid = state._paneSessionIds[paneIdx];
|
||||
state._selectedModels[paneIdx] = {
|
||||
model: m.id, endpoint: m.url, endpointId: m.endpointId, name: m.name,
|
||||
};
|
||||
state._paneSessionIds[paneIdx] = newSessionId;
|
||||
await _savePaneGenerationSettings(paneIdx, {});
|
||||
_persistSelections();
|
||||
if (window._updateCheckBtnState) window._updateCheckBtnState();
|
||||
if (oldSid) {
|
||||
fetch(`${state.API_BASE}/api/session/${oldSid}`, { method: 'DELETE' }).catch(() => {});
|
||||
}
|
||||
|
||||
// Update title display
|
||||
@@ -621,6 +974,9 @@ function _showModelSwapDropdown(paneIdx, titleBtn) {
|
||||
if (previewBtn) { previewBtn.style.display = 'none'; previewBtn.classList.remove('active'); }
|
||||
const badge = document.getElementById('cmp-badge-' + paneIdx);
|
||||
if (badge) { badge.textContent = ''; badge.style.color = ''; }
|
||||
const summary = document.getElementById('cmp-summary-' + paneIdx);
|
||||
if (summary) { summary.textContent = ''; summary.title = ''; }
|
||||
refreshMobilePaneTabs(paneIdx);
|
||||
});
|
||||
dropdown.appendChild(item);
|
||||
});
|
||||
@@ -653,15 +1009,8 @@ function _showModelSwapDropdown(paneIdx, titleBtn) {
|
||||
dropdown.style.top = top + 'px';
|
||||
dropdown.style.maxHeight = Math.min(ddH, vh - margin * 2) + 'px';
|
||||
|
||||
// Close on outside click
|
||||
const close = (e) => {
|
||||
if (!dropdown.contains(e.target) && e.target !== titleBtn) {
|
||||
dropdown.remove();
|
||||
document.removeEventListener('click', close);
|
||||
}
|
||||
};
|
||||
setTimeout(() => document.addEventListener('click', close), 0);
|
||||
}
|
||||
// Close on outside click or Escape (the latter via the registry).
|
||||
closeMenu = bindMenuDismiss(dropdown, () => dropdown.remove(), (e) => !dropdown.contains(e.target) && e.target !== titleBtn);}
|
||||
|
||||
// ── Shuffle / reset ──
|
||||
|
||||
@@ -672,6 +1021,7 @@ function shufflePanePositions() {
|
||||
if (shuffleBtn) { const b = shuffleBtn.querySelector('div'); if (b) b.remove(); }
|
||||
const n = state._selectedModels.length;
|
||||
if (n < 2) return;
|
||||
_showShuffleNotice();
|
||||
|
||||
// Fisher-Yates shuffle to get new order
|
||||
const indices = Array.from({ length: n }, (_, i) => i);
|
||||
@@ -683,6 +1033,7 @@ function shufflePanePositions() {
|
||||
// Reorder internal state
|
||||
const newModels = indices.map(i => state._selectedModels[i]);
|
||||
const newSessionIds = indices.map(i => state._paneSessionIds[i]);
|
||||
const newGenerationSettings = indices.map(i => state._paneGenerationSettings[i]);
|
||||
const newMetrics = indices.map(i => state._paneMetrics[i]);
|
||||
|
||||
// Collect pane contents (HTML) before swapping
|
||||
@@ -698,6 +1049,7 @@ function shufflePanePositions() {
|
||||
// Apply shuffled state
|
||||
state._selectedModels = newModels;
|
||||
state._paneSessionIds = newSessionIds;
|
||||
state._paneGenerationSettings = newGenerationSettings;
|
||||
state._paneMetrics = newMetrics;
|
||||
|
||||
// Spin the shuffle button dice icon
|
||||
@@ -764,7 +1116,10 @@ function shufflePanePositions() {
|
||||
state._blindMode = true;
|
||||
|
||||
// Rebuild vote bar with new labels
|
||||
setTimeout(() => buildVoteBar(n), 250);
|
||||
setTimeout(() => {
|
||||
buildVoteBar(n);
|
||||
refreshMobilePaneTabs();
|
||||
}, 250);
|
||||
}
|
||||
|
||||
function resetCompare() {
|
||||
@@ -773,6 +1128,13 @@ function resetCompare() {
|
||||
|
||||
// Clear last prompt so vote buttons are disabled until next prompt
|
||||
state._lastPrompt = '';
|
||||
state._expectedAnswer = '';
|
||||
const expected = document.getElementById('cmp-eval-expected');
|
||||
if (expected) {
|
||||
expected.classList.add('hidden');
|
||||
const value = expected.querySelector('.cmp-eval-expected-value');
|
||||
if (value) value.textContent = '';
|
||||
}
|
||||
|
||||
// Reset finish badges, titles, winner/loser state
|
||||
state._finishOrder = 0;
|
||||
@@ -781,12 +1143,16 @@ function resetCompare() {
|
||||
for (let i = 0; i < n; i++) {
|
||||
const badge = document.getElementById('cmp-badge-' + i);
|
||||
if (badge) { badge.textContent = ''; badge.style.color = ''; }
|
||||
const summary = document.getElementById('cmp-summary-' + i);
|
||||
if (summary) { summary.textContent = ''; summary.title = ''; }
|
||||
const titleEl = document.getElementById('cmp-title-' + i);
|
||||
if (titleEl) {
|
||||
const lbl = state._blindMode ? 'Model ' + _slotChar(i) : state._selectedModels[i].name;
|
||||
titleEl.innerHTML = escapeHtml(lbl) + ' <span class="pane-title-caret">▾</span>';
|
||||
}
|
||||
if (panes[i]) { panes[i].classList.remove('winner', 'loser'); }
|
||||
if (panes[i]) {
|
||||
panes[i].classList.remove('winner', 'loser', 'is-streaming', 'is-awaiting-input', 'is-done', 'is-failed');
|
||||
}
|
||||
|
||||
// Clear all messages from pane history
|
||||
const hist = document.getElementById('cmp-history-' + i);
|
||||
@@ -823,4 +1189,10 @@ export {
|
||||
_showModelSwapDropdown,
|
||||
shufflePanePositions,
|
||||
resetCompare,
|
||||
mountMobilePaneTabs,
|
||||
syncShuffleButtonPlacement,
|
||||
activateMobilePane,
|
||||
refreshMobilePaneTabs,
|
||||
paneSettingsButtonHtml,
|
||||
togglePaneSettings,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user