Restores classic chat navigation in Google AI Studio and adds a high-performance force-text-paste toggle.
// ==UserScript==
// @name QuickNav for Google AI Studio
// @namespace http://tampermonkey.net/
// @version 22.0
// @description Restores classic chat navigation in Google AI Studio and adds a high-performance force-text-paste toggle.
// @author Axl_script
// @homepageURL https://greasyfork.org/en/scripts/548346-quicknav-for-google-ai-studio
// @contributionURL https://nowpayments.io/donation/axl_script
// @match https://aistudio.google.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// @license MIT
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
if (window.top !== window.self) return;
// Manages undo/redo history and prompt interactions
const PromptActions = {
undoStack: [],
redoStack: [],
textareaRef: null,
reset() {
this.textareaRef = null;
this.undoStack = [];
this.redoStack = [];
},
getTextarea() {
const el = document.querySelector('ms-prompt-box textarea') || document.querySelector('.prompt-box-container textarea');
if (el && el !== this.textareaRef) {
this.textareaRef = el;
this.attachHistoryListeners(el);
}
return el;
},
attachHistoryListeners(textarea) {
textarea.addEventListener('keydown', (e) => {
const isCtrl = e.ctrlKey || e.metaKey;
if (isCtrl && e.code === 'KeyZ' && !e.shiftKey) {
this.handleUndo(textarea);
}
if (isCtrl && (e.code === 'KeyY' || (e.code === 'KeyZ' && e.shiftKey))) {
this.handleRedo(textarea);
}
});
},
handleUndo(textarea) {
const preUndoValue = textarea.value;
setTimeout(() => {
if (textarea.value === preUndoValue && this.undoStack.length > 0) {
const snapshot = this.undoStack.pop();
this.pushToStack(this.redoStack, this.getSnapshot(textarea));
this.applySnapshot(textarea, snapshot);
this.showToast("Undo");
}
}, 0);
},
handleRedo(textarea) {
const preRedoValue = textarea.value;
setTimeout(() => {
if (textarea.value === preRedoValue && this.redoStack.length > 0) {
const snapshot = this.redoStack.pop();
this.pushToStack(this.undoStack, this.getSnapshot(textarea));
this.applySnapshot(textarea, snapshot);
this.showToast("Redo");
}
}, 0);
},
getSnapshot(textarea) {
return {
value: textarea.value,
selectionStart: textarea.selectionStart,
selectionEnd: textarea.selectionEnd,
scrollTop: textarea.scrollTop
};
},
pushToStack(stack, snapshot) {
if (stack.length > 20) stack.shift();
stack.push(snapshot);
},
applySnapshot(textarea, snapshot) {
if (!snapshot) return;
textarea.value = snapshot.value;
textarea.setSelectionRange(snapshot.selectionStart, snapshot.selectionEnd);
textarea.scrollTop = snapshot.scrollTop;
this.triggerInputEvent(textarea);
},
triggerInputEvent(textarea) {
textarea.dispatchEvent(new Event('input', { bubbles: true }));
textarea.dispatchEvent(new Event('change', { bubbles: true }));
},
recordAction(textarea) {
this.pushToStack(this.undoStack, this.getSnapshot(textarea));
this.redoStack = [];
},
clear(isDoubleClick = false) {
const textarea = this.getTextarea();
if (!textarea) return;
if (!isDoubleClick) {
this.showToast("Double-click to clear");
return;
}
textarea.focus();
this.recordAction(textarea);
textarea.value = '';
textarea.style.height = '';
this.triggerInputEvent(textarea);
this.showToast("Cleared");
if (typeof PromptResizer !== 'undefined') {
PromptResizer.forceState(PromptResizer.STATE_MIN);
}
},
async copy() {
const textarea = this.getTextarea();
if (!textarea) return;
textarea.focus();
const text = textarea.value;
if (!text) {
this.showToast("Nothing to copy");
return;
}
try {
await navigator.clipboard.writeText(text);
this.showToast("Copied");
} catch (err) {
this.showToast("Copy Error");
}
},
async paste() {
window.focus();
let clipText;
try {
clipText = await navigator.clipboard.readText();
} catch (err) {
this.showToast("⚠️ Paste failed. Allow Clipboard access.");
return;
}
if (!clipText) {
this.showToast("Clipboard Empty");
return;
}
const textarea = this.getTextarea();
if (!textarea) return;
textarea.focus();
this.recordAction(textarea);
try {
textarea.setRangeText(clipText, textarea.selectionStart, textarea.selectionEnd, 'end');
this.triggerInputEvent(textarea);
textarea.scrollTop = textarea.scrollHeight;
this.showToast("Pasted");
} catch (e) {
this.showToast("Paste Error");
this.undoStack.pop();
}
},
showToast(message) {
let toast = document.querySelector('.quicknav-toast');
if (!toast) {
toast = document.createElement('div');
toast.className = 'quicknav-toast';
const container = document.getElementById('chat-nav-container');
if (container) container.appendChild(toast);
else document.body.appendChild(toast);
}
toast.textContent = message;
if (message.includes('⚠️')) {
toast.style.backgroundColor = '#d93025';
toast.style.color = '#ffffff';
} else {
toast.style.backgroundColor = '';
toast.style.color = '';
}
toast.classList.remove('show');
void toast.offsetWidth;
toast.classList.add('show');
}
};
// Handles forced text pasting functionality
const PasteManager = {
STORAGE_KEY: 'quicknav_force_paste_enabled',
THRESHOLD: 1000,
ICON_PATH: 'M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zM6 20V4h7v5h5v11H6zm8-12V3.5L18.5 8H14z M8 12h8v2H8zm0 4h8v2H8z',
enabled: false,
button: null,
init() {
this.loadState();
document.addEventListener('paste', this.handlePaste.bind(this), true);
},
loadState() {
const stored = localStorage.getItem(this.STORAGE_KEY);
this.enabled = stored === 'true';
},
toggle() {
this.enabled = !this.enabled;
localStorage.setItem(this.STORAGE_KEY, this.enabled);
this.updateButtonVisuals();
PromptActions.showToast(this.enabled ? "Force Text Paste: ON" : "Force Text Paste: OFF");
},
handlePaste(event) {
if (!this.enabled) return;
const target = event.target;
const isTextArea = target.tagName === 'TEXTAREA';
if (!target || !isTextArea) return;
const clipboardData = event.clipboardData;
if (!clipboardData) return;
const text = clipboardData.getData('text/plain');
if (!text || text.length < this.THRESHOLD) return;
event.preventDefault();
event.stopImmediatePropagation();
const start = target.selectionStart;
const end = target.selectionEnd;
target.setRangeText(text, start, end, 'end');
setTimeout(() => {
target.dispatchEvent(new Event('input', { bubbles: true }));
}, 50);
PromptActions.showToast("Pasted as Text");
},
inject(editorRoot) {
if (this.button && document.body.contains(this.button)) {
this.updateButtonVisuals();
return;
}
const toolsButtonInner = editorRoot.querySelector('ms-prompt-box-tools button[iconname="widgets"]');
if (!toolsButtonInner) return;
const toolsContainer = toolsButtonInner.parentElement;
if (!toolsContainer) return;
const btn = document.createElement('button');
btn.className = 'ms-button-borderless ms-button-icon';
btn.style.display = 'flex';
btn.style.alignItems = 'center';
btn.style.justifyContent = 'center';
btn.style.marginLeft = 'auto';
btn.style.height = '28px';
btn.style.width = '28px';
btn.style.minWidth = '28px';
btn.style.borderRadius = '4px';
btn.style.cursor = 'pointer';
btn.style.border = '1px solid transparent';
btn.style.transition = 'background-color 0.2s, color 0.2s';
btn.title = 'Force Plain Text Paste (Prevents file attachment)';
btn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
this.toggle();
};
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("viewBox", "0 0 24 24");
svg.setAttribute("width", "20");
svg.setAttribute("height", "20");
svg.setAttribute("fill", "currentColor");
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", this.ICON_PATH);
svg.appendChild(path);
btn.appendChild(svg);
this.button = btn;
toolsContainer.appendChild(btn);
this.updateButtonVisuals();
},
updateButtonVisuals() {
if (!this.button) return;
if (this.enabled) {
this.button.style.color = '#8ab4f8';
this.button.style.backgroundColor = 'rgba(138, 180, 248, 0.1)';
} else {
this.button.style.color = 'var(--ms-on-surface-variant, #9aa0a6)';
this.button.style.backgroundColor = 'transparent';
}
},
};
// Controls chat scrolling, indexing, fast text extraction, and lazy background enrichment
const ChatController = {
READING_POSITION_RATIO: 0.3,
allTurns: [],
currentIndex: -1,
menuFocusedIndex: -1,
isDownButtonAtEndToggle: false,
JUMP_DISTANCE: 5,
isScrollingProgrammatically: false,
isQueueProcessing: false,
isUnstickingFromBottom: false,
originalScrollTop: 0,
originalCurrentIndex: -1,
loadingQueue: [],
totalToLoad: 0,
chatObserver: null,
readingObserver: null,
visibilityObserver: null,
chatContainerElement: null,
currentScrollContainer: null,
holdTimeout: null,
holdInterval: null,
isBottomHoldActive: false,
bottomHoldTimeout: null,
bottomHoldInterval: null,
throttledBuildTurnIndex: null,
boundHandlePageScroll: null,
boundStopBottomHold: null,
boundHandleEditClick: null,
isScrollLocked: false,
scrollLockInterval: null,
scrollLockTimeout: null,
containerLifecycleObserver: null,
enrichmentTimer: null,
isInputActive: false,
boundFocusIn: null,
boundFocusOut: null,
escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
},
highlightTextInTurn(turnIndex, textToFind) {
if (!textToFind || !textToFind.trim()) return;
const turnElement = this.allTurns[turnIndex];
if (!turnElement) return;
const contentContainer = turnElement.querySelector('.turn-content');
if (!contentContainer) return;
const walker = document.createTreeWalker(contentContainer, NodeFilter.SHOW_TEXT, {
acceptNode: (node) => {
if (node.parentElement && (node.parentElement.closest('ms-thought-chunk') || node.parentElement.closest('.code-preview-text'))) {
return NodeFilter.FILTER_REJECT;
}
return NodeFilter.FILTER_ACCEPT;
}
});
const regex = new RegExp(this.escapeRegExp(textToFind), 'i');
let currentNode;
while (currentNode = walker.nextNode()) {
const match = regex.exec(currentNode.nodeValue);
if (match) {
const range = document.createRange();
range.setStart(currentNode, match.index);
range.setEnd(currentNode, match.index + match[0].length);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
return;
}
}
},
init(chatContainer) {
this.destroy();
this.chatContainerElement = chatContainer;
this.setupInputListeners();
this.containerLifecycleObserver = new MutationObserver(this.handleContainerMutations.bind(this));
this.containerLifecycleObserver.observe(this.chatContainerElement, {
childList: true,
subtree: true
});
this.boundHandlePageScroll = UIManager.handlePageScroll.bind(UIManager);
this.boundStopBottomHold = this.stopBottomHold.bind(this);
this.boundHandleEditClick = this.handleEditClick.bind(this);
this.checkForScrollContainer();
},
setupInputListeners() {
if (this.boundFocusIn) return;
this.boundFocusIn = (e) => {
const target = e.target;
if (target && target.tagName === 'TEXTAREA' && (target.closest('ms-prompt-box') || target.closest('.prompt-box-container'))) {
this.isInputActive = true;
}
};
this.boundFocusOut = (e) => {
const target = e.target;
if (target && target.tagName === 'TEXTAREA' && (target.closest('ms-prompt-box') || target.closest('.prompt-box-container'))) {
this.isInputActive = false;
if (this.readingObserver && this.currentScrollContainer) {
this.buildTurnIndex();
}
}
};
document.addEventListener('focusin', this.boundFocusIn);
document.addEventListener('focusout', this.boundFocusOut);
},
handleContainerMutations(mutations) {
let shouldCheck = false;
for (const mutation of mutations) {
if (mutation.type === 'childList') {
if (mutation.target && mutation.target.closest && (mutation.target.closest('ms-prompt-box') || mutation.target.closest('.prompt-box-container'))) {
continue;
}
if (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0) {
shouldCheck = true;
break;
}
}
}
if (shouldCheck) {
this.checkForScrollContainer();
}
},
checkForScrollContainer() {
const newScrollContainer = this.chatContainerElement ? this.chatContainerElement.querySelector('ms-autoscroll-container') : null;
if (newScrollContainer && newScrollContainer !== this.currentScrollContainer) {
this._startCoreLogic(newScrollContainer);
}
},
_startCoreLogic(scrollContainer) {
if (this.currentScrollContainer) {
this.currentScrollContainer.removeEventListener('scroll', this.boundHandlePageScroll);
}
this.currentScrollContainer = scrollContainer;
if (this.throttledBuildTurnIndex && this.throttledBuildTurnIndex.cancel) {
this.throttledBuildTurnIndex.cancel();
}
this.throttledBuildTurnIndex = this.throttleDebounce(this.buildTurnIndex.bind(this), 1000);
this.initializeChatObserver(this.currentScrollContainer);
this.setupReadingObserver();
this.buildTurnIndex();
this.currentScrollContainer.addEventListener('scroll', this.boundHandlePageScroll, {
passive: true
});
document.removeEventListener('mousedown', this.boundStopBottomHold, true);
document.removeEventListener('wheel', this.boundStopBottomHold, true);
document.removeEventListener('click', this.boundHandleEditClick, true);
document.addEventListener('mousedown', this.boundStopBottomHold, true);
document.addEventListener('wheel', this.boundStopBottomHold, true);
document.addEventListener('click', this.boundHandleEditClick, true);
},
setupReadingObserver() {
if (this.readingObserver) this.readingObserver.disconnect();
const options = {
root: this.currentScrollContainer,
rootMargin: '-45% 0px -50% 0px',
threshold: 0
};
this.readingObserver = new IntersectionObserver((entries) => {
if (this.isScrollingProgrammatically || this.isQueueProcessing || this.isInputActive) return;
const visibleEntries = entries.filter(e => e.isIntersecting);
if (visibleEntries.length === 0) return;
const targetEntry = visibleEntries[visibleEntries.length - 1];
const turnElement = targetEntry.target.closest('ms-chat-turn');
if (turnElement) {
const newIndex = this.allTurns.indexOf(turnElement);
if (newIndex !== -1 && newIndex !== this.currentIndex) {
const oldIndex = this.currentIndex;
this.currentIndex = newIndex;
requestAnimationFrame(() => {
UIManager.updateHighlight(oldIndex, newIndex);
UIManager.updateCounterDisplay();
});
}
}
}, options);
},
handleRealtimeScrollSync() {
return;
},
destroy() {
if (this.containerLifecycleObserver) this.containerLifecycleObserver.disconnect();
if (this.chatObserver) this.chatObserver.disconnect();
if (this.visibilityObserver) this.visibilityObserver.disconnect();
if (this.readingObserver) this.readingObserver.disconnect();
if (this.throttledBuildTurnIndex && this.throttledBuildTurnIndex.cancel) {
this.throttledBuildTurnIndex.cancel();
}
if (this.boundFocusIn) {
document.removeEventListener('focusin', this.boundFocusIn);
this.boundFocusIn = null;
}
if (this.boundFocusOut) {
document.removeEventListener('focusout', this.boundFocusOut);
this.boundFocusOut = null;
}
this.isInputActive = false;
this.containerLifecycleObserver = null;
this.chatObserver = null;
this.visibilityObserver = null;
this.readingObserver = null;
this.throttledBuildTurnIndex = null;
this.clearScrollLock();
if (this.enrichmentTimer) {
clearTimeout(this.enrichmentTimer);
this.enrichmentTimer = null;
}
if (this.currentScrollContainer) {
if (this.boundHandlePageScroll) this.currentScrollContainer.removeEventListener('scroll', this.boundHandlePageScroll);
}
if (this.boundStopBottomHold) {
document.removeEventListener('mousedown', this.boundStopBottomHold, true);
document.removeEventListener('wheel', this.boundStopBottomHold, true);
}
if (this.boundHandleEditClick) {
document.removeEventListener('click', this.boundHandleEditClick, true);
}
this.currentScrollContainer = null;
this.chatContainerElement = null;
this.allTurns.forEach(turn => {
turn.cachedContent = null;
turn._isStale = false;
});
this.allTurns = [];
this.currentIndex = -1;
this.menuFocusedIndex = -1;
},
clearScrollLock() {
clearInterval(this.scrollLockInterval);
clearTimeout(this.scrollLockTimeout);
this.scrollLockInterval = null;
this.scrollLockTimeout = null;
this.isScrollLocked = false;
},
handleEditClick(event) {
const editButton = event.target.closest('ms-chat-turn .toggle-edit-button');
if (!editButton || this.isScrollLocked) return;
const turnElement = event.target.closest('ms-chat-turn');
const scrollContainer = this.currentScrollContainer;
if (!turnElement || !scrollContainer) return;
const buttonIcon = editButton.querySelector('.ms-button-icon-symbol');
const isEnteringEditMode = buttonIcon && buttonIcon.textContent.trim() !== 'done_all';
if (!isEnteringEditMode) return;
const savedScrollTop = scrollContainer.scrollTop;
this.isScrollLocked = true;
const immediateClearLock = () => {
this.clearScrollLock();
scrollContainer.removeEventListener('wheel', immediateClearLock, {
once: true
});
scrollContainer.removeEventListener('mousedown', immediateClearLock, {
once: true
});
};
scrollContainer.addEventListener('wheel', immediateClearLock, {
once: true
});
scrollContainer.addEventListener('mousedown', immediateClearLock, {
once: true
});
this.scrollLockInterval = setInterval(() => {
if (scrollContainer.scrollTop !== savedScrollTop) {
scrollContainer.scrollTop = savedScrollTop;
}
}, 15);
this.scrollLockTimeout = setTimeout(() => {
immediateClearLock();
}, 5000);
},
throttleDebounce(func, delay) {
let timeout = null;
let lastRun = 0;
const fn = (...args) => {
const now = Date.now();
if (now - lastRun >= delay) {
func.apply(this, args);
lastRun = now;
} else {
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(this, args);
lastRun = Date.now();
}, delay - (now - lastRun));
}
};
fn.cancel = () => clearTimeout(timeout);
return fn;
},
initializeChatObserver(container) {
if (!container) return;
const observerCallback = (mutationsList) => {
if (this.isInputActive) return;
let shouldRebuildIndex = false;
const potentialCodeBlocks = new Set();
let needsEnrichment = false;
for (const mutation of mutationsList) {
if (mutation.target && mutation.target.closest && mutation.target.closest('footer')) continue;
if (mutation.type === 'characterData') {
const target = mutation.target.parentElement;
const turn = target ? target.closest('ms-chat-turn') : null;
if (turn && this.allTurns.includes(turn)) {
if (turn.querySelector('loading-indicator')) continue;
const content = turn.querySelector('.turn-content');
if (content) content._quickNav_contentCache = undefined;
turn._isStale = true;
needsEnrichment = true;
}
continue;
}
if (mutation.type === 'childList') {
if (mutation.addedNodes.length > 0) {
for (const node of mutation.addedNodes) {
if (node.nodeType !== 1) continue;
const tagName = node.tagName.toUpperCase();
if (tagName === 'MS-CHAT-TURN' || (node.classList && node.classList.contains('chat-turn-container'))) {
shouldRebuildIndex = true;
}
if (tagName === 'MS-THOUGHT-CHUNK' || node.querySelector('ms-thought-chunk')) {
shouldRebuildIndex = true;
}
if (tagName === 'MS-CODE-BLOCK' || (node.querySelector && node.querySelector('ms-code-block'))) {
const turn = node.closest('ms-chat-turn');
if (turn) potentialCodeBlocks.add(turn);
}
}
}
if (mutation.removedNodes.length > 0) {
for (const node of mutation.removedNodes) {
if (node.nodeType !== 1) continue;
const tagName = node.tagName.toUpperCase();
if (tagName === 'LOADING-INDICATOR' || tagName === 'MS-THOUGHT-CHUNK') {
const turn = mutation.target.closest('ms-chat-turn');
if (turn) {
turn._isStale = true;
needsEnrichment = true;
}
}
}
}
if (!shouldRebuildIndex) {
const target = mutation.target.nodeType === 3 ? mutation.target.parentElement : mutation.target;
const turn = target ? target.closest('ms-chat-turn') : null;
if (turn && this.allTurns.includes(turn)) {
if (turn.querySelector('loading-indicator')) continue;
const content = turn.querySelector('.turn-content');
if (content && (content === target || content.contains(target))) {
content._quickNav_contentCache = undefined;
}
turn._isStale = true;
needsEnrichment = true;
}
}
}
}
if (shouldRebuildIndex) {
this.throttledBuildTurnIndex();
} else {
if (potentialCodeBlocks.size > 0) {
CodeBlockNavigator.processTurns(Array.from(potentialCodeBlocks));
}
if (needsEnrichment) {
this.scheduleEnrichment();
}
}
};
if (this.chatObserver) this.chatObserver.disconnect();
this.chatObserver = new MutationObserver(observerCallback);
this.chatObserver.observe(container, {
childList: true,
subtree: true,
characterData: true
});
},
scheduleEnrichment() {
if (this.enrichmentTimer) return;
this.enrichmentTimer = setTimeout(() => {
this.enrichmentTimer = null;
if (window.requestIdleCallback) {
requestIdleCallback((dl) => this.processEnrichment(dl), { timeout: 2000 });
} else {
this.processEnrichment();
}
}, 500);
},
processEnrichment(deadline = null) {
let processed = 0;
const startTime = performance.now();
for (let i = 0; i < this.allTurns.length; i++) {
const turn = this.allTurns[i];
if (turn._isStale || !turn.cachedContent || turn.cachedContent.source !== 'dom') {
if (!turn.isConnected) continue;
const isGenerating = !!turn.querySelector('loading-indicator');
if (isGenerating) continue;
const newContent = UIManager.getTextFromTurn(turn);
if (newContent.source === 'dom') {
turn.cachedContent = newContent;
turn._isStale = false;
UIManager.updateMenuItemContent(i);
} else if (newContent.source === 'empty') {
turn._isStale = false;
if (!turn.cachedContent) {
const fastText = this.getFastScrollbarText(turn);
turn.cachedContent = { display: fastText, full: fastText, source: 'scrollbar' };
UIManager.updateMenuItemContent(i);
}
}
processed++;
if (deadline && deadline.timeRemaining() < 5) break;
if (!deadline && (performance.now() - startTime) > 15) break;
}
}
const hasMore = this.allTurns.some(t => t._isStale || !t.cachedContent || t.cachedContent.source !== 'dom');
if (hasMore) {
if (window.requestIdleCallback) {
this.enrichmentTimer = requestIdleCallback((dl) => this.processEnrichment(dl), { timeout: 2000 });
} else {
this.enrichmentTimer = setTimeout(() => this.processEnrichment(), 100);
}
} else {
this.enrichmentTimer = null;
}
},
getFastScrollbarText(turn) {
if (!turn.id) return '...';
const turnId = turn.id.replace('turn-', '');
const scrollbarButton = document.getElementById(`scrollbar-item-${turnId}`);
if (scrollbarButton && scrollbarButton.getAttribute('aria-label')) {
return scrollbarButton.getAttribute('aria-label');
}
return '...';
},
setupVisibilityObserver() {
if (this.visibilityObserver) {
this.visibilityObserver.disconnect();
}
const scrollContainer = this.currentScrollContainer;
if (!scrollContainer || this.allTurns.length === 0) {
return;
}
const observerCallback = (entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
const visibleTurnElement = entry.target;
CodeBlockNavigator.processTurns([visibleTurnElement]);
}
}
};
this.visibilityObserver = new IntersectionObserver(observerCallback, {
root: scrollContainer,
rootMargin: "0px",
});
this.allTurns.forEach(turn => this.visibilityObserver.observe(turn));
},
isUserPrompt(turnElement) {
const container = turnElement.querySelector('.virtual-scroll-container') || turnElement.querySelector('.chat-turn-container');
return container?.dataset.turnRole === 'User' || !!turnElement.querySelector('.chat-turn-container.user');
},
_hasMeaningfulContent(contentContainer) {
if (!contentContainer) return false;
if (contentContainer._quickNav_contentCache !== undefined) {
return contentContainer._quickNav_contentCache;
}
if (contentContainer.querySelector('ms-search-entry-point, ms-grounding-sources, ms-function-call')) {
return (contentContainer._quickNav_contentCache = true);
}
const blocks = contentContainer.querySelectorAll('ms-text-chunk, ms-code-block, .cmark-node');
for (let i = 0; i < blocks.length; i++) {
if (!blocks[i].closest('ms-thought-chunk, .thought-panel')) {
return (contentContainer._quickNav_contentCache = true);
}
}
return (contentContainer._quickNav_contentCache = false);
},
buildTurnIndex() {
try {
const rawTurns = Array.from(document.querySelectorAll('ms-chat-turn'));
const freshTurns = [];
for (let i = 0; i < rawTurns.length; i++) {
const turn = rawTurns[i];
let isCandidate = false;
try {
const scrollContainer = turn.querySelector('.virtual-scroll-container');
const mainContainer = turn.querySelector('.chat-turn-container');
const isUser = (scrollContainer?.dataset.turnRole === 'User') || mainContainer?.classList.contains('user');
if (isUser) {
isCandidate = true;
} else {
const isModel = (scrollContainer?.dataset.turnRole === 'Model') || mainContainer?.classList.contains('model');
if (isModel) {
if (turn.querySelector('.toggle-edit-button')) {
isCandidate = true;
} else {
const contentContainer = turn.querySelector('.turn-content');
if (!contentContainer) {
isCandidate = false;
} else {
const hasContent = contentContainer.children.length > 0 || contentContainer.textContent.trim().length > 0;
if (hasContent) {
isCandidate = this._hasMeaningfulContent(contentContainer);
} else {
const nextTurn = rawTurns[i + 1];
let nextIsModel = false;
if (nextTurn) {
const ns = nextTurn.querySelector('.virtual-scroll-container');
const nm = nextTurn.querySelector('.chat-turn-container');
const nextIsUser = (ns?.dataset.turnRole === 'User') || nm?.classList.contains('user');
nextIsModel = !nextIsUser;
}
isCandidate = !nextIsModel;
}
}
}
}
}
} catch (err) {
isCandidate = false;
}
if (isCandidate) {
freshTurns.push(turn);
}
}
if (freshTurns.length !== this.allTurns.length || !this.arraysEqual(this.allTurns, freshTurns)) {
const freshTurnsSet = new Set(freshTurns);
this.allTurns.forEach(oldTurn => {
if (oldTurn && !freshTurnsSet.has(oldTurn)) {
const container = oldTurn.querySelector('.chat-turn-container');
if (container) {
container.classList.remove('prompt-turn-highlight', 'response-turn-highlight');
}
}
});
const contentCache = new Map();
this.allTurns.forEach(turn => {
if (turn && turn.id && turn.cachedContent) {
contentCache.set(turn.id, {
content: turn.cachedContent,
isFallback: turn.isFallbackContent,
isStale: turn._isStale
});
}
});
this.allTurns = freshTurns;
this.allTurns.forEach(turn => {
if (turn.id && contentCache.has(turn.id)) {
const cachedData = contentCache.get(turn.id);
turn.cachedContent = cachedData.content;
turn.isFallbackContent = cachedData.isFallback;
turn._isStale = cachedData.isStale;
} else {
turn.cachedContent = null;
turn.isFallbackContent = false;
turn._isStale = true;
}
});
if (this.allTurns.length === 0) {
this.currentIndex = -1;
} else {
if (this.currentIndex === -1) {
this.currentIndex = this.allTurns.length - 1;
UIManager.updateHighlight(-1, this.currentIndex);
} else if (this.currentIndex >= this.allTurns.length) {
this.currentIndex = this.allTurns.length - 1;
UIManager.updateHighlight(-1, this.currentIndex);
} else {
UIManager.updateHighlight(-1, this.currentIndex);
}
}
this.setupVisibilityObserver();
if (this.readingObserver) {
this.readingObserver.disconnect();
this.allTurns.forEach(turn => {
const container = turn.querySelector('.chat-turn-container');
if (container) this.readingObserver.observe(container);
});
}
this.scheduleEnrichment();
const menuContainer = document.getElementById('chat-nav-menu-container');
if (menuContainer && menuContainer.classList.contains('visible')) {
UIManager.populateNavMenu();
const menuList = document.getElementById('chat-nav-menu');
const items = menuList ? menuList.querySelectorAll('.chat-nav-menu-item') : [];
if (items.length > 0) {
const safeFocusIndex = Math.min(this.menuFocusedIndex, items.length - 1);
UIManager.updateMenuFocus(items, Math.max(0, safeFocusIndex), false);
}
}
}
CodeBlockNavigator.processTurns(this.allTurns);
UIManager.updateCounterDisplay();
} catch (e) {
console.error('QuickNav Index Build Error:', e);
}
},
arraysEqual(a, b) {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
},
setupHoldableButton(button, action) {
const HOLD_DELAY = 300;
const HOLD_INTERVAL = 100;
let isHolding = false;
const stopHold = () => {
clearTimeout(this.holdTimeout);
clearInterval(this.holdInterval);
this.holdInterval = null;
isHolding = false;
};
button.addEventListener('click', (e) => {
if (!isHolding) {
action();
}
stopHold();
});
button.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
isHolding = true;
this.holdTimeout = setTimeout(() => {
if (isHolding) {
this.holdInterval = setInterval(action, HOLD_INTERVAL);
}
}, HOLD_DELAY);
});
button.addEventListener('mouseup', stopHold);
button.addEventListener('mouseleave', stopHold);
},
scrollToAbsoluteTop() {
const scrollContainer = this.currentScrollContainer;
if (!scrollContainer) return;
UIManager.hideBadge(0, true);
this.isScrollingProgrammatically = true;
scrollContainer.scrollTo({
top: 0,
behavior: this.holdInterval ? 'auto' : 'smooth'
});
if (this.allTurns.length > 0 && this.currentIndex !== 0) {
UIManager.updateHighlight(this.currentIndex, 0);
this.currentIndex = 0;
UIManager.updateCounterDisplay();
}
setTimeout(() => {
this.isScrollingProgrammatically = false;
UIManager.showBadge();
UIManager.updateScrollPercentage();
UIManager.hideBadge(2000);
this.focusChatContainer();
}, this.holdInterval ? 50 : 800);
},
waitForContentLoad(turnElement) {
return new Promise((resolve) => {
if (!turnElement.querySelector('loading-indicator')) return resolve();
const observer = new MutationObserver(() => {
if (!turnElement.querySelector('loading-indicator')) {
observer.disconnect();
resolve();
}
});
observer.observe(turnElement, {
childList: true,
subtree: true
});
setTimeout(() => {
observer.disconnect();
resolve();
}, 30000);
});
},
waitForTurnToStabilize(turnElement, timeout = 1000) {
return new Promise(async (resolve) => {
if (!turnElement || !turnElement.isConnected) return resolve();
await this.waitForContentLoad(turnElement);
let stableTimer = null;
let maxTimeoutTimer = null;
const observer = new ResizeObserver(() => {
clearTimeout(stableTimer);
stableTimer = setTimeout(() => {
observer.disconnect();
clearTimeout(maxTimeoutTimer);
resolve();
}, 100);
});
observer.observe(turnElement);
stableTimer = setTimeout(() => {
observer.disconnect();
clearTimeout(maxTimeoutTimer);
resolve();
}, 100);
maxTimeoutTimer = setTimeout(() => {
observer.disconnect();
clearTimeout(stableTimer);
resolve();
}, timeout);
});
},
async scrollToTurn(index, blockPosition = 'center', instant = false) {
const targetTurn = this.allTurns[index];
if (!targetTurn) {
this.isScrollingProgrammatically = false;
return;
}
this.isScrollingProgrammatically = true;
const scrollContainer = this.currentScrollContainer;
if (!scrollContainer) {
this.isScrollingProgrammatically = false;
return;
}
let isInterrupted = false;
const interruptHandler = () => {
isInterrupted = true;
this.isScrollingProgrammatically = false;
};
const interactionEvents = ['wheel', 'mousedown', 'touchstart', 'keydown'];
interactionEvents.forEach(evt => {
scrollContainer.addEventListener(evt, interruptHandler, { capture: true, once: true });
});
const cleanupListeners = () => {
interactionEvents.forEach(evt => {
scrollContainer.removeEventListener(evt, interruptHandler, { capture: true });
});
};
const REQUIRED_STABLE_FRAMES = 30;
const MAX_EXECUTION_TIME = 5000;
const startTime = performance.now();
let consecutiveStableFrames = 0;
const calculateTarget = () => {
const viewportHeight = scrollContainer.clientHeight;
const verticalOffset = viewportHeight * this.READING_POSITION_RATIO;
let target = 0;
if (blockPosition === 'end') {
target = targetTurn.offsetTop - (viewportHeight - targetTurn.offsetHeight);
} else if (blockPosition === 'start') {
target = targetTurn.offsetTop - 80;
} else {
target = targetTurn.offsetTop - verticalOffset;
}
return Math.max(0, Math.min(target, scrollContainer.scrollHeight - viewportHeight));
};
const initialTarget = calculateTarget();
if (instant || !!this.holdInterval || this.isQueueProcessing) {
scrollContainer.scrollTop = initialTarget;
} else {
scrollContainer.scrollTo({
top: initialTarget,
behavior: 'smooth'
});
await new Promise(r => setTimeout(r, 50));
}
const maintainPosition = () => {
if (isInterrupted || !this.isScrollingProgrammatically) {
cleanupListeners();
this.isScrollingProgrammatically = false;
return;
}
const currentTarget = calculateTarget();
const currentScroll = scrollContainer.scrollTop;
const diff = Math.abs(currentScroll - currentTarget);
if (diff > 1) {
scrollContainer.scrollTop = currentTarget;
consecutiveStableFrames = 0;
} else {
consecutiveStableFrames++;
}
const isTimedOut = (performance.now() - startTime) > MAX_EXECUTION_TIME;
const isFullyStable = consecutiveStableFrames >= REQUIRED_STABLE_FRAMES;
if (!isTimedOut && !isFullyStable) {
requestAnimationFrame(maintainPosition);
} else {
cleanupListeners();
if (!this.isQueueProcessing) {
this.isScrollingProgrammatically = false;
requestAnimationFrame(() => {
UIManager.showBadge();
UIManager.updateScrollPercentage();
UIManager.hideBadge(2000);
});
}
}
};
requestAnimationFrame(maintainPosition);
},
async navigateToIndex(newIndex, blockPosition = 'center') {
if (newIndex < 0 || newIndex >= this.allTurns.length) return;
UIManager.hideBadge(0, true);
const oldIndex = this.currentIndex;
if (newIndex < this.allTurns.length - 1) this.isDownButtonAtEndToggle = false;
this.currentIndex = newIndex;
UIManager.updateHighlight(oldIndex, newIndex);
UIManager.updateCounterDisplay();
await this.scrollToTurn(newIndex, blockPosition);
UIManager.updateScrollPercentage();
this.focusChatContainer();
},
async forceScrollToTop(scrollContainer) {
return new Promise(resolve => {
this.isScrollingProgrammatically = true;
const startTime = performance.now();
const animate = () => {
if (scrollContainer.scrollTop <= 0 || performance.now() - startTime > 1000) {
this.isScrollingProgrammatically = false;
resolve();
return;
}
scrollContainer.scrollTop = 0;
requestAnimationFrame(animate);
};
requestAnimationFrame(animate);
});
},
async startDynamicMenuLoading() {
if (this.isQueueProcessing) return;
const loadButton = document.getElementById('chat-nav-load-button');
const scrollContainer = this.currentScrollContainer;
if (!scrollContainer || !loadButton) return;
if (this.chatObserver) {
this.chatObserver.disconnect();
}
const menuList = document.getElementById('chat-nav-menu');
const menuItems = menuList ? menuList.querySelectorAll('.chat-nav-menu-item') : [];
if (menuList && menuItems.length > 0) {
const targetIndex = this.currentIndex > -1 ? this.currentIndex : 0;
UIManager.updateMenuFocus(menuItems, targetIndex, true);
menuList.focus({
preventScroll: true
});
}
this.originalCurrentIndex = this.currentIndex;
this.originalScrollTop = scrollContainer.scrollTop;
this.loadingQueue = this.allTurns
.map((turn, index) => ({
turn,
index,
menuItem: menuItems[index]
}))
.filter(item => {
return item.turn._isStale || !item.turn.cachedContent || item.turn.cachedContent.source !== 'dom';
});
if (this.loadingQueue.length > 0) {
this.totalToLoad = this.loadingQueue.length;
loadButton.disabled = true;
loadButton.classList.add('loading-active');
this.isQueueProcessing = true;
await this.forceScrollToTop(scrollContainer);
this.processLoadingQueue();
} else {
this.initializeChatObserver(this.currentScrollContainer);
const statusIndicator = document.getElementById('chat-nav-loader-status');
if (statusIndicator) {
statusIndicator.textContent = 'All loaded.';
statusIndicator.classList.add('google-text-flash');
statusIndicator.addEventListener('animationend', () => {
statusIndicator.classList.remove('google-text-flash');
}, {
once: true
});
}
}
},
pollForContent(turn) {
return new Promise((resolve, reject) => {
const initialCheck = UIManager.getTextFromTurn(turn);
if (initialCheck.source === 'dom') return resolve(initialCheck);
let checkCount = 0;
const quickCheckInterval = setInterval(() => {
if (!this.isQueueProcessing) {
clearInterval(quickCheckInterval);
return reject(new Error('Stop'));
}
const content = UIManager.getTextFromTurn(turn);
if (content.source === 'dom') {
clearInterval(quickCheckInterval);
resolve(content);
}
checkCount++;
if (checkCount > 20) {
clearInterval(quickCheckInterval);
resolve(content);
}
}, 50);
});
},
async processLoadingQueue() {
const statusIndicator = document.getElementById('chat-nav-loader-status');
const menuItems = document.querySelectorAll('#chat-nav-menu .chat-nav-menu-item');
const menuList = document.getElementById('chat-nav-menu');
const YIELD_THRESHOLD = 12;
let startTime = performance.now();
while (this.loadingQueue.length > 0 && this.isQueueProcessing) {
if (performance.now() - startTime > YIELD_THRESHOLD) {
await new Promise(r => setTimeout(r, 0));
startTime = performance.now();
}
const itemsProcessed = this.totalToLoad - this.loadingQueue.length;
if (statusIndicator) {
statusIndicator.textContent = `Loading ${itemsProcessed + 1} of ${this.totalToLoad}...`;
}
const itemToLoad = this.loadingQueue.shift();
const {
turn,
index,
menuItem
} = itemToLoad;
if (menuItem && menuList) {
UIManager.updateMenuFocus(menuItems, index, false);
const itemTop = menuItem.offsetTop;
const itemHeight = menuItem.clientHeight;
const menuHeight = menuList.clientHeight;
menuList.scrollTo({
top: itemTop - (menuHeight / 2) + (itemHeight / 2),
behavior: 'smooth'
});
}
const textSpan = menuItem ? menuItem.querySelector('.menu-item-text') : null;
if (!turn || !textSpan) continue;
menuItem.classList.add('loading-in-progress');
try {
await this.scrollToTurn(index, 'center', true);
const newContent = await this.pollForContent(turn);
turn.cachedContent = newContent;
turn._isStale = false;
const truncatedText = (newContent.display.length > 200) ? newContent.display.substring(0, 197) + '...' : newContent.display;
textSpan.textContent = truncatedText;
menuItem.dataset.tooltip = newContent.full.replace(/\s+/g, ' ');
if (typeof UIManager !== 'undefined' && UIManager.currentSearchTerm) {
const term = UIManager.currentSearchTerm;
const text = menuItem.dataset.tooltip.toLowerCase();
const visibleText = textSpan.textContent.toLowerCase();
if (text.includes(term) || visibleText.includes(term)) {
menuItem.style.display = '';
menuItem.dataset.visible = 'true';
} else {
menuItem.style.display = 'none';
menuItem.dataset.visible = 'false';
}
}
} catch (error) {
} finally {
menuItem.classList.remove('loading-in-progress');
}
}
this.isQueueProcessing = false;
this.initializeChatObserver(this.currentScrollContainer);
CodeBlockNavigator.processTurns(this.allTurns);
if (this.originalCurrentIndex > -1) {
await this.navigateToIndex(this.originalCurrentIndex, 'center');
} else {
const scrollContainer = this.currentScrollContainer;
if (scrollContainer) {
this.isScrollingProgrammatically = true;
scrollContainer.scrollTo({
top: 0,
behavior: 'auto'
});
setTimeout(() => {
this.isScrollingProgrammatically = false;
}, 100);
}
}
const menuContainer = document.getElementById('chat-nav-menu-container');
const loadButton = document.getElementById('chat-nav-load-button');
if (loadButton) {
loadButton.disabled = false;
loadButton.classList.remove('loading-active');
}
if (statusIndicator) {
statusIndicator.textContent = this.loadingQueue.length > 0 ? 'Stopped.' : 'Done.';
statusIndicator.classList.remove('google-text-flash');
if (this.loadingQueue.length === 0) {
statusIndicator.classList.add('google-text-flash');
statusIndicator.addEventListener('animationend', () => {
statusIndicator.classList.remove('google-text-flash');
}, {
once: true
});
}
}
if (menuContainer && menuContainer.classList.contains('visible')) {
const items = menuContainer.querySelectorAll('.chat-nav-menu-item');
UIManager.updateMenuFocus(items, this.currentIndex, true);
} else {
this.focusChatContainer();
}
},
stopDynamicMenuLoading() {
if (!this.isQueueProcessing) return;
this.isQueueProcessing = false;
const loadButton = document.getElementById('chat-nav-load-button');
if (loadButton) {
loadButton.disabled = false;
loadButton.classList.remove('loading-active');
}
const statusIndicator = document.getElementById('chat-nav-loader-status');
if (statusIndicator) {
statusIndicator.textContent = 'Stopped.';
statusIndicator.classList.remove('google-text-animated');
}
this.initializeChatObserver(this.currentScrollContainer);
},
stopBottomHold() {
if (!this.isBottomHoldActive) return;
this.isBottomHoldActive = false;
clearInterval(this.bottomHoldInterval);
this.bottomHoldInterval = null;
const btnBottom = document.getElementById('nav-bottom');
if (btnBottom) {
btnBottom.classList.remove('auto-click-active');
}
},
focusChatContainer() {
const scrollContainer = this.currentScrollContainer;
if (scrollContainer) {
scrollContainer.tabIndex = -1;
scrollContainer.focus({
preventScroll: true
});
}
},
focusPromptInput() {
const targetElement = document.querySelector('ms-prompt-box textarea') || document.querySelector('.prompt-box-container textarea');
if (!targetElement) {
console.error("QuickNav: Prompt input textarea not found.");
return;
}
const originalTransition = targetElement.style.transition;
const originalShadow = targetElement.style.boxShadow;
targetElement.style.transition = 'box-shadow 0.2s ease-out';
targetElement.style.boxShadow = '0 0 0 2px var(--ms-primary, #8ab4f8)';
setTimeout(() => {
targetElement.style.boxShadow = originalShadow;
setTimeout(() => {
targetElement.style.transition = originalTransition;
}, 200);
}, 400);
const mouseDownEvent = new MouseEvent('mousedown', {
bubbles: true,
cancelable: true,
view: window
});
const mouseUpEvent = new MouseEvent('mouseup', {
bubbles: true,
cancelable: true,
view: window
});
const clickEvent = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window
});
targetElement.dispatchEvent(mouseDownEvent);
targetElement.dispatchEvent(mouseUpEvent);
targetElement.dispatchEvent(clickEvent);
targetElement.focus();
}
};
const SafeStyleInjector = {
inject(id, cssContent) {
if (document.getElementById(id)) return;
requestAnimationFrame(() => {
const style = document.createElement('style');
style.id = id;
style.textContent = cssContent;
(document.head || document.documentElement).appendChild(style);
});
}
};
// Manages styles, settings persistence, and UI controls for the application
const StyleManager = {
FONT: {
DEFAULT: 14,
MIN: 10,
MAX: 32,
STEP: 1,
current: 14,
enabled: true,
storageKey: 'quicknav_font_size',
enableKey: 'quicknav_font_enabled'
},
WIDTH: {
DEFAULT: 1000,
MIN: 600,
MAX: 8000,
STEP: 10,
current: 1000,
enabled: false,
storageKey: 'quicknav_chat_width',
enableKey: 'quicknav_width_enabled'
},
HOVER_MENU: {
enabled: false,
storageKey: 'quicknav_hover_menu_enabled',
enableKey: 'quicknav_hover_menu_enabled'
},
HOVER_DELAY: {
DEFAULT: 500,
MIN: 0,
MAX: 5000,
STEP: 50,
current: 500,
enabled: true,
storageKey: 'quicknav_hover_delay'
},
CODE_PREVIEW: {
enabled: true,
storageKey: 'quicknav_code_preview_enabled',
enableKey: 'quicknav_code_preview_enabled'
},
styleElement: null,
widthResizeObserver: null,
observedElement: null,
ui: {
fontIndicator: null,
fontControls: null,
fontBtnMinus: null,
fontBtnPlus: null,
fontCheckbox: null,
widthIndicator: null,
widthControls: null,
widthBtnMinus: null,
widthBtnPlus: null,
widthCheckbox: null,
hoverCheckbox: null,
hoverControls: null,
hoverIndicator: null,
hoverBtnMinus: null,
hoverBtnPlus: null,
previewCheckbox: null
},
init() {
this.loadSettings();
this.injectStaticBaseStyles();
window.QuickNav_Reset = this.performFactoryReset.bind(this);
setTimeout(() => {
this.setupWidthObserver();
this.applyVariableUpdates();
}, 500);
let resizeTimeout;
window.addEventListener('resize', () => {
const dropdown = document.getElementById('quicknav-settings-dropdown');
if (!dropdown || !dropdown.classList.contains('visible')) return;
if (!resizeTimeout) {
resizeTimeout = setTimeout(() => {
resizeTimeout = null;
this.updateUIDisplay();
}, 150);
}
});
},
performFactoryReset() {
if (!confirm('⚠️ FACTORY RESET ⚠️\n\nThis will delete:\n- All saved Prompts\n- Favorites\n- Custom colors\n- Font & Width settings\n\nAre you sure?')) return;
const knownKeys = [
'quicknav_prompt_library',
'quicknav_tag_colors',
'quicknav_font_size',
'quicknav_font_enabled',
'quicknav_chat_width',
'quicknav_width_enabled',
'quicknav_hover_menu_enabled',
'quicknav_hover_delay',
'quicknav_force_paste_enabled',
'quicknav_prompt_filter',
'quicknav_favorites',
'quicknav_menu_custom_width',
'quicknav_h_collapsed',
'quicknav_h_expanded',
'quicknav_code_preview_enabled'
];
knownKeys.forEach(key => {
try {
if (typeof GM_deleteValue === 'function') {
GM_deleteValue(key);
} else if (typeof GM_setValue === 'function') {
GM_setValue(key, undefined);
}
} catch(e) { }
localStorage.removeItem(key);
});
Object.keys(localStorage).forEach(key => {
if (key.startsWith('quicknav_')) {
localStorage.removeItem(key);
}
});
alert('System Reset Complete. Reloading...');
window.location.reload();
},
disconnectWidthObserver() {
if (this.widthResizeObserver) {
this.widthResizeObserver.disconnect();
this.widthResizeObserver = null;
}
this.observedElement = null;
},
setupWidthObserver() {
const target = document.querySelector('.chat-session-content') || document.querySelector('ms-chunk-editor');
if (target && target !== this.observedElement) {
if (this.widthResizeObserver) this.widthResizeObserver.disconnect();
this.observedElement = target;
this.widthResizeObserver = new ResizeObserver((entries) => {
if (!this.WIDTH.enabled) {
for (const entry of entries) {
if (entry.contentBoxSize) {
const width = entry.contentRect.width;
document.documentElement.style.setProperty('--quicknav-chat-width', `${width}px`);
}
}
}
});
this.widthResizeObserver.observe(target);
}
},
measureAndSyncWidth() {
if (this.observedElement) {
const rect = this.observedElement.getBoundingClientRect();
document.documentElement.style.setProperty('--quicknav-chat-width', `${rect.width}px`);
}
},
_read(key, defaultValue) {
let value;
try {
if (typeof GM_getValue === 'function') {
value = GM_getValue(key);
}
} catch (e) { }
if (value === undefined) {
try {
const local = localStorage.getItem(key);
if (local !== null) {
if (local === 'true') value = true;
else if (local === 'false') value = false;
else if (!isNaN(Number(local))) value = Number(local);
else value = local;
}
} catch (e) { }
}
return value !== undefined ? value : defaultValue;
},
_write(key, value) {
try {
if (typeof GM_setValue === 'function') {
GM_setValue(key, value);
}
} catch (e) { }
try {
localStorage.setItem(key, String(value));
} catch (e) { }
},
loadSettings() {
const savedFont = this._read(this.FONT.storageKey, this.FONT.DEFAULT);
this.FONT.current = parseInt(savedFont, 10) || this.FONT.DEFAULT;
const savedFontEnabled = this._read(this.FONT.enableKey, true);
this.FONT.enabled = !!savedFontEnabled;
const savedWidth = this._read(this.WIDTH.storageKey, this.WIDTH.DEFAULT);
this.WIDTH.current = parseInt(savedWidth, 10) || this.WIDTH.DEFAULT;
const savedWidthEnabled = this._read(this.WIDTH.enableKey, false);
this.WIDTH.enabled = !!savedWidthEnabled;
const savedHover = this._read(this.HOVER_MENU.storageKey, false);
this.HOVER_MENU.enabled = !!savedHover;
const savedHoverDelay = this._read(this.HOVER_DELAY.storageKey, this.HOVER_DELAY.DEFAULT);
this.HOVER_DELAY.current = parseInt(savedHoverDelay, 10);
if (isNaN(this.HOVER_DELAY.current)) this.HOVER_DELAY.current = this.HOVER_DELAY.DEFAULT;
const savedCodePreview = this._read(this.CODE_PREVIEW.enableKey, true);
this.CODE_PREVIEW.enabled = !!savedCodePreview;
},
injectStaticBaseStyles() {
const css = `
:root {
--quicknav-font-size: ${this.FONT.DEFAULT}px;
--quicknav-chat-width: ${this.WIDTH.DEFAULT}px;
}
ms-chat-turn .turn-content {
content-visibility: auto !important;
contain-intrinsic-size: 50px 150px !important;
}
ms-autoscroll-container {
will-change: transform, scroll-position;
}
section.chunk-editor-main > footer,
ms-chunk-editor footer {
contain: layout paint style !important;
z-index: 100 !important;
}
ms-prompt-box textarea,
.prompt-box-container textarea {
will-change: contents, height;
transform: translateZ(0);
}
html.quicknav-font-active .turn-content,
html.quicknav-font-active .turn-content p,
html.quicknav-font-active .turn-content li,
html.quicknav-font-active .turn-content span,
html.quicknav-font-active .turn-content .cmark-node,
html.quicknav-font-active .turn-content pre,
html.quicknav-font-active .turn-content code {
font-size: var(--quicknav-font-size) !important;
line-height: 1.6 !important;
}
html.quicknav-font-active .turn-content textarea,
html.quicknav-font-active .prompt-box-container textarea,
html.quicknav-font-active .prompt-input-wrapper-container textarea,
html.quicknav-font-active ms-prompt-input-wrapper textarea {
font-size: calc(var(--quicknav-font-size) - 1px) !important;
line-height: 1.6 !important;
}
html.quicknav-font-active .chat-nav-menu-item,
html.quicknav-font-active #quicknav-custom-tooltip {
font-size: var(--quicknav-font-size) !important;
}
html.quicknav-width-active .chat-session-content,
html.quicknav-width-active .prompt-box-container,
html.quicknav-width-active ms-prompt-box,
html.quicknav-width-active .prompt-input-wrapper-container {
max-width: var(--quicknav-chat-width) !important;
}
.quicknav-reset-row {
display: flex;
justify-content: flex-end;
padding-top: 4px;
margin-top: 4px;
border-top: 1px solid var(--ms-outline, #dadce0);
}
.quicknav-reset-btn {
width: 24px;
height: 24px;
padding: 0;
margin: 0;
border: 1px solid transparent;
color: var(--ms-on-surface-variant, #9aa0a6);
background: transparent;
border-radius: 4px;
cursor: pointer;
opacity: 0.4;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
}
.quicknav-reset-btn:hover {
opacity: 1;
background-color: rgba(217, 48, 37, 0.1);
color: #d93025;
}
body.dark-theme .quicknav-reset-row {
border-top-color: #5f6368;
}
body.dark-theme .quicknav-reset-btn {
color: #9aa0a6;
}
body.dark-theme .quicknav-reset-btn:hover {
background-color: rgba(242, 139, 130, 0.1);
color: #f28b82;
}
`;
SafeStyleInjector.inject('quicknav-font-styles', css);
},
applyVariableUpdates() {
const root = document.documentElement;
if (this.FONT.enabled) {
root.style.setProperty('--quicknav-font-size', `${this.FONT.current}px`);
root.classList.add('quicknav-font-active');
} else {
root.classList.remove('quicknav-font-active');
}
if (this.WIDTH.enabled) {
root.style.setProperty('--quicknav-chat-width', `${this.WIDTH.current}px`);
root.classList.add('quicknav-width-active');
} else {
root.classList.remove('quicknav-width-active');
this.measureAndSyncWidth();
}
this.updateUIDisplay();
},
applyStyles() {
this.applyVariableUpdates();
},
getSafeWidthMax() {
const scrollContainer = document.querySelector('ms-autoscroll-container') || document.body;
return Math.floor(scrollContainer.clientWidth - 24);
},
updateUIDisplay() {
if (this.ui.fontIndicator) {
this.ui.fontIndicator.textContent = `${this.FONT.current}`;
this.updateIndicatorStyle(this.ui.fontIndicator, this.FONT);
}
if (this.ui.fontCheckbox) {
this.ui.fontCheckbox.checked = this.FONT.enabled;
}
if (this.ui.fontControls) {
if (this.FONT.enabled) {
this.ui.fontControls.classList.remove('disabled');
if (this.ui.fontBtnMinus) this.ui.fontBtnMinus.disabled = (this.FONT.current <= this.FONT.MIN);
if (this.ui.fontBtnPlus) this.ui.fontBtnPlus.disabled = (this.FONT.current >= this.FONT.MAX);
const btnReset = this.ui.fontControls.querySelector('.quicknav-tool-indicator');
if (btnReset) btnReset.disabled = false;
} else {
this.ui.fontControls.classList.add('disabled');
this.ui.fontControls.querySelectorAll('button').forEach(btn => btn.disabled = true);
}
}
if (this.ui.widthIndicator) {
this.ui.widthIndicator.textContent = `${this.WIDTH.current}`;
this.updateIndicatorStyle(this.ui.widthIndicator, this.WIDTH);
}
if (this.ui.widthCheckbox) {
this.ui.widthCheckbox.checked = this.WIDTH.enabled;
}
if (this.ui.widthControls) {
if (this.WIDTH.enabled) {
this.ui.widthControls.classList.remove('disabled');
const safeMax = this.getSafeWidthMax();
if (this.ui.widthBtnMinus) this.ui.widthBtnMinus.disabled = (this.WIDTH.current <= this.WIDTH.MIN);
if (this.ui.widthBtnPlus) {
this.ui.widthBtnPlus.disabled = (this.WIDTH.current >= this.WIDTH.MAX || this.WIDTH.current >= safeMax);
}
const btnReset = this.ui.widthControls.querySelector('.quicknav-tool-indicator');
if (btnReset) btnReset.disabled = false;
} else {
this.ui.widthControls.classList.add('disabled');
this.ui.widthControls.querySelectorAll('button').forEach(btn => btn.disabled = true);
}
}
if (this.ui.hoverCheckbox) {
this.ui.hoverCheckbox.checked = this.HOVER_MENU.enabled;
}
if (this.ui.hoverIndicator) {
this.ui.hoverIndicator.textContent = `${this.HOVER_DELAY.current}`;
this.ui.hoverIndicator.title = `Reset delay to ${this.HOVER_DELAY.DEFAULT}ms`;
const proxyConfig = {
current: this.HOVER_DELAY.current,
DEFAULT: this.HOVER_DELAY.DEFAULT,
enabled: this.HOVER_MENU.enabled
};
this.updateIndicatorStyle(this.ui.hoverIndicator, proxyConfig);
}
if (this.ui.hoverControls) {
if (this.HOVER_MENU.enabled) {
this.ui.hoverControls.classList.remove('disabled');
if (this.ui.hoverBtnMinus) this.ui.hoverBtnMinus.disabled = (this.HOVER_DELAY.current <= this.HOVER_DELAY.MIN);
if (this.ui.hoverBtnPlus) this.ui.hoverBtnPlus.disabled = (this.HOVER_DELAY.current >= this.HOVER_DELAY.MAX);
const btnReset = this.ui.hoverControls.querySelector('.quicknav-tool-indicator');
if (btnReset) btnReset.disabled = false;
} else {
this.ui.hoverControls.classList.add('disabled');
this.ui.hoverControls.querySelectorAll('button').forEach(btn => btn.disabled = true);
}
}
if (this.ui.previewCheckbox) {
this.ui.previewCheckbox.checked = this.CODE_PREVIEW.enabled;
}
},
updateIndicatorStyle(element, config) {
if (config.current !== config.DEFAULT && config.enabled) {
element.style.fontWeight = '500';
element.style.backgroundColor = 'rgba(138, 180, 248, 0.1)';
} else {
element.style.fontWeight = '500';
element.style.backgroundColor = 'transparent';
}
},
toggleSetting(type, state) {
if (type === 'HOVER_MENU') {
this.HOVER_MENU.enabled = state;
this._write(this.HOVER_MENU.enableKey, state);
this.updateUIDisplay();
return;
}
if (type === 'CODE_PREVIEW') {
this.CODE_PREVIEW.enabled = state;
this._write(this.CODE_PREVIEW.enableKey, state);
this.updateUIDisplay();
if (typeof CodeBlockNavigator !== 'undefined') {
CodeBlockNavigator.togglePreviews(state);
}
return;
}
this[type].enabled = state;
this._write(this[type].enableKey, state);
this.applyVariableUpdates();
},
changeValue(type, delta) {
if (type === 'HOVER_DELAY') {
if (!this.HOVER_MENU.enabled) return;
const config = this.HOVER_DELAY;
let newValue = config.current + delta;
if (newValue >= config.MIN && newValue <= config.MAX) {
config.current = newValue;
this._write(config.storageKey, config.current);
this.updateUIDisplay();
}
return;
}
const config = this[type];
if (!config.enabled) return;
let newValue = config.current + delta;
if (type === 'WIDTH') {
const safeVisibleWidth = this.getSafeWidthMax();
if (delta > 0) {
if (config.current >= safeVisibleWidth) return;
if (newValue > safeVisibleWidth) newValue = safeVisibleWidth;
}
if (delta < 0 && config.current > safeVisibleWidth) {
newValue = safeVisibleWidth - config.STEP;
}
}
if (newValue >= config.MIN && newValue <= config.MAX) {
const action = () => {
config.current = newValue;
this._write(config.storageKey, config.current);
this.applyVariableUpdates();
};
if (type === 'FONT') {
this.preserveScrollPosition(action);
} else {
action();
}
}
},
resetValue(type) {
if (type === 'HOVER_DELAY') {
if (!this.HOVER_MENU.enabled) return;
this.HOVER_DELAY.current = this.HOVER_DELAY.DEFAULT;
this._write(this.HOVER_DELAY.storageKey, this.HOVER_DELAY.DEFAULT);
this.updateUIDisplay();
return;
}
const config = this[type];
if (!config.enabled) return;
const action = () => {
config.current = config.DEFAULT;
this._write(config.storageKey, config.current);
this.applyVariableUpdates();
};
if (type === 'FONT') {
this.preserveScrollPosition(action);
} else {
action();
}
},
preserveScrollPosition(actionCallback) {
const scrollContainer = document.querySelector('ms-autoscroll-container');
if (!scrollContainer) {
actionCallback();
return;
}
const originalOverflowAnchor = scrollContainer.style.overflowAnchor;
scrollContainer.style.overflowAnchor = 'none';
const containerRect = scrollContainer.getBoundingClientRect();
const readingLine = containerRect.top + (containerRect.height * 0.5);
const turns = document.querySelectorAll('ms-chat-turn');
let anchor = null;
let closestDist = Infinity;
for (const turn of turns) {
const rect = turn.getBoundingClientRect();
if (rect.top > containerRect.bottom + 500) break;
if (rect.bottom < containerRect.top - 500) continue;
if (rect.top <= readingLine && rect.bottom > readingLine) {
anchor = turn;
break;
}
const distTop = Math.abs(rect.top - readingLine);
const distBottom = Math.abs(rect.bottom - readingLine);
const distance = Math.min(distTop, distBottom);
if (distance < closestDist) {
closestDist = distance;
anchor = turn;
}
}
if (!anchor) {
actionCallback();
scrollContainer.style.overflowAnchor = originalOverflowAnchor;
return;
}
const oldRect = anchor.getBoundingClientRect();
const offsetInElement = readingLine - oldRect.top;
const ratio = oldRect.height > 0 ? offsetInElement / oldRect.height : 0;
actionCallback();
const _ = scrollContainer.scrollHeight;
const newRect = anchor.getBoundingClientRect();
const newOffsetInElement = newRect.height * ratio;
const desiredTop = readingLine - newOffsetInElement;
const diff = newRect.top - desiredTop;
if (Math.abs(diff) > 1) {
scrollContainer.scrollTop += diff;
}
scrollContainer.style.overflowAnchor = originalOverflowAnchor;
},
setupAutoRepeat(button, action) {
let timeout, interval;
const REPEAT_DELAY = 400;
const REPEAT_RATE = 50;
const start = (e) => {
if (e.button !== 0) return;
e.preventDefault();
e.stopPropagation();
if (button.disabled) return;
action();
timeout = setTimeout(() => {
interval = setInterval(() => {
if (button.disabled) {
stop();
} else {
action();
}
}, REPEAT_RATE);
}, REPEAT_DELAY);
};
const stop = () => {
clearTimeout(timeout);
clearInterval(interval);
};
button.addEventListener('mousedown', start);
button.addEventListener('mouseup', stop);
button.addEventListener('mouseleave', stop);
button.addEventListener('click', (e) => e.stopPropagation());
},
createSvgIcon(pathData) {
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("viewBox", "0 0 24 24");
svg.setAttribute("width", "18");
svg.setAttribute("height", "18");
svg.setAttribute("fill", "currentColor");
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", pathData);
svg.appendChild(path);
return svg;
},
createRow(type, iconPath, minusPath, plusPath, titleGroup, titleMinus, titlePlus, titleReset) {
const rowWrapper = document.createElement('div');
rowWrapper.className = 'quicknav-control-row';
const leftGroup = document.createElement('div');
leftGroup.className = 'quicknav-row-label';
leftGroup.title = titleGroup;
leftGroup.style.cursor = 'pointer';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'quicknav-checkbox';
checkbox.checked = this[type].enabled;
checkbox.addEventListener('click', (e) => e.stopPropagation());
checkbox.addEventListener('change', (e) => this.toggleSetting(type, e.target.checked));
if (type === 'FONT') this.ui.fontCheckbox = checkbox;
if (type === 'WIDTH') this.ui.widthCheckbox = checkbox;
const labelIcon = this.createSvgIcon(iconPath);
labelIcon.classList.add('quicknav-icon-label');
leftGroup.append(checkbox, labelIcon);
leftGroup.addEventListener('click', (e) => {
e.stopPropagation();
if (e.target !== checkbox) {
checkbox.checked = !checkbox.checked;
this.toggleSetting(type, checkbox.checked);
}
});
const container = document.createElement('div');
container.className = 'quicknav-toolbar-group';
if (type === 'FONT') this.ui.fontControls = container;
if (type === 'WIDTH') this.ui.widthControls = container;
const btnMinus = document.createElement('button');
btnMinus.className = 'quicknav-tool-btn';
btnMinus.title = titleMinus;
btnMinus.appendChild(this.createSvgIcon(minusPath));
this.setupAutoRepeat(btnMinus, () => this.changeValue(type, -this[type].STEP));
if (type === 'FONT') this.ui.fontBtnMinus = btnMinus;
if (type === 'WIDTH') this.ui.widthBtnMinus = btnMinus;
const btnReset = document.createElement('button');
btnReset.className = 'quicknav-tool-btn quicknav-tool-indicator';
btnReset.title = titleReset;
btnReset.textContent = `${this[type].current}`;
btnReset.addEventListener('click', (e) => { e.stopPropagation(); this.resetValue(type); });
if (type === 'FONT') this.ui.fontIndicator = btnReset;
if (type === 'WIDTH') this.ui.widthIndicator = btnReset;
const btnPlus = document.createElement('button');
btnPlus.className = 'quicknav-tool-btn';
btnPlus.title = titlePlus;
btnPlus.appendChild(this.createSvgIcon(plusPath));
this.setupAutoRepeat(btnPlus, () => this.changeValue(type, this[type].STEP));
if (type === 'FONT') this.ui.fontBtnPlus = btnPlus;
if (type === 'WIDTH') this.ui.widthBtnPlus = btnPlus;
container.append(btnMinus, btnReset, btnPlus);
rowWrapper.append(leftGroup, container);
return rowWrapper;
},
createControls() {
const container = document.createElement('div');
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.gap = '0';
const fontIcon = 'M5 4v3h5.5v12h3V7H19V4z';
const fontMinus = 'M19 13H5v-2h14v2z';
const fontPlus = 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z';
const fontRow = this.createRow('FONT', fontIcon, fontMinus, fontPlus,
'Enable custom font size', 'Decrease font size (Alt + -)', 'Increase font size (Alt + +)', 'Reset font size (Alt + 0)');
const widthIconSimple = 'M21 5H3c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 12H3V7h18v10z';
const widthMinus = 'M19 13H5v-2h14v2z';
const widthPlus = 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z';
const widthRow = this.createRow('WIDTH', widthIconSimple, widthMinus, widthPlus,
'Enable custom chat width', 'Decrease chat width (Shift + Alt + -)', 'Increase chat width (Shift + Alt + +)', 'Reset chat width (Shift + Alt + 0)');
const hoverRow = document.createElement('div');
hoverRow.className = 'quicknav-control-row';
const hoverLeft = document.createElement('div');
hoverLeft.className = 'quicknav-row-label';
hoverLeft.title = 'Open any menu on hover';
hoverLeft.style.cursor = 'pointer';
const hoverCheckbox = document.createElement('input');
hoverCheckbox.type = 'checkbox';
hoverCheckbox.className = 'quicknav-checkbox';
hoverCheckbox.checked = this.HOVER_MENU.enabled;
hoverCheckbox.addEventListener('click', (e) => e.stopPropagation());
hoverCheckbox.addEventListener('change', (e) => this.toggleSetting('HOVER_MENU', e.target.checked));
this.ui.hoverCheckbox = hoverCheckbox;
const hoverIconPath = 'M9 11.24V7.5C9 6.12 10.12 5 11.5 5S14 6.12 14 7.5v3.74c1.21-.81 2-2.18 2-3.74C16 5.01 13.99 3 11.5 3S7 5.01 7 7.5c0 1.56.79 2.93 2 3.74z m9.84 4.63l-4.54-2.26c-.17-.07-.35-.11-.54-.11H13v-6c0-.83-.67-1.5-1.5-1.5S10 6.67 10 7.5v10.74l-3.43-.72c-.08-.01-.15-.03-.24-.03-.31 0-.59.13-.79.33l-.79.8 4.94 4.94c.27.27.65.44 1.06.44h6.79c.75 0 1.33-.55 1.44-1.28l.75-5.27c.01-.07.02-.14.02-.2 0-.62-.38-1.16-.91-1.38z';
const hoverIcon = this.createSvgIcon(hoverIconPath);
hoverIcon.classList.add('quicknav-icon-label');
hoverLeft.append(hoverCheckbox, hoverIcon);
hoverLeft.addEventListener('click', (e) => {
e.stopPropagation();
if (e.target !== hoverCheckbox) {
hoverCheckbox.checked = !hoverCheckbox.checked;
this.toggleSetting('HOVER_MENU', hoverCheckbox.checked);
}
});
const hoverControls = document.createElement('div');
hoverControls.className = 'quicknav-toolbar-group';
this.ui.hoverControls = hoverControls;
const delayMinus = 'M19 13H5v-2h14v2z';
const delayPlus = 'M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z';
const btnMinus = document.createElement('button');
btnMinus.className = 'quicknav-tool-btn';
btnMinus.title = 'Decrease delay';
btnMinus.appendChild(this.createSvgIcon(delayMinus));
this.setupAutoRepeat(btnMinus, () => this.changeValue('HOVER_DELAY', -this.HOVER_DELAY.STEP));
this.ui.hoverBtnMinus = btnMinus;
const btnReset = document.createElement('button');
btnReset.className = 'quicknav-tool-btn quicknav-tool-indicator';
btnReset.title = `Reset delay to ${this.HOVER_DELAY.DEFAULT}ms`;
btnReset.textContent = `${this.HOVER_DELAY.current}`;
btnReset.addEventListener('click', (e) => { e.stopPropagation(); this.resetValue('HOVER_DELAY'); });
this.ui.hoverIndicator = btnReset;
const btnPlus = document.createElement('button');
btnPlus.className = 'quicknav-tool-btn';
btnPlus.title = 'Increase delay';
btnPlus.appendChild(this.createSvgIcon(delayPlus));
this.setupAutoRepeat(btnPlus, () => this.changeValue('HOVER_DELAY', this.HOVER_DELAY.STEP));
this.ui.hoverBtnPlus = btnPlus;
hoverControls.append(btnMinus, btnReset, btnPlus);
hoverRow.append(hoverLeft, hoverControls);
const previewRow = document.createElement('div');
previewRow.className = 'quicknav-control-row';
const previewLeft = document.createElement('div');
previewLeft.className = 'quicknav-row-label';
previewLeft.title = 'Inject text preview into code headers';
previewLeft.style.cursor = 'pointer';
const previewCheckbox = document.createElement('input');
previewCheckbox.type = 'checkbox';
previewCheckbox.className = 'quicknav-checkbox';
previewCheckbox.checked = this.CODE_PREVIEW.enabled;
previewCheckbox.addEventListener('click', (e) => e.stopPropagation());
previewCheckbox.addEventListener('change', (e) => this.toggleSetting('CODE_PREVIEW', e.target.checked));
this.ui.previewCheckbox = previewCheckbox;
const previewIconPath = 'M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z';
const previewIcon = this.createSvgIcon(previewIconPath);
previewIcon.classList.add('quicknav-icon-label');
previewLeft.append(previewCheckbox, previewIcon);
previewLeft.addEventListener('click', (e) => {
e.stopPropagation();
if (e.target !== previewCheckbox) {
previewCheckbox.checked = !previewCheckbox.checked;
this.toggleSetting('CODE_PREVIEW', previewCheckbox.checked);
}
});
previewRow.appendChild(previewLeft);
const resetRow = document.createElement('div');
resetRow.className = 'quicknav-reset-row';
const resetBtn = document.createElement('button');
resetBtn.className = 'quicknav-reset-btn';
resetBtn.title = 'Factory Reset: Clear all saved data and settings';
resetBtn.appendChild(this.createSvgIcon('M13 3a9 9 0 0 0-9 9H1l3.89 3.89.07.14L9 12H6c0-3.87 3.13-7 7-7s7 3.13 7 7-3.13 7-7 7c-1.93 0-3.68-.79-4.94-2.06l-1.42 1.42C8.27 19.99 10.51 21 13 21c4.97 0 9-4.03 9-9s-4.03-9-9-9z'));
resetBtn.onclick = (e) => {
e.stopPropagation();
this.performFactoryReset();
};
resetRow.appendChild(resetBtn);
container.append(fontRow, widthRow, hoverRow, previewRow, resetRow);
this.updateUIDisplay();
return container;
},
};
const TagManager = {
STORAGE_KEY_COLORS: 'quicknav_tag_colors',
STORAGE_KEY_PINNED: 'quicknav_pinned_tags',
colors: {},
pinned: new Set(),
PALETTE: [
'#FF8A80', '#FF80AB', '#EA80FC', '#B388FF',
'#8C9EFF', '#82B1FF', '#80D8FF', '#84FFFF',
'#A7FFEB', '#B9F6CA', '#CCFF90', '#F4FF81',
'#FFFF8D', '#FFE57F', '#FFD180', '#FF9E80'
],
init() {
const rawColors = StyleManager._read(this.STORAGE_KEY_COLORS, '{}');
try { this.colors = JSON.parse(rawColors); } catch (e) { this.colors = {}; }
const rawPinned = StyleManager._read(this.STORAGE_KEY_PINNED, '[]');
try {
const arr = JSON.parse(rawPinned);
this.pinned = new Set(Array.isArray(arr) ? arr : []);
} catch (e) { this.pinned = new Set(); }
},
getColor(tagName) {
if (!this.colors[tagName]) {
const randomColor = this.PALETTE[Math.floor(Math.random() * this.PALETTE.length)];
this.setColor(tagName, randomColor);
}
return this.colors[tagName];
},
setColor(tagName, color) {
this.colors[tagName] = color;
this.saveColors();
},
isPinned(tagName) {
return this.pinned.has(tagName);
},
togglePin(tagName) {
if (this.pinned.has(tagName)) {
this.pinned.delete(tagName);
} else {
this.pinned.add(tagName);
}
this.savePinned();
return this.pinned.has(tagName);
},
saveColors() {
StyleManager._write(this.STORAGE_KEY_COLORS, JSON.stringify(this.colors));
},
savePinned() {
StyleManager._write(this.STORAGE_KEY_PINNED, JSON.stringify(Array.from(this.pinned)));
},
getExportData() {
return {
colors: this.colors,
pinned: Array.from(this.pinned)
};
},
mergeImportData(data) {
if (!data) return;
if (data.colors) {
this.colors = { ...this.colors, ...data.colors };
this.saveColors();
}
if (data.pinned && Array.isArray(data.pinned)) {
data.pinned.forEach(p => this.pinned.add(p));
this.savePinned();
}
}
};
// Manages prompt storage, CRUD operations, and import/export functionality
const PromptLibrary = {
STORAGE_KEY: 'quicknav_prompt_library',
data: [],
currentTagFilter: 'All',
currentSearchQuery: '',
focusedIndex: -1,
_boundKeyHandler: null,
renderLimit: 50,
isLoaded: false,
activeCard: null,
tooltipTimer: null,
load(force = false) {
if (this.isLoaded && !force) return;
TagManager.init();
const raw = StyleManager._read(this.STORAGE_KEY, '[]');
try {
this.data = JSON.parse(raw);
if (!Array.isArray(this.data)) this.data = [];
} catch (e) {
this.data = [];
}
this.currentTagFilter = StyleManager._read('quicknav_prompt_filter', 'All');
this.isLoaded = true;
},
save() {
StyleManager._write(this.STORAGE_KEY, JSON.stringify(this.data));
this.renderList();
this.renderTags();
},
addPrompt(title, tagsStr, content, isPinned = false) {
const tags = tagsStr.split(',').map(t => t.trim()).filter(t => t);
const newPrompt = {
id: Date.now().toString(36) + Math.random().toString(36).substr(2),
title: title.trim() || 'Untitled',
tags: tags,
content: content,
isPinned: !!isPinned
};
this.data.unshift(newPrompt);
if (tags.length > 0) {
this.currentTagFilter = tags[0];
StyleManager._write('quicknav_prompt_filter', this.currentTagFilter);
} else {
this.currentTagFilter = 'All';
StyleManager._write('quicknav_prompt_filter', 'All');
}
this.save();
},
updatePrompt(id, title, tagsStr, content, isPinned = false) {
const idx = this.data.findIndex(p => p.id === id);
if (idx !== -1) {
const tags = tagsStr.split(',').map(t => t.trim()).filter(t => t);
this.data[idx] = {
...this.data[idx],
title: title.trim() || 'Untitled',
tags: tags,
content: content,
isPinned: !!isPinned
};
if (tags.length > 0) {
this.currentTagFilter = tags[0];
StyleManager._write('quicknav_prompt_filter', this.currentTagFilter);
}
this.save();
}
},
deletePrompt(id) {
this.data = this.data.filter(p => p.id !== id);
this.save();
},
insertAtCursor(text) {
const textarea = PromptActions.getTextarea();
if (!textarea) return;
textarea.focus();
PromptActions.recordAction(textarea);
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
textarea.setRangeText(text, start, end, 'end');
PromptActions.triggerInputEvent(textarea);
textarea.scrollTop = textarea.scrollHeight;
this.closeModal();
PromptActions.showToast("Prompt inserted");
},
createModalElements() {
if (document.getElementById('quicknav-lib-modal')) return;
const makeSvg = (pathData) => {
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', '18');
svg.setAttribute('height', '18');
svg.setAttribute('fill', 'currentColor');
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute('d', pathData);
svg.appendChild(path);
return svg;
};
const backdrop = document.createElement('div');
backdrop.id = 'quicknav-lib-backdrop';
backdrop.className = 'quicknav-modal-backdrop';
backdrop.style.backdropFilter = 'none';
backdrop.style.webkitBackdropFilter = 'none';
backdrop.onclick = (e) => {
if (e.target === backdrop) this.closeModal();
};
const modal = document.createElement('div');
modal.id = 'quicknav-lib-modal';
modal.className = 'quicknav-modal';
const header = document.createElement('div');
header.className = 'lib-header';
const topRow = document.createElement('div');
topRow.className = 'lib-header-top';
const searchContainer = document.createElement('div');
searchContainer.className = 'lib-search-container';
const searchIcon = makeSvg('M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z');
searchIcon.style.position = 'absolute';
searchIcon.style.left = '10px';
searchIcon.style.color = '#5f6368';
searchIcon.style.pointerEvents = 'none';
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.placeholder = 'Search prompts...';
searchInput.className = 'lib-search-input';
const clearBtn = document.createElement('span');
clearBtn.textContent = '×';
clearBtn.className = 'lib-search-clear';
clearBtn.onclick = () => {
searchInput.value = '';
this.currentSearchQuery = '';
clearBtn.style.display = 'none';
this.renderList();
searchInput.focus();
};
searchInput.addEventListener('input', (e) => {
this.currentSearchQuery = e.target.value.toLowerCase();
clearBtn.style.display = this.currentSearchQuery ? 'block' : 'none';
this.renderList();
});
searchContainer.append(searchIcon, searchInput, clearBtn);
const actionsGroup = document.createElement('div');
actionsGroup.className = 'lib-actions-group';
const importBtn = document.createElement('button');
importBtn.className = 'lib-btn-secondary';
importBtn.title = 'Import / Export';
importBtn.appendChild(makeSvg('M9 16h6v-6h4l-7-7-7 7h4v6zm-4 2h14v2H5v-2z'));
importBtn.onclick = () => this.openImportExport();
const newBtn = document.createElement('button');
newBtn.className = 'lib-btn-primary';
newBtn.appendChild(makeSvg('M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z'));
newBtn.appendChild(document.createTextNode(' New'));
newBtn.onclick = () => this.openEditor();
actionsGroup.append(importBtn, newBtn);
topRow.append(searchContainer, actionsGroup);
const tagsContainer = document.createElement('div');
tagsContainer.id = 'lib-tags-container';
tagsContainer.className = 'lib-tags-row';
header.append(topRow, tagsContainer);
const listContainer = document.createElement('div');
listContainer.id = 'lib-content-list';
listContainer.className = 'lib-content-list';
modal.append(header, listContainer);
backdrop.appendChild(modal);
document.body.appendChild(backdrop);
},
// Handles global keyboard shortcuts ensuring no conflict with input fields
handleKeyDown(e) {
const isTypingArea = e.target.isContentEditable || e.target.closest('[contenteditable="true"]') || ['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName);
if (e.altKey && e.code === 'KeyP') {
const promptInput = document.querySelector('ms-prompt-box textarea') || document.querySelector('.prompt-box-container textarea');
if (promptInput && document.activeElement !== promptInput) {
e.preventDefault();
e.stopPropagation();
ChatController.focusPromptInput();
return;
}
}
if (e.altKey && !e.shiftKey) {
if (e.code === 'ArrowUp') {
e.preventDefault();
e.stopPropagation();
PromptResizer.shiftState(1);
return;
}
if (e.code === 'ArrowDown') {
e.preventDefault();
e.stopPropagation();
PromptResizer.shiftState(-1);
return;
}
}
if (e.altKey) {
if (e.code === 'KeyC' && !e.shiftKey) {
e.preventDefault();
e.stopPropagation();
PromptActions.copy();
return;
}
if (e.code === 'KeyV' && !e.shiftKey) {
e.preventDefault();
e.stopPropagation();
PromptActions.paste();
return;
}
if (e.code === 'Backspace' && e.shiftKey) {
e.preventDefault();
e.stopPropagation();
PromptActions.clear(true);
return;
}
}
if (e.altKey && (e.code === 'Minus' || e.code === 'Equal')) {
e.preventDefault();
e.stopPropagation();
const now = Date.now();
if (now - this.lastStyleChange < 50) return;
this.lastStyleChange = now;
const isShift = e.shiftKey;
const isPlus = e.code === 'Equal';
const type = isShift ? 'WIDTH' : 'FONT';
const config = StyleManager[type];
const step = isPlus ? config.STEP : -config.STEP;
if (!config.enabled) {
StyleManager.toggleSetting(type, true);
}
StyleManager.changeValue(type, step);
return;
}
if (e.altKey && e.code === 'Digit0') {
e.preventDefault();
e.stopPropagation();
const type = e.shiftKey ? 'WIDTH' : 'FONT';
if (!StyleManager[type].enabled) {
StyleManager.toggleSetting(type, true);
}
StyleManager.resetValue(type);
return;
}
if (e.repeat || (!e.altKey && isTypingArea)) {
return;
}
const combination = this.getKeyCombination(e);
const targetSelector = this.keyMap[combination];
if (targetSelector) {
e.preventDefault();
e.stopPropagation();
if (!this.pressedKeys.has(combination)) {
this.pressedKeys.add(combination);
const targetElement = document.querySelector(targetSelector);
if (targetElement) {
targetElement.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window, buttons: 1 }));
}
}
}
},
updateFocus(cards) {
cards.forEach((card, i) => {
if (i === this.focusedIndex) {
card.classList.add('focused');
card.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
if (card._content) {
this.showTooltip(card._content);
}
} else {
card.classList.remove('focused');
}
});
},
renderTags() {
const container = document.getElementById('lib-tags-container');
if (!container) return;
container.style.alignItems = 'center';
container.style.flexWrap = 'wrap';
container.style.overflowX = 'hidden';
container.style.overflowY = 'auto';
container.style.maxHeight = '85px';
container.replaceChildren();
const chronoTags = new Set();
this.data.forEach(p => {
if (p.tags && Array.isArray(p.tags)) {
p.tags.forEach(t => chronoTags.add(t));
}
});
const pinnedList = [];
const unpinnedList = [];
chronoTags.forEach(tag => {
if (TagManager.isPinned(tag)) {
pinnedList.push(tag);
} else {
unpinnedList.push(tag);
}
});
pinnedList.sort((a, b) => a.localeCompare(b));
const displayOrder = ['All', ...pinnedList, ...unpinnedList];
const isDark = document.body.classList.contains('dark-theme');
displayOrder.forEach(tag => {
const chip = document.createElement('button');
chip.className = `lib-chip ${this.currentTagFilter === tag ? 'active' : ''}`;
chip.textContent = tag;
chip.style.transition = 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)';
if (tag === 'All') {
chip.title = "Show all prompts";
if (this.currentTagFilter === 'All') {
chip.style.backgroundColor = '#1a73e8';
chip.style.color = '#ffffff';
chip.style.boxShadow = '0 2px 6px rgba(0,0,0,0.3)';
chip.style.transform = 'none';
chip.style.fontWeight = '600';
chip.style.zIndex = '2';
chip.style.padding = '6px 14px';
chip.style.fontSize = '12px';
} else {
chip.style.backgroundColor = isDark ? '#3c4043' : '#e8eaed';
chip.style.color = isDark ? '#e8eaed' : '#3c4043';
chip.style.opacity = '0.85';
chip.style.transform = 'none';
chip.style.padding = '1px 12px';
chip.style.fontSize = '12px';
}
chip.onclick = () => {
this.currentTagFilter = tag;
StyleManager._write('quicknav_prompt_filter', tag);
this.renderTags();
this.renderList();
};
} else {
const isPinned = TagManager.isPinned(tag);
const color = TagManager.getColor(tag);
chip.title = `Filter by ${tag}.${isPinned ? ' (Pinned)' : ''}\nDouble-click or Right-click to Edit.`;
chip.style.backgroundColor = color;
chip.style.color = '#202124';
chip.style.border = '1px solid transparent';
if (isPinned) {
chip.style.borderBottom = '2px solid rgba(0,0,0,0.5)';
}
if (this.currentTagFilter === tag) {
chip.style.boxShadow = '0 3px 8px rgba(0,0,0,0.25)';
chip.style.transform = 'none';
chip.style.fontWeight = '700';
chip.style.opacity = '1';
chip.style.zIndex = '2';
chip.style.filter = 'none';
chip.style.padding = '6px 14px';
chip.style.fontSize = '12px';
chip.style.textDecoration = 'underline';
chip.style.textUnderlineOffset = '4px';
chip.style.textDecorationThickness = '2px';
} else {
chip.style.boxShadow = '0 1px 2px rgba(0,0,0,0.1)';
chip.style.opacity = '0.85';
chip.style.transform = 'none';
chip.style.filter = 'none';
chip.style.fontWeight = '500';
chip.style.padding = '1px 12px';
chip.style.fontSize = '12px';
}
chip.onclick = () => {
this.currentTagFilter = tag;
StyleManager._write('quicknav_prompt_filter', tag);
this.renderTags();
this.renderList();
};
chip.oncontextmenu = (e) => {
e.preventDefault();
this.openEditTagDialog(tag);
};
chip.ondblclick = (e) => {
e.preventDefault();
this.openEditTagDialog(tag);
};
}
if (StyleManager.HOVER_MENU.enabled) {
chip.onmouseenter = () => {
if (this.currentTagFilter === tag) return;
this.tagHoverTimer = setTimeout(() => {
this.currentTagFilter = tag;
StyleManager._write('quicknav_prompt_filter', tag);
this.renderTags();
this.renderList();
}, StyleManager.HOVER_DELAY.current);
};
chip.onmouseleave = () => {
clearTimeout(this.tagHoverTimer);
};
}
container.appendChild(chip);
});
},
renderList() {
const container = document.getElementById('lib-content-list');
if (!container) return;
container.replaceChildren();
this.focusedIndex = -1;
let filtered = this.data.filter(p => {
const matchesSearch = p.title.toLowerCase().includes(this.currentSearchQuery) ||
p.content.toLowerCase().includes(this.currentSearchQuery);
const matchesTag = this.currentTagFilter === 'All' || (p.tags && p.tags.includes(this.currentTagFilter));
return matchesSearch && matchesTag;
});
filtered.sort((a, b) => {
const aPin = !!a.isPinned;
const bPin = !!b.isPinned;
if (aPin && !bPin) return -1;
if (!aPin && bPin) return 1;
return 0;
});
if (filtered.length === 0) {
const empty = document.createElement('div');
empty.className = 'lib-empty-state';
empty.textContent = this.data.length === 0 ? 'No prompts yet. Create one!' : 'No matches found.';
container.appendChild(empty);
return;
}
const createIcon = (d, size = 18) => {
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("viewBox", "0 0 24 24");
svg.setAttribute("width", size.toString());
svg.setAttribute("height", size.toString());
svg.setAttribute("fill", "currentColor");
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", d);
svg.appendChild(path);
return svg;
};
const fragment = document.createDocumentFragment();
const itemsToShow = filtered.slice(0, 50);
itemsToShow.forEach(p => {
const card = document.createElement('div');
card.className = 'lib-card';
card._content = p.content;
if (p.isPinned) {
card.style.borderLeft = '3px solid #1a73e8';
}
const cardHeader = document.createElement('div');
cardHeader.className = 'lib-card-header';
cardHeader.style.alignItems = 'flex-start';
const infoGroup = document.createElement('div');
infoGroup.style.display = 'block';
infoGroup.style.flex = '1';
infoGroup.style.minWidth = '0';
infoGroup.style.marginRight = '8px';
infoGroup.style.lineHeight = '1.35';
if (p.isPinned) {
const pinIcon = createIcon("M16 9V4l1 0c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1l1 0v5c0 1.66-1.34 3-3 3v2h5.97v7l1 1 1-1v-7H19v-2c-1.66 0-3-1.34-3-3z", 13);
pinIcon.style.marginRight = '6px';
pinIcon.style.color = '#1a73e8';
pinIcon.style.verticalAlign = 'middle';
pinIcon.style.display = 'inline-block';
infoGroup.appendChild(pinIcon);
}
const tagsWrapper = document.createElement('div');
tagsWrapper.className = 'lib-card-tags';
tagsWrapper.style.display = 'inline';
tagsWrapper.style.margin = '0';
if (p.tags && Array.isArray(p.tags)) {
p.tags.forEach(t => {
const tSpan = document.createElement('span');
tSpan.textContent = t;
tSpan.style.backgroundColor = TagManager.getColor(t);
tSpan.style.color = '#202124';
tSpan.style.borderRadius = '4px';
tSpan.style.padding = '1px 6px';
tSpan.style.fontWeight = '500';
tSpan.style.display = 'inline-flex';
tSpan.style.margin = '0 3px 1px 0';
tSpan.style.verticalAlign = 'middle';
tagsWrapper.appendChild(tSpan);
});
}
infoGroup.appendChild(tagsWrapper);
const title = document.createElement('div');
title.className = 'lib-card-title';
title.textContent = p.title;
title.style.display = 'inline';
title.style.verticalAlign = 'middle';
title.style.marginLeft = (p.tags && p.tags.length > 0) ? '2px' : '0';
infoGroup.appendChild(title);
const actions = document.createElement('div');
actions.className = 'lib-card-actions';
const editBtn = document.createElement('button');
editBtn.className = 'lib-icon-btn';
editBtn.appendChild(createIcon('M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z'));
editBtn.onclick = (e) => { e.stopPropagation(); this.openEditor(p); };
const delBtn = document.createElement('button');
delBtn.className = 'lib-icon-btn delete';
delBtn.appendChild(createIcon('M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z'));
delBtn.onclick = (e) => { e.stopPropagation(); this.confirmDelete(p.id); };
actions.append(editBtn, delBtn);
cardHeader.append(infoGroup, actions);
const preview = document.createElement('div');
preview.className = 'lib-card-preview';
const MAX_PREVIEW_CHARS = 500;
let displayContent = p.content;
if (displayContent && displayContent.length > MAX_PREVIEW_CHARS) {
displayContent = displayContent.substring(0, MAX_PREVIEW_CHARS) + '...';
}
preview.textContent = displayContent;
card.append(cardHeader, preview);
card.onclick = () => this.insertAtCursor(p.content);
card.onmouseenter = () => this.showTooltip(p.content, card);
card.onmouseleave = () => this.hideTooltip();
fragment.appendChild(card);
});
container.appendChild(fragment);
if (filtered.length > 50) {
const moreInfo = document.createElement('div');
moreInfo.style.textAlign = 'center';
moreInfo.style.padding = '8px';
moreInfo.style.color = '#5f6368';
moreInfo.style.fontSize = '12px';
moreInfo.textContent = `Showing 50 of ${filtered.length} prompts. Refine search to see more.`;
container.appendChild(moreInfo);
}
},
showOverlay(contentElement) {
let overlay = document.getElementById('lib-overlay');
if (!overlay) {
overlay = document.createElement('div');
overlay.id = 'lib-overlay';
overlay.className = 'lib-overlay';
const modal = document.getElementById('quicknav-lib-modal');
if (modal) modal.appendChild(overlay);
}
if (!overlay) return;
overlay.replaceChildren();
overlay.appendChild(contentElement);
overlay.classList.add('visible');
},
hideOverlay() {
const overlay = document.getElementById('lib-overlay');
if (overlay) overlay.classList.remove('visible');
},
openEditTagDialog(oldName) {
const container = document.createElement('div');
container.className = 'lib-editor-container';
container.style.justifyContent = 'center';
container.style.alignItems = 'center';
const box = document.createElement('div');
box.className = 'lib-confirm-box';
box.style.maxWidth = '400px';
const header = document.createElement('h3');
header.textContent = 'Edit Tag';
header.style.marginBottom = '16px';
const inputRow = document.createElement('div');
inputRow.style.display = 'flex';
inputRow.style.alignItems = 'center';
inputRow.style.gap = '12px';
inputRow.style.marginBottom = '20px';
inputRow.style.justifyContent = 'center';
let currentColor = TagManager.getColor(oldName);
let isPinned = TagManager.isPinned(oldName);
const colorSwatch = document.createElement('div');
colorSwatch.style.width = '28px';
colorSwatch.style.height = '28px';
colorSwatch.style.borderRadius = '50%';
colorSwatch.style.backgroundColor = currentColor;
colorSwatch.style.border = '1px solid rgba(0,0,0,0.2)';
colorSwatch.style.cursor = 'pointer';
colorSwatch.style.flexShrink = '0';
colorSwatch.title = 'Change Color';
colorSwatch.onclick = (e) => {
const existingPicker = document.getElementById('lib-edit-color-picker');
if (existingPicker) {
existingPicker.remove();
return;
}
const picker = document.createElement('div');
picker.id = 'lib-edit-color-picker';
picker.style.position = 'fixed';
picker.style.zIndex = '100005';
picker.style.padding = '8px';
picker.style.borderRadius = '8px';
picker.style.display = 'grid';
picker.style.gridTemplateColumns = 'repeat(4, 1fr)';
picker.style.gap = '4px';
picker.style.backgroundColor = '#ffffff';
picker.style.border = '1px solid #dadce0';
picker.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
if (document.body.classList.contains('dark-theme')) {
picker.style.backgroundColor = '#202124';
picker.style.borderColor = '#5f6368';
}
TagManager.PALETTE.forEach(color => {
const sw = document.createElement('div');
sw.style.width = '20px';
sw.style.height = '20px';
sw.style.backgroundColor = color;
sw.style.borderRadius = '50%';
sw.style.cursor = 'pointer';
sw.onclick = () => {
currentColor = color;
colorSwatch.style.backgroundColor = color;
picker.remove();
};
picker.appendChild(sw);
});
const rect = colorSwatch.getBoundingClientRect();
picker.style.top = `${rect.bottom + 6}px`;
picker.style.left = `${rect.left}px`;
document.body.appendChild(picker);
const closeHandler = (evt) => {
if (!picker.contains(evt.target) && evt.target !== e.currentTarget) {
picker.remove();
document.removeEventListener('click', closeHandler);
}
};
setTimeout(() => document.addEventListener('click', closeHandler), 0);
};
const pinBtn = document.createElement('button');
pinBtn.style.background = 'transparent';
pinBtn.style.border = 'none';
pinBtn.style.cursor = 'pointer';
pinBtn.style.color = isPinned ? '#1a73e8' : '#5f6368';
pinBtn.title = isPinned ? 'Unpin Tag' : 'Pin Tag to start';
const updatePinIcon = () => {
pinBtn.replaceChildren();
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("viewBox", "0 0 24 24");
svg.setAttribute("width", "24");
svg.setAttribute("height", "24");
svg.setAttribute("fill", "currentColor");
const d = isPinned
? "M16 9V4l1 0c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1l1 0v5c0 1.66-1.34 3-3 3v2h5.97v7l1 1 1-1v-7H19v-2c-1.66 0-3-1.34-3-3z"
: "M16 9V4l1 0c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1l1 0v5c0 1.66-1.34 3-3 3v2h5.97v7l1 1 1-1v-7H19v-2c-1.66 0-3-1.34-3-3zm-1.5 3c0 1.93 1.57 3.5 3.5 3.5H6c1.93 0 3.5-1.57 3.5-3.5V4h5v8z";
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", d);
svg.appendChild(path);
pinBtn.appendChild(svg);
pinBtn.style.color = isPinned ? '#1a73e8' : '#9aa0a6';
pinBtn.title = isPinned ? 'Unpin Tag' : 'Pin Tag to start';
};
updatePinIcon();
pinBtn.onclick = () => {
isPinned = !isPinned;
updatePinIcon();
};
const input = document.createElement('input');
input.type = 'text';
input.className = 'lib-input-full';
input.value = oldName;
input.style.margin = '0';
input.style.textAlign = 'left';
input.style.fontWeight = '500';
input.style.flex = '1';
inputRow.append(colorSwatch, pinBtn, input);
const actions = document.createElement('div');
actions.className = 'lib-confirm-actions';
const cancelBtn = document.createElement('button');
cancelBtn.className = 'lib-btn-secondary';
cancelBtn.textContent = 'Cancel';
cancelBtn.onclick = () => this.hideOverlay();
const saveBtn = document.createElement('button');
saveBtn.className = 'lib-btn-primary';
saveBtn.textContent = 'Save';
const performSave = () => {
const newName = input.value.trim();
const nameChanged = newName && newName !== oldName;
const colorChanged = currentColor !== TagManager.getColor(oldName);
const pinChanged = isPinned !== TagManager.isPinned(oldName);
if (nameChanged) {
this.performRenameTag(oldName, newName);
TagManager.setColor(newName, currentColor);
if (isPinned) TagManager.pinned.add(newName);
else TagManager.pinned.delete(newName);
TagManager.savePinned();
} else {
if (colorChanged) TagManager.setColor(oldName, currentColor);
if (pinChanged) {
if (isPinned) TagManager.pinned.add(oldName);
else TagManager.pinned.delete(oldName);
TagManager.savePinned();
}
}
this.renderTags();
this.renderList();
this.hideOverlay();
};
saveBtn.onclick = performSave;
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') performSave();
if (e.key === 'Escape') this.hideOverlay();
});
actions.append(cancelBtn, saveBtn);
box.append(header, inputRow, actions);
container.appendChild(box);
this.showOverlay(container);
input.focus();
input.select();
},
performRenameTag(oldName, newName) {
let updateCount = 0;
this.data.forEach(p => {
if (p.tags.includes(oldName)) {
p.tags = p.tags.filter(t => t !== oldName);
if (!p.tags.includes(newName)) {
p.tags.push(newName);
}
updateCount++;
}
});
if (updateCount > 0) {
const oldColor = TagManager.colors[oldName];
if (oldColor) {
TagManager.setColor(newName, oldColor);
delete TagManager.colors[oldName];
TagManager.saveColors();
}
if (TagManager.isPinned(oldName)) {
TagManager.pinned.delete(oldName);
TagManager.pinned.add(newName);
TagManager.savePinned();
}
if (this.currentTagFilter === oldName) {
this.currentTagFilter = newName;
StyleManager._write('quicknav_prompt_filter', newName);
}
this.save();
this.renderTags();
this.renderList();
PromptActions.showToast(`Renamed tag in ${updateCount} prompts`);
}
},
openEditor(promptToEdit = null) {
const container = document.createElement('div');
container.className = 'lib-editor-container';
const header = document.createElement('h3');
header.textContent = promptToEdit ? 'Edit Prompt' : 'New Prompt';
const titleRow = document.createElement('div');
titleRow.style.display = 'flex';
titleRow.style.alignItems = 'center';
titleRow.style.gap = '8px';
const inputTitle = document.createElement('input');
inputTitle.type = 'text';
inputTitle.className = 'lib-input-full';
inputTitle.placeholder = 'Prompt Title';
inputTitle.value = promptToEdit ? promptToEdit.title : '';
inputTitle.style.flex = '1';
let isPinned = promptToEdit ? !!promptToEdit.isPinned : false;
const pinToggle = document.createElement('button');
pinToggle.style.background = 'transparent';
pinToggle.style.border = 'none';
pinToggle.style.cursor = 'pointer';
pinToggle.style.padding = '4px';
pinToggle.title = 'Pin to top';
const updatePinIcon = () => {
pinToggle.replaceChildren();
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("viewBox", "0 0 24 24");
svg.setAttribute("width", "24");
svg.setAttribute("height", "24");
svg.setAttribute("fill", "currentColor");
const d = isPinned
? "M16 9V4l1 0c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1l1 0v5c0 1.66-1.34 3-3 3v2h5.97v7l1 1 1-1v-7H19v-2c-1.66 0-3-1.34-3-3z"
: "M16 9V4l1 0c.55 0 1-.45 1-1s-.45-1-1-1H7c-.55 0-1 .45-1 1s.45 1 1 1l1 0v5c0 1.66-1.34 3-3 3v2h5.97v7l1 1 1-1v-7H19v-2c-1.66 0-3-1.34-3-3zm-1.5 3c0 1.93 1.57 3.5 3.5 3.5H6c1.93 0 3.5-1.57 3.5-3.5V4h5v8z";
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", d);
svg.appendChild(path);
pinToggle.appendChild(svg);
pinToggle.style.color = isPinned ? '#1a73e8' : '#9aa0a6';
};
updatePinIcon();
pinToggle.onclick = () => {
isPinned = !isPinned;
updatePinIcon();
};
titleRow.append(inputTitle, pinToggle);
const tagsContainer = document.createElement('div');
tagsContainer.className = 'lib-input-full';
tagsContainer.style.display = 'flex';
tagsContainer.style.flexWrap = 'wrap';
tagsContainer.style.gap = '6px';
tagsContainer.style.alignItems = 'center';
tagsContainer.style.padding = '6px';
tagsContainer.style.minHeight = '42px';
tagsContainer.style.cursor = 'text';
tagsContainer.style.position = 'relative';
const currentTags = promptToEdit ? [...promptToEdit.tags] : [];
const allKnownTags = Array.from(new Set(this.data.flatMap(p => p.tags)));
const renderChips = () => {
const input = tagsContainer.querySelector('input');
if (input) input.remove();
while (tagsContainer.firstChild) {
if (tagsContainer.firstChild.classList && tagsContainer.firstChild.classList.contains('lib-suggestions-menu')) {
break;
}
tagsContainer.removeChild(tagsContainer.firstChild);
}
currentTags.forEach((tag, idx) => {
const chip = document.createElement('span');
chip.textContent = tag;
chip.style.backgroundColor = TagManager.getColor(tag);
chip.style.color = '#202124';
chip.style.padding = '2px 8px';
chip.style.borderRadius = '12px';
chip.style.fontSize = '12px';
chip.style.cursor = 'pointer';
chip.style.display = 'inline-flex';
chip.style.alignItems = 'center';
chip.style.gap = '4px';
chip.style.userSelect = 'none';
chip.title = 'Click to change color';
chip.onclick = (e) => {
e.stopPropagation();
showColorPicker(e, tag);
};
const closeX = document.createElement('span');
closeX.textContent = '×';
closeX.style.fontWeight = 'bold';
closeX.style.fontSize = '14px';
closeX.style.color = 'rgba(0,0,0,0.5)';
closeX.style.cursor = 'pointer';
closeX.onmouseenter = () => closeX.style.color = '#000';
closeX.onmouseleave = () => closeX.style.color = 'rgba(0,0,0,0.5)';
closeX.onclick = (e) => {
e.stopPropagation();
currentTags.splice(idx, 1);
renderChips();
};
chip.appendChild(closeX);
tagsContainer.appendChild(chip);
});
if (input) tagsContainer.appendChild(input);
else {
const newInput = createTagInput();
tagsContainer.appendChild(newInput);
if (promptToEdit || currentTags.length > 0) newInput.focus();
}
};
const createTagInput = () => {
const tagInput = document.createElement('input');
tagInput.style.border = 'none';
tagInput.style.outline = 'none';
tagInput.style.flex = '1';
tagInput.style.minWidth = '60px';
tagInput.style.fontSize = '14px';
tagInput.style.backgroundColor = 'transparent';
tagInput.style.color = 'inherit';
tagInput.placeholder = currentTags.length === 0 ? 'Tags...' : '';
let suggestionsBox = null;
const closeSuggestions = () => {
if (suggestionsBox) {
suggestionsBox.remove();
suggestionsBox = null;
}
};
const updateSuggestions = () => {
const val = tagInput.value.trim().toLowerCase();
const matches = allKnownTags.filter(t =>
(val === '' || t.toLowerCase().includes(val)) && !currentTags.includes(t)
);
matches.sort();
if (!suggestionsBox) {
suggestionsBox = document.createElement('div');
suggestionsBox.className = 'lib-suggestions-menu';
suggestionsBox.style.top = '100%';
suggestionsBox.style.left = '0';
tagsContainer.appendChild(suggestionsBox);
}
suggestionsBox.replaceChildren();
if (matches.length > 0) {
matches.forEach(match => {
const item = document.createElement('div');
item.className = 'lib-suggestion-item';
const colorDot = document.createElement('div');
colorDot.className = 'lib-suggestion-color';
colorDot.style.backgroundColor = TagManager.getColor(match);
const text = document.createElement('span');
text.textContent = match;
item.append(colorDot, text);
item.onmousedown = (e) => {
e.preventDefault();
currentTags.push(match);
tagInput.value = '';
closeSuggestions();
renderChips();
};
suggestionsBox.appendChild(item);
});
}
if (val && !currentTags.includes(tagInput.value.trim())) {
const createRow = document.createElement('div');
createRow.className = 'lib-suggestion-create-row';
const label = document.createElement('div');
label.className = 'lib-suggestion-create-label';
label.textContent = `Create "${tagInput.value.trim()}":`;
const palette = document.createElement('div');
palette.className = 'lib-suggestion-palette';
TagManager.PALETTE.forEach(color => {
const dot = document.createElement('div');
dot.className = 'lib-suggestion-palette-item';
dot.style.backgroundColor = color;
dot.onmousedown = (e) => {
e.preventDefault();
const newTagName = tagInput.value.trim();
TagManager.setColor(newTagName, color);
currentTags.push(newTagName);
tagInput.value = '';
closeSuggestions();
renderChips();
};
palette.appendChild(dot);
});
createRow.append(label, palette);
suggestionsBox.appendChild(createRow);
}
if (matches.length === 0 && !val) {
closeSuggestions();
}
};
tagInput.addEventListener('input', updateSuggestions);
tagInput.addEventListener('focus', updateSuggestions);
tagInput.addEventListener('blur', () => {
setTimeout(closeSuggestions, 200);
});
tagInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ',') {
e.preventDefault();
const val = tagInput.value.trim().replace(',', '');
if (val && !currentTags.includes(val)) {
currentTags.push(val);
tagInput.value = '';
closeSuggestions();
renderChips();
setTimeout(() => {
const newInput = tagsContainer.querySelector('input');
if (newInput) newInput.focus();
}, 0);
}
} else if (e.key === 'Backspace' && !tagInput.value && currentTags.length > 0) {
currentTags.pop();
closeSuggestions();
renderChips();
setTimeout(() => {
const newInput = tagsContainer.querySelector('input');
if (newInput) newInput.focus();
}, 0);
}
});
return tagInput;
};
tagsContainer.onclick = (e) => {
if (e.target === tagsContainer) {
const input = tagsContainer.querySelector('input');
if (input) input.focus();
}
};
const showColorPicker = (e, tagName) => {
const existingPicker = document.getElementById('lib-color-picker');
if (existingPicker) existingPicker.remove();
const picker = document.createElement('div');
picker.id = 'lib-color-picker';
picker.style.position = 'fixed';
picker.style.zIndex = '100002';
picker.style.padding = '8px';
picker.style.borderRadius = '8px';
picker.style.display = 'grid';
picker.style.gridTemplateColumns = 'repeat(4, 1fr)';
picker.style.gap = '4px';
TagManager.PALETTE.forEach(color => {
const swatch = document.createElement('div');
swatch.style.width = '20px';
sw.style.height = '20px';
sw.style.backgroundColor = color;
sw.style.borderRadius = '50%';
sw.style.cursor = 'pointer';
sw.style.border = '1px solid rgba(0,0,0,0.1)';
sw.onclick = () => {
TagManager.setColor(tagName, color);
picker.remove();
renderChips();
this.renderTags();
};
picker.appendChild(swatch);
});
const rect = e.currentTarget.getBoundingClientRect();
picker.style.top = `${rect.bottom + 4}px`;
picker.style.left = `${rect.left}px`;
document.body.appendChild(picker);
const closeHandler = (evt) => {
if (!picker.contains(evt.target) && evt.target !== e.currentTarget) {
picker.remove();
document.removeEventListener('click', closeHandler);
}
};
setTimeout(() => document.addEventListener('click', closeHandler), 0);
};
tagsContainer.appendChild(createTagInput());
renderChips();
const contentToolbar = document.createElement('div');
contentToolbar.className = 'lib-editor-toolbar';
const pasteBtn = document.createElement('button');
pasteBtn.textContent = 'Paste Clipboard';
pasteBtn.className = 'lib-btn-text';
pasteBtn.onclick = async () => {
try {
const text = await navigator.clipboard.readText();
inputContent.value += text;
} catch(e) { }
};
const fromChatBtn = document.createElement('button');
fromChatBtn.textContent = 'From Chat Input';
fromChatBtn.className = 'lib-btn-text';
fromChatBtn.onclick = () => {
const ta = PromptActions.getTextarea();
if (ta) inputContent.value += ta.value;
};
contentToolbar.append(pasteBtn, fromChatBtn);
const inputContent = document.createElement('textarea');
inputContent.className = 'lib-textarea-full';
inputContent.placeholder = 'Prompt Content...';
inputContent.value = promptToEdit ? promptToEdit.content : '';
const footer = document.createElement('div');
footer.className = 'lib-overlay-footer';
const cancelBtn = document.createElement('button');
cancelBtn.className = 'lib-btn-secondary';
cancelBtn.textContent = 'Cancel';
cancelBtn.onclick = () => this.hideOverlay();
const saveBtn = document.createElement('button');
saveBtn.className = 'lib-btn-primary';
saveBtn.textContent = 'Save';
saveBtn.onclick = () => {
if (!inputContent.value.trim()) return;
if (promptToEdit) {
const idx = this.data.findIndex(p => p.id === promptToEdit.id);
if (idx !== -1) {
this.data[idx] = {
...this.data[idx],
title: inputTitle.value.trim() || 'Untitled',
tags: currentTags,
content: inputContent.value,
isPinned: isPinned
};
if (currentTags.length > 0) {
this.currentTagFilter = currentTags[0];
StyleManager._write('quicknav_prompt_filter', this.currentTagFilter);
}
this.save();
}
} else {
const newPrompt = {
id: Date.now().toString(36) + Math.random().toString(36).substr(2),
title: inputTitle.value.trim() || 'Untitled',
tags: currentTags,
content: inputContent.value,
isPinned: isPinned
};
this.data.unshift(newPrompt);
if (currentTags.length > 0) {
this.currentTagFilter = currentTags[0];
StyleManager._write('quicknav_prompt_filter', this.currentTagFilter);
} else {
this.currentTagFilter = 'All';
StyleManager._write('quicknav_prompt_filter', 'All');
}
this.save();
}
this.renderList();
this.renderTags();
this.hideOverlay();
};
footer.append(cancelBtn, saveBtn);
container.append(header, titleRow, tagsContainer, contentToolbar, inputContent, footer);
this.showOverlay(container);
inputTitle.focus();
},
confirmDelete(id) {
const container = document.createElement('div');
container.className = 'lib-confirm-wrap';
const box = document.createElement('div');
box.className = 'lib-confirm-box';
const header = document.createElement('h3');
header.textContent = 'Delete Prompt?';
const subtext = document.createElement('p');
subtext.textContent = 'This action cannot be undone.';
const actions = document.createElement('div');
actions.className = 'lib-confirm-actions';
const cancelBtn = document.createElement('button');
cancelBtn.className = 'lib-btn-secondary';
cancelBtn.textContent = 'Cancel';
cancelBtn.onclick = () => this.hideOverlay();
const delBtn = document.createElement('button');
delBtn.className = 'lib-btn-danger';
delBtn.textContent = 'Delete';
delBtn.onclick = () => {
this.deletePrompt(id);
this.renderList();
this.renderTags();
this.hideOverlay();
};
actions.append(cancelBtn, delBtn);
box.append(header, subtext, actions);
container.appendChild(box);
this.showOverlay(container);
},
openImportExport() {
const container = document.createElement('div');
container.className = 'lib-editor-container';
const header = document.createElement('h3');
header.textContent = 'Import / Export';
const fileSection = document.createElement('div');
fileSection.style.display = 'flex';
fileSection.style.gap = '12px';
fileSection.style.marginBottom = '12px';
fileSection.style.alignItems = 'center';
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.json';
fileInput.style.display = 'none';
fileInput.onchange = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (evt) => {
try {
this.processImport(JSON.parse(evt.target.result));
fileInput.value = '';
} catch(err) { alert('Invalid JSON'); }
};
reader.readAsText(file);
};
const exportBtn = document.createElement('button');
exportBtn.className = 'lib-btn-primary';
exportBtn.style.flex = '1';
exportBtn.style.justifyContent = 'center';
exportBtn.textContent = 'Export to File';
exportBtn.onclick = () => {
const exportData = {
version: 2,
prompts: this.data,
tagColors: TagManager.getExportData()
};
const blob = new Blob([JSON.stringify(exportData, null, 2)], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `quicknav_prompts_v2_${Date.now()}.json`;
a.click();
};
const importFileBtn = document.createElement('button');
importFileBtn.className = 'lib-btn-secondary';
importFileBtn.style.flex = '1';
importFileBtn.style.justifyContent = 'center';
importFileBtn.textContent = 'Import from File';
importFileBtn.onclick = () => fileInput.click();
fileSection.append(exportBtn, importFileBtn, fileInput);
const separator = document.createElement('div');
separator.style.height = '1px';
separator.style.backgroundColor = '#e0e0e0';
separator.style.margin = '0 0 16px 0';
separator.style.opacity = '0.6';
const textLabel = document.createElement('div');
textLabel.className = 'lib-io-desc';
textLabel.textContent = 'Or paste JSON content directly:';
textLabel.style.marginBottom = '6px';
textLabel.style.fontWeight = '500';
const jsonInput = document.createElement('textarea');
jsonInput.className = 'lib-textarea-full';
jsonInput.style.flex = '1';
jsonInput.style.minHeight = '150px';
jsonInput.placeholder = '{ "version": 2, "prompts": [...], "tagColors": {...} }';
const importTextBtn = document.createElement('button');
importTextBtn.className = 'lib-btn-primary';
importTextBtn.style.width = '100%';
importTextBtn.style.marginTop = '12px';
importTextBtn.style.justifyContent = 'center';
importTextBtn.textContent = 'Import from Text';
importTextBtn.onclick = () => {
try {
const val = jsonInput.value.trim();
if (!val) return;
this.processImport(JSON.parse(val));
} catch(err) { alert('Invalid JSON'); }
};
const clearZoneWrapper = document.createElement('div');
clearZoneWrapper.style.marginTop = 'auto';
clearZoneWrapper.style.paddingTop = '16px';
const dangerSep = document.createElement('div');
dangerSep.style.height = '1px';
dangerSep.style.backgroundColor = '#e0e0e0';
dangerSep.style.margin = '0 0 12px 0';
dangerSep.style.opacity = '0.6';
const dangerZone = document.createElement('div');
dangerZone.style.display = 'flex';
dangerZone.style.justifyContent = 'space-between';
dangerZone.style.alignItems = 'center';
let deleteConfirmLevel = 0;
const renderClearZone = () => {
dangerZone.replaceChildren();
if (deleteConfirmLevel === 0) {
const clearBtn = document.createElement('button');
clearBtn.className = 'lib-btn-danger';
clearBtn.textContent = 'Clear Library';
clearBtn.style.fontSize = '12px';
clearBtn.onclick = () => {
deleteConfirmLevel = 1;
renderClearZone();
};
const spacer = document.createElement('div');
spacer.style.flex = '1';
const closeBtn = document.createElement('button');
closeBtn.className = 'lib-btn-secondary';
closeBtn.textContent = 'Close';
closeBtn.onclick = () => this.hideOverlay();
dangerZone.append(clearBtn, spacer, closeBtn);
} else {
const label = document.createElement('span');
label.textContent = 'Delete all prompts?';
label.style.fontSize = '13px';
label.style.fontWeight = '500';
label.style.color = '#d93025';
const btnGroup = document.createElement('div');
btnGroup.style.display = 'flex';
btnGroup.style.gap = '8px';
const yesBtn = document.createElement('button');
yesBtn.className = 'lib-btn-danger';
yesBtn.textContent = 'Confirm';
yesBtn.style.fontSize = '12px';
yesBtn.onclick = () => {
this.data = [];
this.save();
this.renderList();
this.renderTags();
PromptActions.showToast("Library cleared");
deleteConfirmLevel = 0;
renderClearZone();
};
const noBtn = document.createElement('button');
noBtn.className = 'lib-btn-secondary';
noBtn.textContent = 'Cancel';
noBtn.style.fontSize = '12px';
noBtn.onclick = () => {
deleteConfirmLevel = 0;
renderClearZone();
};
btnGroup.append(yesBtn, noBtn);
dangerZone.append(label, btnGroup);
}
};
renderClearZone();
clearZoneWrapper.append(dangerSep, dangerZone);
container.append(header, fileSection, separator, textLabel, jsonInput, importTextBtn, clearZoneWrapper);
this.showOverlay(container);
if (!this._boundKeyHandler) {
this._boundKeyHandler = this.handleKeyDown.bind(this);
}
document.addEventListener('keydown', this._boundKeyHandler);
},
processImport(imported) {
let prompts = [];
let colors = null;
if (Array.isArray(imported)) {
prompts = imported;
} else if (imported && imported.prompts) {
prompts = imported.prompts;
colors = imported.tagColors;
} else {
alert('Invalid Data Format');
return;
}
if (colors) {
TagManager.mergeImportData(colors);
}
if (Array.isArray(prompts)) {
let added = 0;
const ids = new Set(this.data.map(p => p.id));
[...prompts].reverse().forEach(p => {
if (p.content) {
if (!p.id || ids.has(p.id)) {
p.id = Date.now().toString(36) + Math.random().toString(36).substr(2);
}
p.title = p.title || 'Untitled';
p.tags = Array.isArray(p.tags) ? p.tags : [];
p.isPinned = !!p.isPinned;
this.data.unshift(p);
added++;
}
});
this.save();
this.renderTags();
this.renderList();
alert(`Imported ${added} prompts.`);
this.hideOverlay();
}
},
open() {
this.load();
this.createModalElements();
this.renderTags();
this.renderList();
requestAnimationFrame(() => {
const backdrop = document.getElementById('quicknav-lib-backdrop');
if (backdrop) {
requestAnimationFrame(() => {
backdrop.classList.add('visible');
});
const search = backdrop.querySelector('.lib-search-input');
if (search) search.focus();
}
});
if (!this._boundKeyHandler) {
this._boundKeyHandler = this.handleKeyDown.bind(this);
}
document.addEventListener('keydown', this._boundKeyHandler);
},
toggle() {
const backdrop = document.getElementById('quicknav-lib-backdrop');
if (backdrop && backdrop.classList.contains('visible')) {
this.closeModal();
} else {
this.open();
}
},
closeModal() {
const backdrop = document.getElementById('quicknav-lib-backdrop');
if (backdrop) backdrop.classList.remove('visible');
this.hideOverlay();
this.hideTooltip(true);
if (this._boundKeyHandler) {
document.removeEventListener('keydown', this._boundKeyHandler);
}
},
showTooltip(text, sourceElement = null) {
clearTimeout(this.tooltipTimer);
if (this.activeCard && this.activeCard !== sourceElement) {
this.activeCard.classList.remove('active-tooltip-source');
}
if (sourceElement) {
sourceElement.classList.add('active-tooltip-source');
this.activeCard = sourceElement;
}
const tt = UIManager.customTooltip;
const modal = document.getElementById('quicknav-lib-modal');
if (tt && modal) {
const MAX_TOOLTIP_CHARS = 10000;
let displayText = text;
if (displayText.length > MAX_TOOLTIP_CHARS) {
displayText = displayText.substring(0, MAX_TOOLTIP_CHARS) + '\n\n[...Truncated for preview...]';
}
tt.textContent = displayText;
tt.classList.add('lib-fixed-tooltip');
tt.style.opacity = '1';
tt.style.pointerEvents = 'auto';
tt.onmouseenter = () => clearTimeout(this.tooltipTimer);
tt.onmouseleave = () => this.hideTooltip();
const modalRect = modal.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const SCREEN_PAD = 12;
const ELEMENT_GAP = 4;
const leftPos = modalRect.right + ELEMENT_GAP;
const availableWidth = viewportWidth - leftPos - SCREEN_PAD;
const availableHeight = viewportHeight - modalRect.top - SCREEN_PAD;
tt.style.left = `${leftPos}px`;
tt.style.top = `${modalRect.top}px`;
tt.style.width = `${Math.max(200, availableWidth)}px`;
tt.style.maxHeight = `${availableHeight}px`;
tt.style.overflowY = 'auto';
}
},
hideTooltip(immediate = false) {
const performHide = () => {
const tt = UIManager.customTooltip;
if (tt) {
tt.style.opacity = '0';
tt.classList.remove('lib-fixed-tooltip');
tt.style.pointerEvents = '';
tt.style.width = '';
tt.style.maxHeight = '';
tt.style.overflowY = '';
tt.onmouseenter = null;
tt.onmouseleave = null;
}
if (this.activeCard) {
this.activeCard.classList.remove('active-tooltip-source');
this.activeCard = null;
}
};
if (immediate) {
clearTimeout(this.tooltipTimer);
performHide();
} else {
this.tooltipTimer = setTimeout(performHide, 300);
}
}
};
// Manages favorites storage and UI
const FavoritesManager = {
STORAGE_KEY: 'quicknav_favorites',
menuId: 'quicknav-favorites-menu',
deletingId: null,
ioPanelVisible: false,
deleteConfirmLevel: 0,
filterText: '',
focusedIndex: -1,
boundHandler: null,
getFavorites() {
try {
const raw = localStorage.getItem(this.STORAGE_KEY);
return raw ? JSON.parse(raw) : [];
} catch (e) {
return [];
}
},
saveFavorites(favs) {
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(favs));
this.updateButtonState();
},
isCurrentFavorite() {
const currentUrl = window.location.href;
const favs = this.getFavorites();
return favs.some(f => f.url === currentUrl);
},
// Fetches the current chat title adapting to the latest DOM structure
getCurrentTitle() {
const titleEl = document.querySelector('ms-playground-toolbar h1.mode-title, ms-toolbar h1.mode-title');
return titleEl ? titleEl.textContent.trim() : document.title;
},
addCurrent() {
const favs = this.getFavorites();
const url = window.location.href;
if (favs.some(f => f.url === url)) return;
favs.unshift({
id: Date.now().toString(36),
title: this.getCurrentTitle(),
url: url
});
this.saveFavorites(favs);
this.filterText = '';
this.renderMenu();
},
startDelete(id) {
this.deletingId = id;
this.renderMenu();
},
cancelDelete() {
this.deletingId = null;
this.renderMenu();
},
remove(id) {
let favs = this.getFavorites();
favs = favs.filter(f => f.id !== id);
this.saveFavorites(favs);
this.deletingId = null;
this.renderMenu();
},
clearAll() {
this.saveFavorites([]);
this.deleteConfirmLevel = 0;
this.ioPanelVisible = false;
this.renderMenu();
PromptActions.showToast("All favorites cleared");
},
update(id, newTitle, newUrl) {
const favs = this.getFavorites();
const idx = favs.findIndex(f => f.id === id);
if (idx !== -1) {
favs[idx].title = newTitle;
favs[idx].url = newUrl;
this.saveFavorites(favs);
this.renderMenu();
}
},
exportFavorites() {
const favs = this.getFavorites();
if (favs.length === 0) {
PromptActions.showToast("No favorites to export");
return;
}
const dataStr = JSON.stringify(favs, null, 2);
const blob = new Blob([dataStr], {type: "application/json"});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `quicknav_favorites_${Date.now()}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
PromptActions.showToast("Favorites exported");
},
importFavorites() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.style.display = 'none';
input.onchange = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (evt) => {
try {
const imported = JSON.parse(evt.target.result);
if (Array.isArray(imported)) {
const current = this.getFavorites();
const currentUrls = new Set(current.map(f => f.url));
let addedCount = 0;
[...imported].reverse().forEach(item => {
if (item.url && item.title && !currentUrls.has(item.url)) {
if (!item.id) item.id = Date.now().toString(36) + Math.random();
current.unshift(item);
currentUrls.add(item.url);
addedCount++;
}
});
this.saveFavorites(current);
this.filterText = '';
this.renderMenu();
PromptActions.showToast(`Imported ${addedCount} favorites`);
} else {
alert("Invalid JSON format: Expected an array.");
}
} catch (err) {
alert("Error parsing JSON file.");
}
};
reader.readAsText(file);
};
document.body.appendChild(input);
input.click();
setTimeout(() => input.remove(), 1000);
},
updateButtonState() {
const btn = document.getElementById('quicknav-fav-trigger');
if (!btn) return;
const isFav = this.isCurrentFavorite();
const svg = btn.querySelector('svg');
if (svg) svg.remove();
const filledPath = 'M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z';
const outlinePath = 'M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.01 4.38.38-3.32 2.88 1 4.28L12 15.4z';
const newSvg = StyleManager.createSvgIcon(isFav ? filledPath : outlinePath);
newSvg.setAttribute('width', '24');
newSvg.setAttribute('height', '24');
newSvg.setAttribute('viewBox', '0 0 24 24');
newSvg.style.color = 'var(--ms-primary, #8ab4f8)';
if (isFav) {
newSvg.setAttribute('fill', 'currentColor');
} else {
newSvg.setAttribute('fill', 'currentColor');
}
btn.appendChild(newSvg);
},
applyFilter() {
const menu = document.getElementById(this.menuId);
if (!menu) return;
const term = this.filterText.toLowerCase().trim();
const items = menu.querySelectorAll('.quicknav-fav-item');
items.forEach(item => {
const titleEl = item.querySelector('.fav-title');
const urlEl = item.querySelector('.fav-url');
if (!titleEl || !urlEl) return;
const title = titleEl.textContent.toLowerCase();
const url = urlEl.textContent.toLowerCase();
if (!term || title.includes(term) || url.includes(term)) {
item.style.display = '';
item.dataset.visible = 'true';
} else {
item.style.display = 'none';
item.dataset.visible = 'false';
}
});
const clearBtn = menu.querySelector('.fav-filter-clear');
if (clearBtn) {
clearBtn.style.display = term ? 'flex' : 'none';
}
this.focusedIndex = -1;
this.updateVisualFocus();
},
onOpen() {
this.renderMenu();
this.focusedIndex = -1;
const input = document.getElementById('quicknav-fav-filter-input');
if (input) {
setTimeout(() => input.focus(), 50);
}
if (!this.boundHandler) {
this.boundHandler = this.handleKeyDown.bind(this);
}
document.addEventListener('keydown', this.boundHandler);
},
onClose() {
if (this.boundHandler) {
document.removeEventListener('keydown', this.boundHandler);
}
this.focusedIndex = -1;
this.deleteConfirmLevel = 0;
},
handleKeyDown(e) {
const menu = document.getElementById(this.menuId);
if (!menu || !menu.classList.contains('visible')) return;
const visibleItems = Array.from(menu.querySelectorAll('.quicknav-fav-item[data-visible="true"]'));
const input = document.getElementById('quicknav-fav-filter-input');
if (e.key === 'ArrowDown') {
e.preventDefault();
if (visibleItems.length === 0) return;
if (this.focusedIndex === -1) {
this.focusedIndex = 0;
if (input) input.blur();
} else {
this.focusedIndex = Math.min(this.focusedIndex + 1, visibleItems.length - 1);
}
this.updateVisualFocus(visibleItems);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (this.focusedIndex > 0) {
this.focusedIndex--;
this.updateVisualFocus(visibleItems);
} else if (this.focusedIndex === 0) {
this.focusedIndex = -1;
this.updateVisualFocus(visibleItems);
if (input) {
input.focus();
const len = input.value.length;
input.setSelectionRange(len, len);
}
}
} else if (e.key === 'Enter') {
if (this.focusedIndex > -1 && visibleItems[this.focusedIndex]) {
e.preventDefault();
const link = visibleItems[this.focusedIndex].querySelector('.fav-text-group');
if (link) link.click();
}
}
},
updateVisualFocus(items) {
if (!items) {
const menu = document.getElementById(this.menuId);
if (menu) items = Array.from(menu.querySelectorAll('.quicknav-fav-item[data-visible="true"]'));
else return;
}
items.forEach((item, idx) => {
if (idx === this.focusedIndex) {
item.classList.add('key-focused');
item.scrollIntoView({ block: 'nearest' });
} else {
item.classList.remove('key-focused');
}
});
},
renderMenu() {
const menu = document.getElementById(this.menuId);
if (!menu) return;
menu.replaceChildren();
const headerRow = document.createElement('div');
headerRow.className = 'quicknav-dropdown-item fav-action-row';
headerRow.style.display = 'flex';
headerRow.style.alignItems = 'center';
headerRow.style.justifyContent = 'space-between';
headerRow.style.borderBottom = '1px solid var(--ms-outline, #dadce0)';
headerRow.style.paddingBottom = '8px';
headerRow.style.marginBottom = '4px';
headerRow.style.cursor = 'default';
headerRow.style.gap = '8px';
headerRow.style.backgroundColor = 'transparent';
const addBtn = document.createElement('button');
addBtn.style.display = 'flex';
addBtn.style.alignItems = 'center';
addBtn.style.gap = '6px';
addBtn.style.cursor = 'pointer';
addBtn.style.border = 'none';
addBtn.style.backgroundColor = 'var(--ms-primary, #1a73e8)';
addBtn.style.color = '#ffffff';
addBtn.style.padding = '4px 12px';
addBtn.style.borderRadius = '16px';
addBtn.style.fontSize = '12px';
addBtn.style.fontWeight = '500';
addBtn.style.transition = 'background-color 0.2s';
addBtn.title = 'Add current page to favorites';
addBtn.onmouseenter = () => { addBtn.style.backgroundColor = '#1557b0'; };
addBtn.onmouseleave = () => { addBtn.style.backgroundColor = 'var(--ms-primary, #1a73e8)'; };
const addIcon = StyleManager.createSvgIcon('M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z');
addIcon.style.width = '16px';
addIcon.style.height = '16px';
const addText = document.createElement('span');
addText.textContent = 'Add';
addBtn.append(addIcon, addText);
addBtn.onclick = (e) => {
e.stopPropagation();
this.addCurrent();
};
const filterWrapper = document.createElement('div');
filterWrapper.style.position = 'relative';
filterWrapper.style.display = 'flex';
filterWrapper.style.alignItems = 'center';
filterWrapper.style.flex = '1';
filterWrapper.style.minWidth = '0';
const filterInput = document.createElement('input');
filterInput.id = 'quicknav-fav-filter-input';
filterInput.type = 'text';
filterInput.placeholder = 'Filter...';
filterInput.value = this.filterText;
filterInput.style.width = '100%';
filterInput.style.padding = '4px 24px 4px 8px';
filterInput.style.borderRadius = '4px';
filterInput.style.border = '1px solid var(--ms-outline, #dadce0)';
filterInput.style.fontSize = '12px';
filterInput.style.outline = 'none';
filterInput.style.backgroundColor = 'transparent';
filterInput.style.color = 'inherit';
filterInput.style.transition = 'border-color 0.2s';
filterInput.onfocus = () => {
filterInput.style.borderColor = 'var(--ms-primary, #8ab4f8)';
this.focusedIndex = -1;
this.updateVisualFocus();
};
filterInput.onblur = () => { filterInput.style.borderColor = 'var(--ms-outline, #dadce0)'; };
filterInput.onclick = (e) => e.stopPropagation();
filterInput.oninput = (e) => {
this.filterText = e.target.value;
this.applyFilter();
};
const clearFilterBtn = document.createElement('div');
clearFilterBtn.className = 'fav-filter-clear';
clearFilterBtn.textContent = '×';
clearFilterBtn.style.position = 'absolute';
clearFilterBtn.style.right = '4px';
clearFilterBtn.style.height = '16px';
clearFilterBtn.style.width = '16px';
clearFilterBtn.style.display = this.filterText ? 'flex' : 'none';
clearFilterBtn.style.alignItems = 'center';
clearFilterBtn.style.justifyContent = 'center';
clearFilterBtn.style.fontSize = '14px';
clearFilterBtn.style.fontWeight = 'bold';
clearFilterBtn.style.color = 'var(--ms-on-surface-variant, #9aa0a6)';
clearFilterBtn.style.cursor = 'pointer';
clearFilterBtn.onclick = (e) => {
e.stopPropagation();
this.filterText = '';
filterInput.value = '';
this.applyFilter();
filterInput.focus();
};
filterWrapper.append(filterInput, clearFilterBtn);
const ioToggle = document.createElement('button');
ioToggle.className = 'lib-icon-btn';
ioToggle.title = 'Import / Export Favorites';
ioToggle.style.color = this.ioPanelVisible ? 'var(--ms-primary, #8ab4f8)' : 'inherit';
ioToggle.appendChild(StyleManager.createSvgIcon('M9 3L5 6.99h3V14h2V6.99h3L9 3zm7 14.01V10h-2v7.01h-3L15 21l4-3.99h-3z'));
ioToggle.onclick = (e) => {
e.stopPropagation();
this.ioPanelVisible = !this.ioPanelVisible;
this.deleteConfirmLevel = 0;
this.renderMenu();
};
headerRow.append(addBtn, filterWrapper, ioToggle);
menu.appendChild(headerRow);
if (this.ioPanelVisible) {
const ioPanel = document.createElement('div');
ioPanel.style.display = 'flex';
ioPanel.style.gap = '8px';
ioPanel.style.padding = '8px 4px';
ioPanel.style.backgroundColor = 'rgba(138, 180, 248, 0.05)';
ioPanel.style.marginBottom = '8px';
ioPanel.style.borderRadius = '4px';
ioPanel.style.alignItems = 'center';
if (this.deleteConfirmLevel > 0) {
const confirmLabel = document.createElement('span');
confirmLabel.textContent = this.deleteConfirmLevel === 1 ? 'Clear All?' : 'Irreversible!';
confirmLabel.style.fontSize = '12px';
confirmLabel.style.fontWeight = '600';
confirmLabel.style.color = '#d93025';
confirmLabel.style.marginLeft = '4px';
const yesBtn = document.createElement('button');
yesBtn.className = 'lib-btn-danger';
yesBtn.textContent = this.deleteConfirmLevel === 1 ? 'Yes' : 'Confirm';
yesBtn.style.flex = '1';
yesBtn.style.justifyContent = 'center';
yesBtn.style.fontSize = '12px';
yesBtn.style.padding = '4px 8px';
yesBtn.onclick = (e) => {
e.stopPropagation();
if (this.deleteConfirmLevel === 1) {
this.deleteConfirmLevel = 2;
this.renderMenu();
} else {
this.clearAll();
}
};
const noBtn = document.createElement('button');
noBtn.className = 'lib-btn-secondary';
noBtn.textContent = 'No';
noBtn.style.flex = '1';
noBtn.style.justifyContent = 'center';
noBtn.style.fontSize = '12px';
noBtn.style.padding = '4px 8px';
noBtn.onclick = (e) => {
e.stopPropagation();
this.deleteConfirmLevel = 0;
this.renderMenu();
};
ioPanel.append(confirmLabel, yesBtn, noBtn);
} else {
const clearBtn = document.createElement('button');
clearBtn.className = 'lib-icon-btn delete';
clearBtn.title = 'Clear All Favorites';
clearBtn.style.height = '28px';
clearBtn.style.width = '28px';
clearBtn.style.display = 'flex';
clearBtn.style.alignItems = 'center';
clearBtn.style.justifyContent = 'center';
clearBtn.style.marginRight = '4px';
clearBtn.appendChild(StyleManager.createSvgIcon('M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z'));
clearBtn.onclick = (e) => {
e.stopPropagation();
this.deleteConfirmLevel = 1;
this.renderMenu();
};
const exportBtn = document.createElement('button');
exportBtn.className = 'lib-btn-primary';
exportBtn.textContent = 'Export JSON';
exportBtn.style.flex = '1';
exportBtn.style.justifyContent = 'center';
exportBtn.style.fontSize = '12px';
exportBtn.onclick = (e) => { e.stopPropagation(); this.exportFavorites(); };
const importBtn = document.createElement('button');
importBtn.className = 'lib-btn-secondary';
importBtn.textContent = 'Import JSON';
importBtn.style.flex = '1';
importBtn.style.justifyContent = 'center';
importBtn.style.fontSize = '12px';
importBtn.onclick = (e) => { e.stopPropagation(); this.importFavorites(); };
ioPanel.append(clearBtn, exportBtn, importBtn);
}
menu.appendChild(ioPanel);
}
const favs = this.getFavorites();
if (favs.length === 0) return;
favs.forEach(fav => {
const item = document.createElement('div');
item.className = 'quicknav-fav-item';
item.dataset.visible = 'true';
if (this.deletingId === fav.id) {
item.className += ' fav-confirm-mode';
const confirmRow = document.createElement('div');
confirmRow.className = 'fav-confirm-row';
const label = document.createElement('span');
label.textContent = 'Delete?';
label.className = 'fav-confirm-label';
const actions = document.createElement('div');
actions.style.display = 'flex';
actions.style.gap = '8px';
const yesBtn = document.createElement('button');
yesBtn.className = 'lib-icon-btn delete';
yesBtn.title = 'Confirm Delete';
yesBtn.appendChild(StyleManager.createSvgIcon('M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'));
yesBtn.onclick = (e) => {
e.stopPropagation();
this.remove(fav.id);
};
const noBtn = document.createElement('button');
noBtn.className = 'lib-icon-btn';
noBtn.title = 'Cancel';
noBtn.appendChild(StyleManager.createSvgIcon('M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 17.59 13.41 12z'));
noBtn.onclick = (e) => {
e.stopPropagation();
this.cancelDelete();
};
actions.append(yesBtn, noBtn);
confirmRow.append(label, actions);
item.appendChild(confirmRow);
} else {
const isActive = window.location.href === fav.url;
if (isActive) item.classList.add('fav-active-item');
item.addEventListener('mousedown', () => {
const allItems = menu.querySelectorAll('.quicknav-fav-item');
allItems.forEach(i => i.classList.remove('fav-active-item'));
item.classList.add('fav-active-item');
});
const displayContainer = document.createElement('div');
displayContainer.className = 'fav-display';
const textGroup = document.createElement('a');
textGroup.className = 'fav-text-group';
textGroup.href = fav.url;
textGroup.target = '_blank';
textGroup.rel = 'noopener noreferrer';
const titleSpan = document.createElement('div');
titleSpan.className = 'fav-title';
titleSpan.textContent = fav.title;
const urlSpan = document.createElement('div');
urlSpan.className = 'fav-url';
urlSpan.textContent = fav.url;
textGroup.append(titleSpan, urlSpan);
const actions = document.createElement('div');
actions.className = 'fav-actions';
const editBtn = document.createElement('button');
editBtn.className = 'lib-icon-btn';
editBtn.appendChild(StyleManager.createSvgIcon('M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z'));
const delBtn = document.createElement('button');
delBtn.className = 'lib-icon-btn delete';
delBtn.appendChild(StyleManager.createSvgIcon('M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z'));
delBtn.onclick = (e) => {
e.stopPropagation();
this.startDelete(fav.id);
};
actions.append(editBtn, delBtn);
displayContainer.append(textGroup, actions);
const editForm = document.createElement('div');
editForm.className = 'fav-edit-form';
editForm.style.display = 'none';
const inputTitle = document.createElement('input');
inputTitle.type = 'text';
inputTitle.className = 'lib-input-full';
inputTitle.value = fav.title;
inputTitle.placeholder = 'Name';
inputTitle.style.marginBottom = '4px';
const inputUrl = document.createElement('input');
inputUrl.type = 'text';
inputUrl.className = 'lib-input-full';
inputUrl.value = fav.url;
inputUrl.placeholder = 'URL';
inputUrl.style.marginBottom = '4px';
const btnRow = document.createElement('div');
btnRow.style.display = 'flex';
btnRow.style.justifyContent = 'flex-end';
btnRow.style.gap = '8px';
const saveBtn = document.createElement('button');
saveBtn.className = 'lib-btn-primary';
saveBtn.textContent = 'Save';
saveBtn.style.padding = '4px 12px';
saveBtn.onclick = () => this.update(fav.id, inputTitle.value, inputUrl.value);
const cancelBtn = document.createElement('button');
cancelBtn.className = 'lib-btn-secondary';
cancelBtn.textContent = 'Cancel';
cancelBtn.style.padding = '4px 12px';
cancelBtn.onclick = () => {
editForm.style.display = 'none';
displayContainer.style.display = 'flex';
};
btnRow.append(cancelBtn, saveBtn);
editForm.append(inputTitle, inputUrl, btnRow);
editBtn.onclick = (e) => {
e.stopPropagation();
displayContainer.style.display = 'none';
editForm.style.display = 'flex';
editForm.style.flexDirection = 'column';
};
item.append(displayContainer, editForm);
}
menu.appendChild(item);
});
if (this.filterText) {
this.applyFilter();
}
}
};
const ThemeManager = {
observer: null,
init() {
this.updateState();
if (this.observer) this.observer.disconnect();
this.observer = new MutationObserver(() => this.updateState());
this.observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });
},
toggle() {
const body = document.body;
if (body.classList.contains('dark-theme')) {
body.classList.remove('dark-theme');
body.classList.add('light-theme');
} else {
body.classList.remove('light-theme');
body.classList.add('dark-theme');
}
this.updateState();
},
updateState() {
const btn = document.getElementById('quicknav-theme-trigger');
if (!btn) return;
const isDark = document.body.classList.contains('dark-theme');
btn.title = isDark ? 'Switch to Light Mode (Session only)' : 'Switch to Dark Mode (Session only)';
let iconSpan = btn.querySelector('.material-symbols-outlined');
if (!iconSpan) {
btn.replaceChildren();
iconSpan = document.createElement('span');
iconSpan.className = 'material-symbols-outlined notranslate';
iconSpan.setAttribute('aria-hidden', 'true');
iconSpan.style.fontSize = '24px';
btn.appendChild(iconSpan);
}
iconSpan.textContent = isDark ? 'light_mode' : 'nightlight';
if (isDark) {
iconSpan.classList.add('quicknav-icon-sun');
iconSpan.classList.remove('quicknav-icon-moon');
} else {
iconSpan.classList.add('quicknav-icon-moon');
iconSpan.classList.remove('quicknav-icon-sun');
}
},
};
// Manages primary user interface elements, fast layout rendering, and DOM text extractions
const UIManager = {
customTooltip: null,
tooltipTimeout: null,
hideTooltipTimer: null,
badgeFadeTimeout: null,
lastScrollTime: 0,
lastScrollTop: 0,
isHoveringOnNav: false,
isThrottled: false,
footerElement: null,
toolbarElement: null,
navContainerElement: null,
listenedTurnElement: null,
_handleTurnMouseMove: null,
_handleTurnMouseLeave: null,
boundCloseMenu: null,
boundResizeHandler: null,
activeObservers: new Set(),
create(targetNode) {
if (this.navContainerElement && document.body.contains(this.navContainerElement)) return;
StyleManager.init();
this.injectStyles();
this.createGlobalElements();
this.createAndInjectUI();
this.cacheStaticElements();
this._handleTurnMouseMove = this._handleTurnMouseMove.bind(this);
this._handleTurnMouseLeave = this._handleTurnMouseLeave.bind(this);
this.boundCloseMenu = this.closeSettingsMenu.bind(this);
this.boundResizeHandler = this.closeSettingsMenu.bind(this);
document.addEventListener('click', this.boundCloseMenu);
window.addEventListener('resize', this.boundResizeHandler);
},
destroy() {
if (this.activeObservers) {
this.activeObservers.forEach(observer => observer.disconnect());
this.activeObservers.clear();
}
clearTimeout(this.tooltipTimeout);
clearTimeout(this.hideTooltipTimer);
clearTimeout(this.badgeFadeTimeout);
const ui = document.getElementById('chat-nav-container');
if (ui) ui.remove();
const settingsWrapper = document.querySelector('.quicknav-settings-wrapper');
if (settingsWrapper) settingsWrapper.remove();
const dropdown = document.getElementById('quicknav-settings-dropdown');
if (dropdown) dropdown.remove();
const menu = document.getElementById('chat-nav-menu-container');
if (menu) menu.remove();
const badge = document.getElementById('quicknav-badge-floater');
if (badge) badge.remove();
const tooltip = document.getElementById('quicknav-custom-tooltip');
if (tooltip) tooltip.remove();
this.customTooltip = null;
this.footerElement = null;
this.toolbarElement = null;
this.navContainerElement = null;
if (this.listenedTurnElement) {
this.listenedTurnElement.removeEventListener('mousemove', this._handleTurnMouseMove);
this.listenedTurnElement.removeEventListener('mouseleave', this._handleTurnMouseLeave);
this.listenedTurnElement = null;
}
if (this.boundCloseMenu) {
document.removeEventListener('click', this.boundCloseMenu);
this.boundCloseMenu = null;
}
if (this.boundResizeHandler) {
window.removeEventListener('resize', this.boundResizeHandler);
this.boundResizeHandler = null;
}
},
// Caches static DOM elements to prevent repetitive queries
cacheStaticElements() {
this.footerElement = document.querySelector('ms-chunk-editor footer, section.chunk-editor-main > footer');
this.toolbarElement = document.querySelector('ms-playground-toolbar, ms-toolbar');
},
showBadge() {
const floater = document.getElementById('quicknav-badge-floater');
if (!floater) return;
this.cancelHideBadge();
floater.style.opacity = '1';
},
hideBadge(delay = 1000, force = false) {
const floater = document.getElementById('quicknav-badge-floater');
if (!floater || (this.isHoveringOnNav && !force)) return;
this.cancelHideBadge();
if (force) {
floater.classList.add('quicknav-badge-notransition');
floater.style.opacity = '0';
requestAnimationFrame(() => {
floater.classList.remove('quicknav-badge-notransition');
});
} else {
this.badgeFadeTimeout = setTimeout(() => {
floater.style.opacity = '0';
}, delay);
}
},
cancelHideBadge() {
clearTimeout(this.badgeFadeTimeout);
},
setupMessageObserver() {
const scrollContainer = document.querySelector('ms-autoscroll-container');
const messages = document.querySelectorAll('.message-turn');
if (!scrollContainer || !messages.length) return;
const observerOptions = { root: scrollContainer, rootMargin: '0px', threshold: 0.1 };
const observer = new IntersectionObserver(this.handleMessageVisibilityChange.bind(this), observerOptions);
messages.forEach(msg => observer.observe(msg));
},
handleMessageVisibilityChange(entries) {
const visibleEntries = entries.filter(entry => entry.isIntersecting);
if (visibleEntries.length === 0) return;
const bottomMostEntry = visibleEntries.reduce((prev, current) => {
return prev.boundingClientRect.top > current.boundingClientRect.top ? prev : current;
});
if (bottomMostEntry && bottomMostEntry.target !== this.currentVisibleMessage) {
this.currentVisibleMessage = bottomMostEntry.target;
this.showBadge();
this.updateScrollPercentage();
this.hideBadge();
}
},
// Handles page scroll events with high performance debouncing
handlePageScroll() {
if (this.isThrottled) return;
this.isThrottled = true;
setTimeout(() => {
const scrollContainer = typeof ChatController !== 'undefined' ? ChatController.currentScrollContainer : null;
if (scrollContainer) {
const currentTime = performance.now();
const currentScrollTop = scrollContainer.scrollTop;
const deltaTime = currentTime - this.lastScrollTime;
if (deltaTime > 50) {
const deltaScroll = Math.abs(currentScrollTop - this.lastScrollTop);
const scrollSpeed = (deltaScroll / deltaTime) * 1000;
if (scrollSpeed > 800) {
this.showBadge();
this.updateScrollPercentage();
this.hideBadge(1000);
}
}
this.lastScrollTop = currentScrollTop;
this.lastScrollTime = currentTime;
}
this.isThrottled = false;
}, 100);
},
createGlobalElements() {
if (!document.getElementById('quicknav-badge-floater')) {
const badgeFloater = document.createElement('div');
badgeFloater.id = 'quicknav-badge-floater';
const badgeIndex = document.createElement('div');
badgeIndex.id = 'quicknav-badge-index';
const badgePercentage = document.createElement('div');
badgePercentage.id = 'quicknav-badge-percentage';
badgeFloater.append(badgeIndex, badgePercentage);
document.body.appendChild(badgeFloater);
}
if (!document.getElementById('quicknav-custom-tooltip')) {
this.customTooltip = document.createElement('div');
this.customTooltip.id = 'quicknav-custom-tooltip';
document.body.appendChild(this.customTooltip);
}
},
debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
},
// Injects all CSS styles for the navigation interface
injectStyles() {
if (document.getElementById('chat-nav-styles')) return;
const cssModules = {
core: `
.chat-turn-container { position: relative; z-index: 1; }
.chat-turn-container:hover, .chat-turn-container:focus-within, .chat-turn-container:has([aria-expanded="true"]) { z-index: 20 !important; }
@keyframes google-text-flow { to { background-position: 200% center; } }
@keyframes donate-blue-flow { to { background-position: 200% center; } }
@keyframes load-all-flow { to { background-position: 200% center; } }
@keyframes quicknav-fade-in-out { 0% { opacity: 0; transform: translate(-50%, 10px); } 15% { opacity: 1; transform: translate(-50%, 0); } 85% { opacity: 1; transform: translate(-50%, 0); } 100% { opacity: 0; transform: translate(-50%, -10px); } }
@keyframes quicknav-plasma-blue {
0% { box-shadow: 0 0 6px 1px rgba(66, 133, 244, 0.5), inset 0 0 2px rgba(138, 180, 248, 0.4); border-color: #8ab4f8; color: #ffffff; background-color: rgba(66, 133, 244, 0.15); }
40% { box-shadow: 0 0 14px 4px rgba(66, 133, 244, 0.9), inset 0 0 8px rgba(138, 180, 248, 0.7); border-color: #aecbfa; color: #ffffff; background-color: rgba(66, 133, 244, 0.35); }
80% { box-shadow: 0 0 2px 0 rgba(66, 133, 244, 0.2); border-color: #669df6; color: #8ab4f8; background-color: rgba(66, 133, 244, 0.05); }
100% { box-shadow: 0 0 6px 1px rgba(66, 133, 244, 0.5), inset 0 0 2px rgba(138, 180, 248, 0.4); border-color: #8ab4f8; color: #ffffff; background-color: rgba(66, 133, 244, 0.15); }
}
@keyframes quicknav-loading-flow { to { background-position: -200% center; } }
.google-text-animated, .google-text-flash { background: linear-gradient(90deg, #8ab4f8, #e67c73, #f7cb73, #57bb8a, #8ab4f8); background-size: 200% auto; -webkit-background-clip: text; background-clip: text; color: transparent !important; }
.google-text-animated { animation: google-text-flow 10s linear infinite; }
.google-text-flash { animation: google-text-flow 1.5s ease-in-out 1; }
.donate-button-animated { background: linear-gradient(90deg, #a8c7fa, #8ab4f8, #669df6, #8ab4f8, #a8c7fa); background-size: 200% auto; -webkit-background-clip: text; background-clip: text; color: transparent !important; animation: donate-blue-flow 8s ease-in-out infinite; }
ms-autoscroll-container:focus { outline: none; }
`,
navBar: `
#chat-nav-container { display: flex; justify-content: center; align-items: center; gap: 12px; margin: 4px auto; width: 100%; box-sizing: border-box; position: relative; z-index: 2147483647; }
.counter-wrapper { position: relative; pointer-events: none; z-index: 9999; }
.chat-nav-button, #chat-nav-counter {
background-color: transparent; border: 1px solid var(--ms-on-surface-variant, #888888);
transition: transform 0.1s ease-out, background-color 0.15s ease-in-out, border-color 0.15s ease, box-shadow 0.2s ease, filter 0.2s ease, color 0.15s ease;
pointer-events: auto; user-select: none; cursor: pointer; -webkit-transform: translateZ(0); transform: translateZ(0); box-shadow: 0 0 1px rgba(0,0,0,0);
}
.chat-nav-button { color: var(--ms-on-surface-variant, #888888); flex-shrink: 0; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 50%; }
#nav-up, #nav-down { color: #8ab4f8; }
#nav-top, #nav-bottom, #chat-nav-counter { color: var(--ms-on-surface-variant, #888888); }
#chat-nav-counter { font-family: 'Google Sans', sans-serif; font-size: 14px; padding: 4px 8px; border-radius: 8px; display: inline-flex; align-items: baseline; border-color: var(--ms-on-surface-variant, #888888); }
.quicknav-resize-btn { border-color: transparent !important; background-color: transparent !important; color: var(--ms-on-surface-variant, #888888) !important; transition: color 0.2s ease; }
.quicknav-resize-btn:hover:not(:disabled) { background-color: transparent !important; border-color: transparent !important; color: #8ab4f8 !important; filter: none !important; }
.quicknav-resize-btn:disabled { opacity: 0.3; cursor: default; pointer-events: none; filter: grayscale(100%); }
.lib-btn-separator { margin-right: 6px; position: relative; }
.lib-btn-separator::after { content: ''; position: absolute; right: -5px; top: 6px; bottom: 6px; width: 1px; background-color: #5f6368; opacity: 0.3; pointer-events: none; }
.chat-nav-button:hover, .chat-nav-button:focus { outline: none; }
#nav-top:hover, #nav-bottom:hover, #nav-top:focus, #nav-bottom:focus { background-color: rgba(136, 136, 136, 0.04); border-color: #bbbbbb; color: #e8eaed; filter: drop-shadow(0 0 2px rgba(189, 193, 198, 0.3)); }
#nav-up:hover, #nav-down:hover, #nav-up:focus, #nav-down:focus, #chat-nav-counter:hover, #chat-nav-counter:focus { background-color: rgba(138, 180, 248, 0.04); border-color: #aecbfa; color: #d2e3fc; filter: drop-shadow(0 0 2px rgba(138, 180, 248, 0.4)); }
.chat-nav-button:active { -webkit-transform: scale(0.95) translateZ(0); transform: scale(0.95) translateZ(0); }
#nav-bottom.auto-click-active { animation: quicknav-plasma-blue 2.2s ease-in-out infinite; }
#chat-nav-current-num.chat-nav-current-grey { color: var(--ms-on-surface-variant, #888888); }
#chat-nav-current-num.chat-nav-current-blue { color: #8ab4f8; font-weight: 500; }
#chat-nav-total-num { color: #8ab4f8; font-weight: 500; }
.quicknav-title { flex: 2; text-align: center; font-family: 'Google Sans', 'Inter Tight', sans-serif; font-size: 14px; user-select: text; font-weight: 600; }
#quicknav-badge-floater { position: fixed; z-index: 99998; opacity: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; pointer-events: none; padding: 4px 3px; border-radius: 8px; font-family: 'Google Sans', sans-serif; transition: opacity 0.15s ease-in-out; min-width: 28px; box-sizing: border-box; }
.quicknav-badge-notransition { transition: none !important; }
#quicknav-badge-index { font-size: 13px; font-weight: 500; line-height: 1.2; }
#quicknav-badge-percentage { font-size: 10px; font-weight: 400; line-height: 1.2; border-top: 1px solid rgba(255, 255, 255, 0.3); margin-top: 3px; padding-top: 3px; }
.prompt-badge-bg { background-color: #5f6368; color: #FFFFFF; }
.response-badge-bg { background-color: #174ea6; color: #FFFFFF; }
`,
navMenu: `
#chat-nav-menu-container { display: flex; flex-direction: column; position: fixed; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.25); max-height: 90vh; z-index: 99999; max-width: 95vw; min-width: 300px; box-sizing: border-box; visibility: hidden; opacity: 0; pointer-events: none; transition: opacity 0.15s ease-in-out, visibility 0s linear 0.15s; background-color: #f1f3f4; border: 2px solid #1a73e8; }
#chat-nav-menu-container.visible { visibility: visible; opacity: 1; pointer-events: auto; transition: opacity 0.15s ease-in-out, visibility 0s linear 0s; }
#chat-nav-menu-container:focus { outline: none; }
#chat-nav-menu { list-style: none; margin: 0; padding: 0 8px 8px 8px; overflow-y: auto; scroll-behavior: smooth; flex-grow: 1; border-radius: 0 0 10px 10px; background-color: #f1f3f4; scrollbar-width: thin; scrollbar-color: #8ab4f8 transparent; }
.chat-nav-menu-item { display: flex; align-items: center; padding: 8px 12px; margin: 2px 0; border-radius: 8px; cursor: pointer; font-size: 13px; font-family: 'Google Sans', sans-serif; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; background-color: #ffffff; color: #202124; }
.chat-nav-menu-item:hover { background-color: #e8eaed; }
.chat-nav-menu-item.menu-item-focused { background-color: #dfe1e5; }
.chat-nav-menu-item.loading-in-progress { background: linear-gradient(100deg, #f1f3f4 20%, #d2e3fc 40%, #d2e3fc 60%, #f1f3f4 80%); background-size: 200% 100%; animation: quicknav-loading-flow 1.8s linear infinite; }
.menu-item-number { font-weight: 500; margin-right: 8px; flex-shrink: 0; }
.prompt-number-color { color: var(--ms-on-surface-variant, #9aa0a6); }
.response-number-color { color: var(--ms-primary, #8ab4f8); }
.menu-item-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.prompt-item-bg { border-left: 3px solid var(--ms-on-surface-variant, #9aa0a6); margin-left: 32px; }
.response-item-bg { border-left: 3px solid var(--ms-primary, #8ab4f8); border-bottom: 1px solid var(--ms-primary, #8ab4f8); }
.chat-nav-menu-header { flex-shrink: 0; z-index: 1; display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; border-radius: 10px 10px 0 0; background-color: #f1f3f4; border-bottom: 1px solid #1a73e8; }
.chat-nav-search-row { flex-shrink: 0; padding: 8px 12px; background-color: #f1f3f4; border-bottom: 1px solid var(--ms-outline, #dadce0); display: flex; align-items: center; position: relative; }
.chat-nav-filter-input { flex: 1; box-sizing: border-box; margin: 0; height: 32px; padding: 0 65px 0 12px; border-radius: 6px; border: 1px solid #202124; font-size: 13px; font-family: 'Google Sans', sans-serif; outline: none; background-color: #ffffff; color: #202124; transition: border-color 0.2s; text-align: center; }
.chat-nav-filter-input:focus { border-color: #202124; }
.chat-nav-search-clear { position: absolute; right: 16px; top: 50%; transform: translateY(-50%); cursor: pointer; color: #5f6368; font-weight: 500; font-size: 12px; display: none; align-items: center; justify-content: center; background-color: rgba(0,0,0,0.04); border: 1px solid rgba(0,0,0,0.1); border-radius: 4px; transition: all 0.2s; padding: 0 8px; height: 24px; box-sizing: border-box; width: auto; }
.chat-nav-search-clear:hover { background-color: rgba(0,0,0,0.08); border-color: rgba(0,0,0,0.2); color: #202124; }
.header-controls { flex: 1; display: flex; align-items: center; gap: 8px; }
.header-controls.left { justify-content: flex-start; }
.header-controls.right { justify-content: flex-end; }
.header-button { font-family: 'Google Sans', sans-serif; text-decoration: none; font-size: 12px; padding: 4px 10px; border-radius: 16px; transition: background-color 0.15s ease-in-out; border: 1px solid #669df6; cursor: pointer; font-weight: 500; }
.header-button:disabled { opacity: 0.5; cursor: not-allowed; }
.header-button:hover:not(:disabled) { box-shadow: 0 0 8px rgba(66, 133, 244, 0.4); }
#chat-nav-load-button { background-color: #e8f0fe; color: #1967d2; }
#chat-nav-load-button:hover:not(:disabled) { background-color: #d2e3fc; }
#chat-nav-load-button.loading-active { color: #ffffff; background: linear-gradient(90deg, #1967d2, #4285f4, #1967d2); background-size: 200% auto; animation: load-all-flow 2s linear infinite; }
#chat-nav-loader-status { font-family: 'Google Sans', sans-serif; font-size: 12px; color: var(--ms-on-surface-variant, #9aa0a6); padding: 4px 10px; font-weight: 600; }
#quicknav-custom-tooltip { position: fixed; z-index: 100000; border-radius: 6px; font-size: 13px; font-family: 'Google Sans', sans-serif; max-width: 40vw; pointer-events: none; opacity: 0; transition: opacity 0.15s ease-in-out; white-space: pre-wrap; background-color: #f1f3f4; color: #202124; border: 1px solid #dadce0; box-shadow: 0 2px 6px rgba(0,0,0,0.15); line-height: 1.2; display: flex; flex-direction: column; padding: 0; overflow: hidden; }
.quicknav-tooltip-body { padding: 8px 12px; overflow-y: auto; flex: 1; min-height: 0; scrollbar-width: thin; scrollbar-color: #8ab4f8 transparent; }
.quicknav-tooltip-body::-webkit-scrollbar { width: 6px; height: 6px; }
.quicknav-tooltip-body::-webkit-scrollbar-track { background: transparent; }
.quicknav-tooltip-body::-webkit-scrollbar-thumb { background-color: #8ab4f8 !important; border-radius: 3px; }
.quicknav-tooltip-body::-webkit-scrollbar-thumb:hover { background-color: #174ea6 !important; }
.response-item-bg .menu-item-text { color: #174ea6; }
#quicknav-custom-tooltip.lib-fixed-tooltip { z-index: 100002 !important; max-width: none !important; box-shadow: 0 4px 12px rgba(0,0,0,0.25) !important; background-color: #ffffff !important; border-color: #8ab4f8 !important; }
#chat-nav-menu::-webkit-scrollbar { width: 6px; }
#chat-nav-menu::-webkit-scrollbar-track { background: transparent; }
#chat-nav-menu::-webkit-scrollbar-thumb { background-color: #8ab4f8; border-radius: 3px; }
#chat-nav-menu::-webkit-scrollbar-thumb:hover { background-color: #174ea6; }
.quicknav-menu-resizer { position: absolute; top: 0; bottom: 0; width: 20px; cursor: ew-resize; z-index: 2147483647; touch-action: none; background: transparent; transition: background-color 0.15s; }
.quicknav-menu-resizer:hover { background-color: rgba(138, 180, 248, 0.2); }
.quicknav-resizer-left { left: -10px; }
.quicknav-resizer-right { right: -10px; }
.quicknav-search-highlight { background-color: #fce8b2; color: #202124; border-radius: 2px; padding: 0 1px; scroll-margin-top: 40px; }
.quicknav-search-highlight.active { background-color: #f9ab00; color: #fff; box-shadow: 0 0 2px rgba(0,0,0,0.3); }
.quicknav-tooltip-toolbar { flex-shrink: 0; width: 100%; box-sizing: border-box; background-color: #f1f3f4; border-bottom: 1px solid #dadce0; padding: 6px 12px; font-weight: 500; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 0; z-index: 20; }
.quicknav-tooltip-left-group { display: flex; align-items: center; gap: 4px; justify-self: start; }
.quicknav-tooltip-btn { background: transparent; border: 1px solid rgba(0,0,0,0.1); color: #5f6368; cursor: pointer; border-radius: 3px; width: 42px; height: 24px; display: flex; align-items: center; justify-content: center; padding: 0; transition: background 0.2s; font-size: 16px; font-weight: bold; flex-shrink: 0; }
.quicknav-tooltip-btn:hover { background-color: rgba(0,0,0,0.05); color: #202124; }
.quicknav-match-counter { font-size: 12px; color: #5f6368; font-family: monospace; padding: 0 2px; }
.quicknav-msg-title { user-select: none; font-size: 15px; font-weight: 600; text-align: center; color: #202124; justify-self: center; }
.quicknav-no-results { padding: 16px; text-align: center; color: #9aa0a6; font-size: 13px; font-style: italic; }
body.dark-theme #chat-nav-menu-container { background-color: #191919; border-color: #8ab4f8; }
body.dark-theme #chat-nav-menu { background-color: #191919; }
body.dark-theme .chat-nav-menu-item { background-color: #202124; color: #e8eaed; }
body.dark-theme .chat-nav-menu-item.loading-in-progress { background: linear-gradient(100deg, #202124 20%, #3c4043 40%, #3c4043 60%, #202124 80%); background-size: 200% 100%; animation: quicknav-loading-flow 1.8s linear infinite; }
body.dark-theme .chat-nav-menu-item:hover { background-color: #3c4043; }
body.dark-theme .chat-nav-menu-item.menu-item-focused { background-color: #5f6368; }
body.dark-theme .chat-nav-menu-header { background-color: #191919; border-color: #8ab4f8; }
body.dark-theme .chat-nav-search-row { background-color: #191919; border-bottom-color: #5f6368; }
body.dark-theme .chat-nav-filter-input { background-color: #303134; border-color: #171717; color: #e8eaed; }
body.dark-theme .chat-nav-filter-input:focus { border-color: #171717; background-color: #202124; }
body.dark-theme .chat-nav-search-clear { color: #9aa0a6; border-color: rgba(255,255,255,0.1); background-color: rgba(255,255,255,0.04); }
body.dark-theme .chat-nav-search-clear:hover { color: #e8eaed; border-color: rgba(255,255,255,0.2); background-color: rgba(255,255,255,0.08); }
body.dark-theme .header-button { background-color: #3c4043; color: #e8eaed; border-color: #8ab4f8; }
body.dark-theme #chat-nav-load-button { background-color: #28354a; color: #a8c7fa; }
body.dark-theme .header-button:hover:not(:disabled) { background-color: #5f6368; }
body.dark-theme #chat-nav-load-button:hover:not(:disabled) { background-color: #3c4043; }
body.dark-theme #chat-nav-load-button.loading-active { color: #202124; background: linear-gradient(90deg, #8ab4f8, #a8c7fa, #8ab4f8); background-size: 200% auto; animation: load-all-flow 2s linear infinite; }
body.dark-theme #quicknav-custom-tooltip { background-color: #2d2d2d; color: #e0e0e0; border: 1px solid #555; scrollbar-color: #8ab4f8 transparent; }
body.dark-theme .quicknav-tooltip-body::-webkit-scrollbar-thumb { background-color: #8ab4f8 !important; }
body.dark-theme .quicknav-tooltip-body::-webkit-scrollbar-thumb:hover { background-color: #aecbfa !important; }
body.dark-theme #quicknav-custom-tooltip.lib-fixed-tooltip { background-color: #202124 !important; border-color: #8ab4f8 !important; }
body.dark-theme .quicknav-search-highlight { background-color: #5e4510; color: #e8eaed; }
body.dark-theme .quicknav-search-highlight.active { background-color: #fce8b2; color: #202124; }
body.dark-theme .quicknav-tooltip-toolbar { background-color: #2d2d2d; color: #e8eaed; border-bottom-color: #555; }
body.dark-theme .quicknav-tooltip-btn { border-color: rgba(255,255,255,0.2); color: #e8eaed; }
body.dark-theme .quicknav-tooltip-btn:hover { background-color: rgba(255,255,255,0.1); }
body.dark-theme .quicknav-match-counter { color: #9aa0a6; }
body.dark-theme .quicknav-msg-title { color: #e8eaed; }
body.dark-theme .response-item-bg .menu-item-text { color: var(--ms-primary, #8ab4f8); }
body.dark-theme #chat-nav-menu::-webkit-scrollbar-thumb { background-color: #8ab4f8; }
body.dark-theme #chat-nav-menu::-webkit-scrollbar-thumb:hover { background-color: #aecbfa; }
`,
settings: `
.quicknav-settings-wrapper { position: absolute; right: max(10px, calc(((100% - var(--quicknav-chat-width, 1000px)) / 2) + 10px)); top: 50%; transform: translateY(-50%); display: flex; align-items: center; gap: 13px; z-index: 10; margin: 0; }
.quicknav-left-wrapper { position: absolute; left: max(10px, calc(((100% - var(--quicknav-chat-width, 1000px)) / 2) + 10px)); top: 50%; transform: translateY(-50%); display: flex; align-items: center; gap: 4px; z-index: 10; margin: 0; }
.quicknav-dropdown-trigger { border: 1px solid transparent; background: transparent; color: #8ab4f8; height: 28px; min-width: 28px; display: flex; align-items: center; justify-content: center; cursor: pointer; border-radius: 4px; transition: all 0.15s ease-in-out; padding: 0; margin-left: 8px; }
.quicknav-dropdown-trigger:hover, .quicknav-dropdown-trigger.active { background-color: rgba(138, 180, 248, 0.04); border-color: rgba(138, 180, 248, 0.5); color: #d2e3fc; filter: drop-shadow(0 0 2px rgba(138, 180, 248, 0.4)); }
.quicknav-dropdown-menu { position: fixed; background-color: #ffffff; border: 1px solid #dadce0; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); padding: 8px; z-index: 2147483648; min-width: 160px; display: none; flex-direction: column; gap: 8px; }
.quicknav-dropdown-menu.visible { display: flex; }
.quicknav-control-row { display: flex; align-items: center; gap: 8px; padding: 0 4px; justify-content: space-between; min-height: 30px; margin-bottom: 4px; }
.quicknav-checkbox { cursor: pointer; width: 16px; height: 16px; accent-color: #8ab4f8; margin: 0; }
.quicknav-toolbar-group { display: flex; align-items: center; gap: 4px; justify-content: center; transition: opacity 0.2s ease, filter 0.2s ease; }
.quicknav-toolbar-group.disabled { opacity: 0.4; pointer-events: none; filter: grayscale(100%); }
.quicknav-tool-btn { border: 1px solid transparent; background: transparent; color: #8ab4f8; height: 28px; min-width: 28px; display: flex; align-items: center; justify-content: center; cursor: pointer; border-radius: 4px; transition: all 0.15s ease-in-out; padding: 0 4px; }
.quicknav-tool-indicator { font-family: 'Google Sans', sans-serif; font-size: 13px; font-weight: 500; min-width: 40px; text-align: center; }
.quicknav-tool-btn:hover { background-color: rgba(138, 180, 248, 0.08); border-color: rgba(138, 180, 248, 0.5); }
.quicknav-tool-btn:active { background-color: rgba(138, 180, 248, 0.25); }
.quicknav-tool-btn:disabled { color: var(--ms-on-surface-variant, #9aa0a6); opacity: 0.5; cursor: default; background-color: transparent; border-color: transparent; }
.quicknav-row-label { display: flex; align-items: center; gap: 6px; }
.quicknav-icon-label { width: 18px; height: 18px; color: #5f6368; }
.quicknav-dropdown-item { padding: 8px 12px; cursor: pointer; font-size: 13px; color: #202124; display: flex; align-items: center; gap: 8px; border-radius: 4px; }
.quicknav-dropdown-item:hover { background-color: #f1f3f4; }
.quicknav-fav-item { display: flex; flex-direction: column; padding: 4px 8px; border-radius: 4px; gap: 4px; cursor: default; border-left: 3px solid transparent; min-height: 0; flex-shrink: 0; }
.quicknav-fav-item:hover { background-color: #f8f9fa; }
.quicknav-fav-item.fav-active-item { background-color: rgba(138, 180, 248, 0.12); border-left-color: #1a73e8; }
.quicknav-fav-item.key-focused { background-color: rgba(138, 180, 248, 0.12); border-left-color: #1a73e8; }
.quicknav-fav-item.fav-confirm-mode { background-color: #fce8e6; border: 1px solid #fad2cf; }
.fav-display { display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; width: 100%; }
.fav-text-group { display: flex; flex-direction: column; flex: 1; text-decoration: none; color: inherit; cursor: pointer; min-width: 0; }
.fav-title { font-weight: 500; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: #202124; line-height: 1.3; }
.fav-url { font-size: 9px; color: #5f6368; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 0; line-height: 1.2; }
.fav-actions { display: flex; gap: 2px; opacity: 0; transition: opacity 0.2s; }
.quicknav-fav-item:hover .fav-actions { opacity: 1; }
.fav-confirm-row { display: flex; align-items: center; justify-content: space-between; width: 100%; color: #d93025; }
.fav-confirm-label { font-size: 13px; font-weight: 500; }
#quicknav-favorites-menu { max-height: 80vh; height: auto; display: flex; flex-direction: column; overflow-y: auto; overflow-x: hidden; gap: 2px; padding: 4px; scrollbar-width: thin; scrollbar-color: #8ab4f8 transparent; }
#quicknav-favorites-menu::-webkit-scrollbar { width: 6px; }
#quicknav-favorites-menu::-webkit-scrollbar-track { background: transparent; }
#quicknav-favorites-menu::-webkit-scrollbar-thumb { background-color: #8ab4f8; border-radius: 3px; }
#quicknav-favorites-menu::-webkit-scrollbar-thumb:hover { background-color: #174ea6; }
body.dark-theme .quicknav-dropdown-menu { background-color: #202124; border-color: #5f6368; box-shadow: 0 4px 6px rgba(0,0,0,0.3); }
body.dark-theme .quicknav-icon-label { color: #9aa0a6; }
body.dark-theme .quicknav-tool-indicator { color: #e8eaed; }
body.dark-theme .quicknav-tool-btn:hover { background-color: rgba(138, 180, 248, 0.15); }
body.dark-theme .quicknav-dropdown-item { color: #e8eaed; }
body.dark-theme .quicknav-dropdown-item:hover { background-color: #3c4043; }
body.dark-theme .fav-title { color: #e8eaed; }
body.dark-theme .fav-url { color: #9aa0a6; }
body.dark-theme .quicknav-fav-item:hover { background-color: #2d2e30; }
body.dark-theme .quicknav-fav-item.fav-active-item { background-color: rgba(138, 180, 248, 0.15); border-left-color: #8ab4f8; }
body.dark-theme .quicknav-fav-item.key-focused { background-color: rgba(138, 180, 248, 0.15); border-left-color: #8ab4f8; }
body.dark-theme .fav-action-row { border-bottom-color: #5f6368 !important; }
body.dark-theme .quicknav-fav-item.fav-confirm-mode { background-color: #410e0b; border-color: #8c1d18; }
body.dark-theme .fav-confirm-row { color: #f28b82; }
body.dark-theme #quicknav-favorites-menu::-webkit-scrollbar-thumb { background-color: #8ab4f8; }
body.dark-theme #quicknav-favorites-menu::-webkit-scrollbar-thumb:hover { background-color: #aecbfa; }
`,
library: `
.quicknav-modal-backdrop { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.4); backdrop-filter: blur(2px); z-index: 100000; display: none; align-items: center; justify-content: center; opacity: 0; transition: opacity 0.2s; }
.quicknav-modal-backdrop.visible { display: flex; opacity: 1; }
.quicknav-modal { width: 800px; max-width: 90vw; height: 70vh; background-color: #ffffff; border-radius: 16px; box-shadow: 0 12px 24px rgba(0,0,0,0.2); display: flex; flex-direction: column; overflow: hidden; transform: scale(0.96); transition: transform 0.2s; position: relative; }
.quicknav-modal-backdrop.visible .quicknav-modal { transform: scale(1); }
.lib-header { padding: 12px; background-color: #ffffff; border-bottom: 1px solid #e0e0e0; display: flex; flex-direction: column; gap: 8px; }
.lib-header-top { display: flex; justify-content: space-between; align-items: center; gap: 16px; }
.lib-search-container { flex: 1; position: relative; display: flex; align-items: center; }
.lib-search-input { width: 100%; padding: 8px 32px 8px 36px; border: 1px solid #dadce0; border-radius: 8px; font-size: 14px; font-family: 'Google Sans', sans-serif; outline: none; background-color: #f1f3f4; color: #202124; transition: background-color 0.2s, box-shadow 0.2s; }
.lib-search-input:focus { background-color: #ffffff; box-shadow: 0 1px 2px rgba(0,0,0,0.1); border-color: #1a73e8; }
.lib-search-container svg { position: absolute; left: 10px; color: #5f6368; pointer-events: none; }
.lib-search-clear { position: absolute; right: 10px; cursor: pointer; color: #5f6368; display: none; font-weight: bold; font-size: 16px; line-height: 1; padding: 4px; }
.lib-search-clear:hover { color: #202124; }
.lib-actions-group { display: flex; gap: 8px; }
.lib-btn-primary { background-color: #1a73e8; color: white; border: none; padding: 6px 16px; border-radius: 20px; font-size: 13px; font-weight: 500; cursor: pointer; display: flex; align-items: center; gap: 6px; transition: background 0.2s; }
.lib-btn-primary:hover { background-color: #1557b0; }
.lib-btn-secondary { background-color: transparent; color: #5f6368; border: 1px solid #dadce0; padding: 6px 12px; border-radius: 20px; font-size: 13px; cursor: pointer; display: flex; align-items: center; transition: background 0.2s; }
.lib-btn-secondary:hover { background-color: #f1f3f4; }
.lib-btn-danger { background-color: #d93025; color: white; border: none; padding: 6px 16px; border-radius: 20px; cursor: pointer; }
.lib-btn-danger:hover { background-color: #b31412; }
.lib-tags-row { display: flex; gap: 8px; overflow-x: auto; padding: 2px 4px 6px 4px; scrollbar-width: none; }
.lib-chip { background-color: #e8eaed; border: none; padding: 4px 12px; border-radius: 8px; font-size: 12px; color: #3c4043; cursor: pointer; transition: all 0.2s; white-space: nowrap; }
.lib-chip:hover { background-color: #dadce0; }
.lib-chip.active { background-color: #e8f0fe; color: #1967d2; font-weight: 500; }
.lib-content-list { flex: 1; overflow-y: auto; background-color: #f8f9fa; padding: 16px; display: grid; grid-template-columns: 1fr; gap: 8px; align-content: start; scrollbar-width: thin; scrollbar-color: #8ab4f8 transparent; }
.lib-content-list::-webkit-scrollbar, .lib-tags-row::-webkit-scrollbar { width: 6px; height: 6px; }
.lib-content-list::-webkit-scrollbar-track, .lib-tags-row::-webkit-scrollbar-track { background: transparent; }
.lib-content-list::-webkit-scrollbar-thumb, .lib-tags-row::-webkit-scrollbar-thumb { background-color: #8ab4f8; border-radius: 3px; }
.lib-content-list::-webkit-scrollbar-thumb:hover, .lib-tags-row::-webkit-scrollbar-thumb:hover { background-color: #174ea6; }
.lib-empty-state { text-align: center; color: #5f6368; margin-top: 40px; font-size: 14px; }
.lib-card { background-color: #ffffff; border: 1px solid #e0e0e0; border-radius: 12px; padding: 8px; cursor: pointer; transition: all 0.2s; display: flex; flex-direction: column; gap: 4px; position: relative; min-height: auto; height: auto; }
.lib-card:hover, .lib-card.active-tooltip-source { border-color: #8ab4f8; transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0,0,0,0.05); }
.lib-card.focused { border-color: #1a73e8; box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.3); }
.lib-card-header { display: flex; justify-content: space-between; align-items: flex-start; }
.lib-card-title { font-weight: 600; font-size: 14px; color: #202124; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; max-width: 85%; }
.lib-card-actions { opacity: 0; transition: opacity 0.2s; display: flex; gap: 4px; }
.lib-card:hover .lib-card-actions, .lib-card.focused .lib-card-actions, .lib-card.active-tooltip-source .lib-card-actions { opacity: 1; }
.lib-icon-btn { background: transparent; border: none; padding: 4px; border-radius: 4px; color: #5f6368; cursor: pointer; }
.lib-icon-btn:hover { background-color: #f1f3f4; color: #1a73e8; }
.lib-icon-btn.delete:hover { background-color: #fce8e6; color: #d93025; }
.lib-card-tags { display: inline; }
.lib-card-tags span { font-size: 10px; background-color: #f1f3f4; padding: 2px 6px; border-radius: 4px; color: #5f6368; }
.lib-card-preview { font-size: 13px; color: #5f6368; display: -webkit-box !important; -webkit-line-clamp: 2 !important; -webkit-box-orient: vertical !important; overflow: hidden; line-height: 1.35; margin-top: 4px; word-break: break-word; flex: 0 0 auto !important; }
.lib-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: #ffffff; z-index: 10; display: flex; flex-direction: column; padding: 20px; box-sizing: border-box; opacity: 0; pointer-events: none; transform: scale(0.98); transition: opacity 0.2s ease, transform 0.2s ease; }
.lib-overlay.visible { opacity: 1; pointer-events: auto; transform: scale(1); }
.lib-editor-container { display: flex; flex-direction: column; height: 100%; gap: 12px; }
.lib-editor-container h3 { margin: 0 0 8px 0; font-size: 18px; color: #202124; }
.lib-input-full { padding: 8px 12px; border: 1px solid #dadce0; border-radius: 4px; font-family: 'Google Sans', sans-serif; font-size: 14px; outline: none; background-color: #f1f3f4; color: #202124; }
.lib-input-full:focus { border-color: #1a73e8; background-color: #ffffff; }
.lib-textarea-full { flex: 1; padding: 12px; border: 1px solid #dadce0 !important; border-radius: 4px; font-family: 'Roboto Mono', monospace; font-size: 13px; outline: none; resize: none; background-color: #f1f3f4; color: #202124; }
.lib-textarea-full:focus { border-color: #1a73e8; background-color: #ffffff; }
.lib-suggestions-menu { position: absolute; background: white; border: 1px solid #dadce0; border-radius: 8px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); max-height: 250px; overflow-y: auto; z-index: 20; display: flex; flex-direction: column; width: 220px; }
.lib-suggestion-item { padding: 8px 12px; cursor: pointer; font-size: 13px; color: #202124; display: flex; align-items: center; gap: 8px; }
.lib-suggestion-item:hover { background-color: #f1f3f4; }
.lib-suggestion-color { width: 10px; height: 10px; border-radius: 50%; }
.lib-suggestion-create-row { display: flex; flex-direction: column; padding: 8px 12px; border-top: 1px solid #f1f3f4; gap: 6px; }
.lib-suggestion-create-label { font-size: 12px; color: #5f6368; font-weight: 500; }
.lib-suggestion-palette { display: grid; grid-template-columns: repeat(8, 1fr); gap: 4px; }
.lib-suggestion-palette-item { width: 16px; height: 16px; border-radius: 50%; cursor: pointer; border: 1px solid rgba(0,0,0,0.1); }
.lib-suggestion-palette-item:hover { transform: scale(1.2); }
#lib-color-picker { background-color: #ffffff; border: 1px solid #dadce0; box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
.lib-editor-toolbar { display: flex; gap: 8px; padding-bottom: 4px; }
.lib-btn-text { background: none; border: none; color: #1a73e8; font-size: 12px; font-weight: 500; cursor: pointer; padding: 0; }
.lib-btn-text:hover { text-decoration: underline; }
.lib-overlay-footer { margin-top: auto; display: flex; justify-content: flex-end; gap: 12px; padding-top: 12px; border-top: 1px solid #f1f3f4; }
.lib-confirm-wrap { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; }
.lib-confirm-box { background: #fff; border: 1px solid #dadce0; border-radius: 12px; padding: 24px; text-align: center; max-width: 320px; width: 100%; box-shadow: 0 4px 12px rgba(0,0,0,0.1); }
.lib-confirm-box h3 { margin: 0 0 8px 0; color: #d93025; font-size: 18px; }
.lib-confirm-box p { color: #5f6368; margin-bottom: 20px; font-size: 14px; }
.lib-confirm-actions { display: flex; justify-content: center; gap: 12px; }
.lib-io-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; flex: 1; min-height: 0; }
.lib-io-col { display: flex; flex-direction: column; gap: 10px; padding: 16px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e0e0e0; }
.lib-io-col h4 { margin: 0 0 8px 0; font-size: 14px; color: #202124; }
.lib-io-desc { font-size: 12px; color: #5f6368; margin-bottom: 8px; }
body.dark-theme #lib-color-picker { background-color: #202124; border-color: #5f6368; box-shadow: 0 4px 12px rgba(0,0,0,0.5); }
body.dark-theme .quicknav-modal { background-color: #202124; }
body.dark-theme .lib-header { background-color: #202124; border-bottom-color: #5f6368; }
body.dark-theme .lib-search-input { background-color: #303134; border-color: #5f6368; color: #e8eaed; }
body.dark-theme .lib-search-input:focus { background-color: #202124; border-color: #8ab4f8; }
body.dark-theme .lib-search-clear { color: #9aa0a6; }
body.dark-theme .lib-search-clear:hover { color: #e8eaed; }
body.dark-theme .lib-content-list { background-color: #171717; }
body.dark-theme .lib-card { background-color: #2d2e30; border-color: #5f6368; }
body.dark-theme .lib-card:hover, body.dark-theme .lib-card.active-tooltip-source { border-color: #8ab4f8; background-color: #303134; }
body.dark-theme .lib-card.focused { border-color: #8ab4f8; box-shadow: 0 0 0 2px rgba(138, 180, 248, 0.3); }
body.dark-theme .lib-card-title { color: #e8eaed; }
body.dark-theme .lib-icon-btn { color: #9aa0a6; }
body.dark-theme .lib-icon-btn:hover { background-color: #3c4043; color: #8ab4f8; }
body.dark-theme .lib-icon-btn.delete:hover { background-color: #410e0b; color: #f28b82; }
body.dark-theme .lib-card-tags span { background-color: #3c4043; color: #9aa0a6; }
body.dark-theme .lib-card-preview { color: #9aa0a6; }
body.dark-theme .lib-chip { background-color: #303134; color: #e8eaed; }
body.dark-theme .lib-chip:hover { background-color: #3c4043; }
body.dark-theme .lib-chip.active { background-color: #174ea6; color: #ffffff; }
body.dark-theme .lib-btn-secondary { border-color: #5f6368; color: #e8eaed; }
body.dark-theme .lib-btn-secondary:hover { background-color: #3c4043; }
body.dark-theme .lib-overlay { background-color: #202124; }
body.dark-theme .lib-editor-container h3 { color: #e8eaed; }
body.dark-theme .lib-input-full { background-color: #303134; border-color: #5f6368; color: #e8eaed; }
body.dark-theme .lib-textarea-full { background-color: #303134; border-color: #5f6368 !important; color: #e8eaed; }
body.dark-theme .lib-input-full:focus, body.dark-theme .lib-textarea-full:focus { border-color: #8ab4f8; background-color: #202124; }
body.dark-theme .lib-overlay-footer { border-top-color: #3c4043; }
body.dark-theme .lib-btn-text { color: #8ab4f8; }
body.dark-theme .lib-btn-separator::after { background-color: #9aa0a6; opacity: 0.3; }
body.dark-theme .lib-empty-state { color: #9aa0a6; }
body.dark-theme .lib-confirm-box { background-color: #2d2d2d; border-color: #5f6368; }
body.dark-theme .lib-confirm-box h3 { color: #f28b82; }
body.dark-theme .lib-confirm-box p { color: #e8eaed; }
body.dark-theme .lib-io-col { background-color: #303134; border-color: #5f6368; }
body.dark-theme .lib-io-col h4 { color: #e8eaed; }
body.dark-theme .lib-io-desc { color: #9aa0a6; }
body.dark-theme .lib-suggestions-menu { background-color: #202124; border-color: #5f6368; }
body.dark-theme .lib-suggestion-item { color: #e8eaed; }
body.dark-theme .lib-suggestion-item:hover { background-color: #3c4043; }
body.dark-theme .lib-suggestion-create-row { border-top-color: #3c4043; }
body.dark-theme .lib-suggestion-create-label { color: #9aa0a6; }
body.dark-theme .lib-content-list::-webkit-scrollbar-thumb, body.dark-theme .lib-tags-row::-webkit-scrollbar-thumb { background-color: #8ab4f8; }
body.dark-theme .lib-content-list::-webkit-scrollbar-thumb:hover, body.dark-theme .lib-tags-row::-webkit-scrollbar-thumb:hover { background-color: #aecbfa; }
`,
misc: `
.prompt-turn-highlight { border: 2px solid var(--ms-on-surface-variant, #9aa0a6) !important; z-index: 10 !important; border-radius: 12px; box-sizing: border-box !important; box-shadow: none !important; }
.response-turn-highlight { border: 2px solid var(--ms-primary, #8ab4f8) !important; z-index: 10 !important; border-radius: 12px; box-sizing: border-box !important; box-shadow: none !important; }
body.quicknav-resizing { cursor: ew-resize !important; user-select: none !important; }
.code-block-nav-container { display: flex; align-items: center; gap: 8px; margin-left: auto; }
.code-nav-button { background: transparent; border: 1px solid #669df6; color: #8ab4f8; height: 28px; border-radius: 8px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background-color 0.15s ease, transform 0.1s ease, border-color 0.15s ease, color 0.15s ease; padding: 0 12px; }
.code-nav-counter { font-family: 'Google Sans', sans-serif; font-size: 12px; font-weight: 500; color: #8ab4f8; user-select: none; }
.code-nav-button:hover:not(:disabled) { background-color: rgba(138, 180, 248, 0.08); }
.code-nav-button:active:not(:disabled) { transform: scale(0.95); }
.code-nav-button:disabled { opacity: 0.5; cursor: not-allowed; color: var(--ms-on-surface-variant, #9aa0a6); border-color: var(--ms-on-surface-variant, #9aa0a6); }
ms-prompt-box .text-wrapper, .prompt-box-container .text-wrapper { position: relative !important; }
ms-prompt-box textarea, .prompt-box-container textarea { resize: none !important; min-height: 40px !important; max-height: 90vh !important; overflow-y: auto !important; }
.quicknav-custom-resizer { position: absolute; top: 0; right: 0; width: 20px; height: 8px; background-color: #1a73e8; cursor: ns-resize; z-index: 100; clip-path: polygon(0 0, 100% 0, 100% 100%); opacity: 0.3; transition: opacity 0.2s, transform 0.1s; }
.quicknav-custom-resizer:hover { opacity: 0.9; transform: scale(1.1); }
.quicknav-toast { position: absolute; top: -40px; left: 50%; transform: translateX(-50%); background-color: #202124; color: #fff; padding: 6px 12px; border-radius: 4px; font-family: 'Google Sans', sans-serif; font-size: 13px; pointer-events: none; opacity: 0; z-index: 200; box-shadow: 0 2px 4px rgba(0,0,0,0.2); white-space: nowrap; }
.quicknav-toast.show { animation: quicknav-fade-in-out 2s forwards; }
body.dark-theme .quicknav-toast { background-color: #e8eaed; color: #202124; }
html.quicknav-font-active .turn-content .code-preview-text .code-preview-line,
html.quicknav-font-active .turn-content .code-preview-text,
.code-preview-text,
.code-preview-line { font-family: 'Roboto Mono', monospace !important; font-size: 11px !important; line-height: 14px !important; }
.code-preview-text { display: flex; flex-direction: column; align-items: flex-start; justify-content: center; flex: 1; flex-shrink: 1; margin: 0 12px 0 50px; max-height: 34px; cursor: help; pointer-events: auto; overflow: hidden; color: #1a73e8 !important; min-width: 0; z-index: 1; position: relative; }
.code-preview-line { font-weight: 500 !important; max-width: 100%; width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: pre !important; display: block; }
body.dark-theme .code-preview-text { color: #8ab4f8 !important; }
html.quicknav-font-active .code-block-nav-container .code-nav-counter,
.code-nav-counter { font-size: 12px !important; line-height: 1.5 !important; }
html.quicknav-font-active .code-block-nav-container .code-nav-button,
.code-nav-button { font-size: 14px !important; }
`,
triggers: `
@keyframes quicknav-moon-sway { 0% { transform: rotate(-10deg); } 50% { transform: rotate(10deg); } 100% { transform: rotate(-10deg); } }
@keyframes quicknav-sun-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
.quicknav-icon-sun { transform-origin: center center; }
#quicknav-theme-trigger:hover .quicknav-icon-sun { animation: quicknav-sun-spin 12s linear infinite !important; }
.quicknav-icon-moon { transform-origin: center 30%; transform: rotate(-10deg); }
#quicknav-theme-trigger:hover .quicknav-icon-moon { animation: quicknav-moon-sway 4s ease-in-out infinite !important; }
#quicknav-theme-trigger:hover .material-symbols-outlined { filter: none !important; background-color: transparent !important; box-shadow: none !important; }
`
};
const styleSheet = document.createElement("style");
styleSheet.id = 'chat-nav-styles';
styleSheet.textContent = Object.values(cssModules).join('\n');
document.head.appendChild(styleSheet);
},
waitForElement(selector, callback) {
const existing = document.querySelector(selector);
if (existing) {
callback(existing);
return;
}
const observer = new MutationObserver((mutations, obs) => {
const element = document.querySelector(selector);
if (element) {
obs.disconnect();
if (this.activeObservers) this.activeObservers.delete(obs);
callback(element);
}
});
observer.observe(document.body, { childList: true, subtree: true });
if (this.activeObservers) this.activeObservers.add(observer);
},
toggleSettingsMenu(e, forceState = null) {
e.stopPropagation();
const dropdown = document.getElementById('quicknav-settings-dropdown');
const trigger = e.currentTarget;
if (dropdown && trigger) {
const isVisible = dropdown.classList.contains('visible');
const shouldOpen = forceState !== null ? forceState : !isVisible;
if (!shouldOpen) {
dropdown.classList.remove('visible');
trigger.classList.remove('active');
} else {
StyleManager.updateUIDisplay();
dropdown.classList.add('visible');
trigger.classList.add('active');
const rect = trigger.getBoundingClientRect();
const menuWidth = dropdown.offsetWidth;
const menuHeight = dropdown.offsetHeight;
const viewportHeight = window.innerHeight;
let leftPos = rect.right - menuWidth;
if (leftPos < 4) leftPos = rect.left;
if (leftPos + menuWidth > window.innerWidth) leftPos = window.innerWidth - menuWidth - 10;
const spaceBelow = viewportHeight - rect.bottom;
const spaceAbove = rect.top;
if (spaceBelow > menuHeight || spaceBelow > spaceAbove) {
dropdown.style.top = `${rect.bottom + 6}px`;
dropdown.style.bottom = 'auto';
} else {
dropdown.style.top = 'auto';
dropdown.style.bottom = `${viewportHeight - rect.top + 6}px`;
}
dropdown.style.left = `${leftPos}px`;
}
}
},
closeSettingsMenu(e) {
const dropdown = document.getElementById('quicknav-settings-dropdown');
const trigger = document.querySelector('.quicknav-dropdown-trigger');
if (dropdown && dropdown.classList.contains('visible')) {
if (!dropdown.contains(e.target) && (!trigger || !trigger.contains(e.target))) {
dropdown.classList.remove('visible');
if (trigger) trigger.classList.remove('active');
}
}
},
// Creates and injects main navigation interface components into DOM
createAndInjectUI() {
if (!document.getElementById('quicknav-header-styles')) {
const style = document.createElement('style');
style.id = 'quicknav-header-styles';
style.textContent = `
#quicknav-theme-trigger { border: 1px solid transparent !important; transition: color 0.2s ease, opacity 0.2s ease; color: var(--ms-on-surface-variant, #5f6368); opacity: 0.5; margin: 0 auto; }
#quicknav-theme-trigger:hover { opacity: 1; }
body:not(.dark-theme) #quicknav-theme-trigger:hover { background-color: transparent !important; border-color: transparent !important; box-shadow: none !important; filter: none !important; color: #174ea6 !important; cursor: pointer; }
body.dark-theme #quicknav-theme-trigger:hover { background-color: transparent !important; border-color: transparent !important; box-shadow: none !important; color: #aecbfa !important; filter: drop-shadow(0 0 5px rgba(174, 203, 250, 0.5)) !important; cursor: pointer; }
.quicknav-icon-sun { transform-origin: center center; }
#quicknav-theme-trigger:hover .quicknav-icon-sun { animation: quicknav-sun-spin 12s linear infinite; }
.quicknav-icon-moon { transform-origin: center 30%; }
#quicknav-theme-trigger:hover .quicknav-icon-moon { animation: quicknav-moon-sway 1.5s ease-in-out infinite; }
#quicknav-theme-trigger:hover .material-symbols-outlined { filter: none !important; background-color: transparent !important; box-shadow: none !important; }
`;
document.head.appendChild(style);
}
const navContainer = document.createElement('div');
navContainer.id = 'chat-nav-container';
const createButton = (id, title, pathData) => {
const button = document.createElement('button');
button.id = id;
button.className = 'chat-nav-button';
button.title = title;
button.setAttribute('aria-label', title);
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute('height', '24px');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', '24px');
svg.setAttribute('fill', 'currentColor');
const path = document.createElementNS("http://www.w3.org/2000/svg", 'path');
path.setAttribute('d', pathData);
svg.appendChild(path);
button.appendChild(svg);
return button;
};
const btnTop = createButton('nav-top', 'Go to the first message (Shift + Alt + PgUp)', 'M12 4l-6 6 1.41 1.41L12 6.83l4.59 4.58L18 10z M12 12l-6 6 1.41 1.41L12 14.83l4.59 4.58L18 18z');
const btnUp = createButton('nav-up', 'Go to the previous message (Alt + PgUp)', 'M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z');
const btnDown = createButton('nav-down', 'Go to the next message (Alt + PgDown)', 'M12 16l-6-6 1.41-1.41L12 13.17l4.59-4.58L18 10z');
const btnBottom = createButton('nav-bottom', 'Go to the last message (Shift + Alt + PgDown)', 'M12 12l-6-6 1.41-1.41L12 9.17l4.59-4.58L18 6z M12 20l-6-6 1.41-1.41L12 17.17l4.59-4.58L18 14z');
const counterWrapper = document.createElement('div');
counterWrapper.className = 'counter-wrapper';
const counter = document.createElement('span');
counter.id = 'chat-nav-counter';
counter.title = 'Open navigation menu (Alt + M)';
counter.tabIndex = 0;
counter.setAttribute('role', 'button');
const currentNumSpan = document.createElement('span');
currentNumSpan.id = 'chat-nav-current-num';
const separatorSpan = document.createElement('span');
separatorSpan.id = 'chat-nav-separator';
separatorSpan.textContent = ' / ';
const totalNumSpan = document.createElement('span');
totalNumSpan.id = 'chat-nav-total-num';
counter.append(currentNumSpan, separatorSpan, totalNumSpan);
counterWrapper.appendChild(counter);
let menuContainer = document.getElementById('chat-nav-menu-container');
if (!menuContainer) {
menuContainer = document.createElement('div');
menuContainer.id = 'chat-nav-menu-container';
menuContainer.tabIndex = -1;
menuContainer.setAttribute('role', 'menu');
const resizerLeft = document.createElement('div');
resizerLeft.className = 'quicknav-menu-resizer quicknav-resizer-left';
resizerLeft.title = 'Drag to resize';
const resizerRight = document.createElement('div');
resizerRight.className = 'quicknav-menu-resizer quicknav-resizer-right';
resizerRight.title = 'Drag to resize';
menuContainer.append(resizerLeft, resizerRight);
this.setupMenuResizer(resizerLeft, -1, menuContainer);
this.setupMenuResizer(resizerRight, 1, menuContainer);
document.body.appendChild(menuContainer);
}
const settingsWrapper = document.createElement('div');
settingsWrapper.className = 'quicknav-settings-wrapper';
const btnLib = createButton('nav-action-lib', 'Toggle Prompt Library (Alt + L)', 'M3 7h26v1H3z M3 12h26v1H3z M3 17h26v1H3z');
btnLib.classList.add('quicknav-resize-btn', 'lib-btn-separator');
btnLib.style.setProperty('color', '#8ab4f8', 'important');
btnLib.style.width = '44px';
btnLib.style.borderRadius = '8px';
const libSvg = btnLib.querySelector('svg');
if (libSvg) { libSvg.setAttribute('viewBox', '0 0 32 24'); libSvg.setAttribute('width', '32px'); }
btnLib.addEventListener('click', () => PromptLibrary.toggle());
let libHoverTimeout;
btnLib.addEventListener('mouseenter', () => {
btnLib.style.setProperty('filter', 'drop-shadow(0 0 3px #8ab4f8)', 'important');
if (!StyleManager.HOVER_MENU.enabled) return;
clearTimeout(libHoverTimeout);
libHoverTimeout = setTimeout(() => {
if (!document.getElementById('quicknav-lib-backdrop')?.classList.contains('visible')) {
PromptLibrary.open();
}
}, StyleManager.HOVER_DELAY.current);
});
btnLib.addEventListener('mouseleave', () => {
btnLib.style.removeProperty('filter');
clearTimeout(libHoverTimeout);
});
const btnClear = createButton('nav-action-clear', 'Double-click to clear prompt (Alt + Shift + Backspace)', 'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z');
btnClear.classList.add('quicknav-resize-btn');
btnClear.addEventListener('click', () => PromptActions.clear(false));
btnClear.addEventListener('dblclick', () => PromptActions.clear(true));
btnClear.addEventListener('mouseenter', () => btnClear.style.setProperty('color', '#d93025', 'important'));
btnClear.addEventListener('mouseleave', () => btnClear.style.removeProperty('color'));
const btnCopy = createButton('nav-action-copy', 'Copy prompt (Alt + C)', 'M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z');
btnCopy.classList.add('quicknav-resize-btn');
btnCopy.addEventListener('click', () => PromptActions.copy());
const btnPaste = createButton('nav-action-paste', "Paste to prompt (Alt + V)", 'M19 2h-4.18C14.4.84 13.3 0 12 0c-1.3 0-2.4.84-2.82 2H5c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-7 0c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zm7 18H5V4h2v3h10V4h2v16z');
btnPaste.classList.add('quicknav-resize-btn');
btnPaste.addEventListener('click', () => PromptActions.paste());
const leftWrapper = document.createElement('div');
leftWrapper.className = 'quicknav-left-wrapper';
leftWrapper.append(btnLib, btnClear, btnCopy, btnPaste);
const btnExpandDown = createButton('nav-expand-down', 'Decrease prompt height (Alt + Down)', 'M4 20h16v2H4z');
btnExpandDown.classList.add('quicknav-resize-btn');
const btnExpandUp = createButton('nav-expand-up', 'Increase prompt height (Alt + Up)', 'M4 2h16v2H4z');
btnExpandUp.classList.add('quicknav-resize-btn');
const resizeGroup = document.createElement('div');
resizeGroup.style.display = 'flex';
resizeGroup.style.gap = '0px';
resizeGroup.append(btnExpandDown, btnExpandUp);
settingsWrapper.appendChild(resizeGroup);
navContainer.append(leftWrapper, btnTop, btnUp, counterWrapper, btnDown, btnBottom, settingsWrapper);
this.navContainerElement = navContainer;
const headerBtn = document.createElement('button');
headerBtn.id = 'quicknav-header-trigger';
headerBtn.className = 'quicknav-dropdown-trigger quicknav-header-sep';
headerBtn.title = 'QuickNav Settings';
headerBtn.style.cssText = 'height: 40px; width: 40px; margin: 0 4px 0 0; border-radius: 50%;';
const hSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
hSvg.setAttribute('height', '24px'); hSvg.setAttribute('viewBox', '0 0 24 24'); hSvg.setAttribute('width', '24px'); hSvg.setAttribute('fill', 'var(--ms-primary, #8ab4f8)');
const hPath = document.createElementNS("http://www.w3.org/2000/svg", 'path');
hPath.setAttribute('d', 'M6 10.5c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm12 0c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5zm-6 0c-.83 0-1.5.67-1.5 1.5s.67 1.5 1.5 1.5 1.5-.67 1.5-1.5-.67-1.5-1.5-1.5z');
hSvg.appendChild(hPath);
headerBtn.appendChild(hSvg);
let settingsOpenTimeout;
headerBtn.addEventListener('click', this.toggleSettingsMenu.bind(this));
headerBtn.addEventListener('mouseenter', (e) => {
clearTimeout(this.settingsCloseTimeout);
if (StyleManager.HOVER_MENU.enabled) {
clearTimeout(settingsOpenTimeout);
settingsOpenTimeout = setTimeout(() => {
const mockEvent = {
stopPropagation: () => {},
currentTarget: headerBtn,
target: headerBtn
};
this.toggleSettingsMenu(mockEvent, true);
}, StyleManager.HOVER_DELAY.current);
}
});
headerBtn.addEventListener('mouseleave', () => {
clearTimeout(settingsOpenTimeout);
if (StyleManager.HOVER_MENU.enabled) {
this.settingsCloseTimeout = setTimeout(() => {
const dropdown = document.getElementById('quicknav-settings-dropdown');
if (dropdown) dropdown.classList.remove('visible');
headerBtn.classList.remove('active');
}, 300);
}
});
const favBtn = document.createElement('button');
favBtn.id = 'quicknav-fav-trigger';
favBtn.className = 'quicknav-dropdown-trigger';
favBtn.title = 'Favorites (Shift + Alt + F)';
favBtn.style.cssText = 'height: 40px; width: 40px; margin: 0 8px 0 0; border-radius: 50%;';
let favoritesMenu = document.getElementById('quicknav-favorites-menu');
if (!favoritesMenu) {
favoritesMenu = document.createElement('div');
favoritesMenu.id = 'quicknav-favorites-menu';
favoritesMenu.className = 'quicknav-dropdown-menu';
favoritesMenu.style.width = '450px';
favoritesMenu.style.visibility = 'hidden';
favoritesMenu.style.opacity = '0';
favoritesMenu.style.pointerEvents = 'none';
document.body.appendChild(favoritesMenu);
}
const updateFavPosition = () => {
if (!favoritesMenu.classList.contains('visible')) return;
const rect = favBtn.getBoundingClientRect();
const spaceBelow = window.innerHeight - rect.bottom - 10;
favoritesMenu.style.maxHeight = `${Math.max(100, spaceBelow)}px`;
favoritesMenu.style.top = `${rect.bottom + 6}px`;
favoritesMenu.style.left = `${rect.right - 450}px`;
};
window.addEventListener('resize', () => requestAnimationFrame(updateFavPosition));
let favOpenTimer = null;
const closeFavMenu = (e) => {
if (favoritesMenu && favBtn && !favoritesMenu.contains(e.target) && !favBtn.contains(e.target)) {
toggleFavMenu(false);
}
};
const toggleFavMenu = (forceState = null) => {
const isVisible = favoritesMenu.classList.contains('visible');
const shouldOpen = forceState !== null ? forceState : !isVisible;
if (shouldOpen) {
FavoritesManager.onOpen();
favoritesMenu.classList.add('visible');
favoritesMenu.style.visibility = 'visible';
favoritesMenu.style.opacity = '1';
favoritesMenu.style.pointerEvents = 'auto';
favBtn.classList.add('active');
updateFavPosition();
setTimeout(() => document.addEventListener('click', closeFavMenu, true), 0);
} else {
FavoritesManager.onClose();
favoritesMenu.classList.remove('visible');
favoritesMenu.style.visibility = 'hidden';
favoritesMenu.style.opacity = '0';
favoritesMenu.style.pointerEvents = 'none';
favBtn.classList.remove('active');
clearTimeout(favOpenTimer);
document.removeEventListener('click', closeFavMenu, true);
}
};
favBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
toggleFavMenu();
});
favBtn.addEventListener('mouseenter', () => {
if (!StyleManager.HOVER_MENU.enabled) return;
if (!favoritesMenu.classList.contains('visible')) {
clearTimeout(favOpenTimer);
favOpenTimer = setTimeout(() => {
toggleFavMenu(true);
}, StyleManager.HOVER_DELAY.current);
}
});
favBtn.addEventListener('mouseleave', () => {
clearTimeout(favOpenTimer);
});
favoritesMenu.addEventListener('mouseenter', () => {
clearTimeout(favOpenTimer);
});
const themeBtn = document.createElement('button');
themeBtn.id = 'quicknav-theme-trigger';
themeBtn.className = 'quicknav-dropdown-trigger';
themeBtn.style.cssText = 'height: 40px; width: 40px; border-radius: 50%; color: var(--ms-primary, #8ab4f8);';
themeBtn.onclick = () => ThemeManager.toggle();
let dropdownMenu = document.getElementById('quicknav-settings-dropdown');
if (!dropdownMenu) {
dropdownMenu = document.createElement('div');
dropdownMenu.id = 'quicknav-settings-dropdown';
dropdownMenu.className = 'quicknav-dropdown-menu';
dropdownMenu.appendChild(StyleManager.createControls());
document.body.appendChild(dropdownMenu);
}
dropdownMenu.addEventListener('mouseenter', () => clearTimeout(this.settingsCloseTimeout));
dropdownMenu.addEventListener('mouseleave', () => {
if (StyleManager.HOVER_MENU.enabled) {
this.settingsCloseTimeout = setTimeout(() => {
dropdownMenu.classList.remove('visible');
headerBtn.classList.remove('active');
}, 300);
}
});
const injectNavBar = (anchor) => {
if (document.getElementById('chat-nav-container')) return;
if (anchor && anchor.parentNode) {
anchor.parentNode.insertBefore(navContainer, anchor);
this.attachNavigationLogic(btnTop, btnUp, btnDown, btnBottom, counter, menuContainer);
this.updateCounterDisplay();
navContainer.addEventListener('mouseenter', () => {
this.isHoveringOnNav = true;
this.updateScrollPercentage();
this.showBadge();
});
navContainer.addEventListener('mouseleave', () => {
this.isHoveringOnNav = false;
this.hideBadge(1000);
});
}
};
const ensureHeaderTriggers = () => {
const toolbarRight = document.querySelector('ms-playground-toolbar .toolbar-right, ms-toolbar .toolbar-right');
if (!toolbarRight) return;
if (!headerBtn.isConnected || !favBtn.isConnected || !themeBtn.isConnected) {
['quicknav-header-trigger', 'quicknav-fav-trigger', 'quicknav-theme-trigger'].forEach(id => {
const el = document.getElementById(id);
if (el) el.remove();
});
toolbarRight.insertBefore(headerBtn, toolbarRight.firstChild);
toolbarRight.insertBefore(favBtn, headerBtn);
const toolbarContainer = toolbarRight.parentElement;
if (toolbarContainer) {
toolbarContainer.insertBefore(themeBtn, toolbarRight);
}
FavoritesManager.updateButtonState();
ThemeManager.updateState();
}
};
this.waitForElement('ms-chunk-editor footer, section.chunk-editor-main > footer', injectNavBar);
this.waitForElement('ms-prompt-box', (inputWrapper) => {
if (!document.getElementById('chat-nav-container')) injectNavBar(inputWrapper);
});
this.waitForElement('ms-playground-toolbar .toolbar-right, ms-toolbar .toolbar-right', (toolbarRight) => {
ensureHeaderTriggers();
const observer = new MutationObserver(() => ensureHeaderTriggers());
observer.observe(toolbarRight, { childList: true });
if (this.activeObservers) this.activeObservers.add(observer);
});
},
createButton(id, title, pathData) {
const button = document.createElement('button');
button.id = id;
button.className = 'chat-nav-button';
button.title = title;
button.setAttribute('aria-label', title);
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute('height', '24px');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', '24px');
svg.setAttribute('fill', 'currentColor');
const path = document.createElementNS("http://www.w3.org/2000/svg", 'path');
path.setAttribute('d', pathData);
svg.appendChild(path);
button.appendChild(svg);
return button;
},
attachNavigationLogic(btnTop, btnUp, btnDown, btnBottom, counter, menuContainer) {
const lastIndex = () => ChatController.allTurns.length - 1;
ChatController.setupHoldableButton(btnTop, () => {
ChatController.scrollToAbsoluteTop();
});
ChatController.setupHoldableButton(btnUp, async () => {
const last = lastIndex();
const scrollContainer = document.querySelector('ms-autoscroll-container');
if (ChatController.currentIndex === last && last > -1 && scrollContainer) {
const currentTurn = ChatController.allTurns[last];
const isScrolledPastTop = scrollContainer.scrollTop > currentTurn.offsetTop + 10;
if (isScrolledPastTop) {
await ChatController.navigateToIndex(last, 'start');
return;
}
}
if (ChatController.currentIndex === 0) {
ChatController.scrollToAbsoluteTop();
return;
}
await ChatController.navigateToIndex(ChatController.currentIndex - 1, 'center');
});
ChatController.setupHoldableButton(btnDown, async () => {
const last = lastIndex();
if (ChatController.currentIndex < last) {
await ChatController.navigateToIndex(ChatController.currentIndex + 1, 'center');
} else {
const scrollContainer = document.querySelector('ms-autoscroll-container');
if (!scrollContainer) return;
if (ChatController.isDownButtonAtEndToggle) {
await ChatController.navigateToIndex(last, 'center');
ChatController.isDownButtonAtEndToggle = false;
} else {
UIManager.hideBadge(0, true);
ChatController.isScrollingProgrammatically = true;
scrollContainer.scrollTo({ top: scrollContainer.scrollHeight, behavior: 'smooth' });
setTimeout(() => {
ChatController.isScrollingProgrammatically = false;
UIManager.showBadge();
UIManager.updateScrollPercentage();
UIManager.hideBadge(3000);
ChatController.focusChatContainer();
}, 800);
ChatController.isDownButtonAtEndToggle = true;
}
}
});
let ignoreNextBottomClick = false;
const bottomNavAction = async () => {
UIManager.hideBadge(0, true);
const last = lastIndex();
if (ChatController.currentIndex === last) {
const scrollContainer = document.querySelector('ms-autoscroll-container');
if (scrollContainer) {
const isSmooth = !ChatController.isBottomHoldActive;
ChatController.isScrollingProgrammatically = true;
scrollContainer.scrollTo({ top: scrollContainer.scrollHeight, behavior: isSmooth ? 'smooth' : 'auto' });
setTimeout(() => {
ChatController.isScrollingProgrammatically = false;
UIManager.showBadge();
UIManager.updateScrollPercentage();
UIManager.hideBadge(3000);
ChatController.focusChatContainer();
}, isSmooth ? 800 : 50);
}
} else {
await ChatController.navigateToIndex(last, 'center');
}
};
btnBottom.addEventListener('mousedown', (e) => {
if (ChatController.isBottomHoldActive) return;
ChatController.bottomHoldTimeout = setTimeout(() => {
ChatController.isBottomHoldActive = true;
btnBottom.classList.add('auto-click-active');
ignoreNextBottomClick = true;
bottomNavAction();
ChatController.bottomHoldInterval = setInterval(bottomNavAction, 150);
}, 750);
});
const stopBottomHoldDetector = () => { clearTimeout(ChatController.bottomHoldTimeout); };
btnBottom.addEventListener('mouseup', stopBottomHoldDetector);
btnBottom.addEventListener('mouseleave', stopBottomHoldDetector);
btnBottom.addEventListener('click', (e) => {
if (ignoreNextBottomClick) {
ignoreNextBottomClick = false;
return;
}
if (ChatController.isBottomHoldActive) {
ChatController.stopBottomHold();
} else {
if (e.target.closest('#nav-bottom')) {
bottomNavAction();
}
}
});
counter.addEventListener('click', (e) => { e.stopPropagation(); this.toggleNavMenu(); });
let hoverOpenTimeout = null;
const handleHoverOpen = () => {
if (!StyleManager.HOVER_MENU.enabled) return;
if (!menuContainer.classList.contains('visible')) {
clearTimeout(hoverOpenTimeout);
hoverOpenTimeout = setTimeout(() => {
this.toggleNavMenu();
}, StyleManager.HOVER_DELAY.current);
}
};
counter.addEventListener('mouseenter', handleHoverOpen);
counter.addEventListener('mouseleave', () => {
clearTimeout(hoverOpenTimeout);
});
menuContainer.addEventListener('mouseenter', () => {
clearTimeout(hoverOpenTimeout);
});
counter.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
this.toggleNavMenu();
} else if (e.key === 'Tab' && menuContainer.classList.contains('visible')) {
e.preventDefault();
const menuList = document.getElementById('chat-nav-menu');
const donateBtn = document.getElementById('chat-nav-donate-link');
if (e.shiftKey) {
if (donateBtn) donateBtn.focus();
} else {
const searchInput = menuContainer.querySelector('.chat-nav-filter-input');
if (searchInput) searchInput.focus();
else if (menuList) menuList.focus();
}
}
});
menuContainer.addEventListener('keydown', (e) => {
const menuList = document.getElementById('chat-nav-menu');
const loadBtn = document.getElementById('chat-nav-load-button');
const donateBtn = document.getElementById('chat-nav-donate-link');
const searchInput = menuContainer.querySelector('.chat-nav-filter-input');
const activeEl = document.activeElement;
if (e.key === 'Tab') {
e.preventDefault();
if (e.shiftKey) {
if (activeEl === menuList) searchInput ? searchInput.focus() : counter.focus();
else if (activeEl === searchInput) counter.focus();
else if (activeEl === loadBtn) menuList.focus();
else if (activeEl === donateBtn) loadBtn.focus();
} else {
if (activeEl === menuList) loadBtn.focus();
else if (activeEl === searchInput) menuList.focus();
else if (activeEl === loadBtn) donateBtn.focus();
else if (activeEl === donateBtn) counter.focus();
}
return;
}
const items = menuContainer.querySelectorAll('.chat-nav-menu-item');
let newIndex = ChatController.menuFocusedIndex;
let shouldUpdateFocus = true;
const findNextVisible = (start, dir) => {
let probe = start;
const len = items.length;
for (let i = 0; i < len; i++) {
probe = (probe + dir + len) % len;
if (items[probe].style.display !== 'none') return probe;
}
return start;
};
const findJumpVisible = (start, dir, jump) => {
let probe = start;
const len = items.length;
let count = 0;
while (count < jump) {
probe = probe + dir;
if (probe < 0) { probe = 0; break; }
if (probe >= len) { probe = len - 1; break; }
if (items[probe].style.display !== 'none') count++;
}
while (probe >= 0 && probe < len && items[probe].style.display === 'none') {
probe += dir;
}
return Math.max(0, Math.min(probe, len - 1));
};
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
if (items.length > 0) newIndex = findNextVisible(Math.max(0, ChatController.menuFocusedIndex), 1);
break;
case 'ArrowUp':
e.preventDefault();
if (items.length > 0) newIndex = findNextVisible(Math.max(0, ChatController.menuFocusedIndex), -1);
break;
case 'PageDown':
e.preventDefault();
if (items.length > 0) newIndex = findJumpVisible(Math.max(0, ChatController.menuFocusedIndex), 1, ChatController.JUMP_DISTANCE);
break;
case 'PageUp':
e.preventDefault();
if (items.length > 0) newIndex = findJumpVisible(Math.max(0, ChatController.menuFocusedIndex), -1, ChatController.JUMP_DISTANCE);
break;
case 'Home':
e.preventDefault();
if (items.length > 0) {
newIndex = 0;
if (items[newIndex].style.display === 'none') newIndex = findNextVisible(newIndex, 1);
}
break;
case 'End':
e.preventDefault();
if (items.length > 0) {
newIndex = items.length - 1;
if (items[newIndex].style.display === 'none') newIndex = findNextVisible(newIndex, -1);
}
break;
case 'Enter':
e.preventDefault();
if (activeEl && activeEl.matches('button, a[href]') && menuContainer.contains(activeEl)) {
activeEl.click();
} else if (ChatController.menuFocusedIndex !== -1 && items[ChatController.menuFocusedIndex]) {
items[ChatController.menuFocusedIndex].click();
}
shouldUpdateFocus = false;
break;
case 'Escape': e.preventDefault(); this.toggleNavMenu(); shouldUpdateFocus = false; break;
default: shouldUpdateFocus = false; break;
}
if (shouldUpdateFocus && newIndex !== ChatController.menuFocusedIndex) { this.updateMenuFocus(items, newIndex); }
});
},
_handleTurnMouseEnter() {
if (this.listenedTurnElement) {
this._cachedTurnRect = this.listenedTurnElement.getBoundingClientRect();
}
},
_handleTurnMouseMove(e) {
if (this._isMouseMovePending) return;
this._isMouseMovePending = true;
requestAnimationFrame(() => {
if (this.listenedTurnElement && this._cachedTurnRect) {
const hotzoneWidth = 24;
const rect = this._cachedTurnRect;
const cursorX = e.clientX;
if (cursorX >= rect.left && cursorX <= rect.left + hotzoneWidth ||
cursorX >= rect.right - hotzoneWidth && cursorX <= rect.right) {
this.showBadge();
this.updateScrollPercentage();
} else {
this.hideBadge(1000);
}
}
this._isMouseMovePending = false;
});
},
_handleTurnMouseLeave() {
this.hideBadge(1000);
this._cachedTurnRect = null;
},
updateHighlight(oldIndex, newIndex) {
if (oldIndex > -1 && oldIndex < ChatController.allTurns.length) {
const oldTurn = ChatController.allTurns[oldIndex];
const oldTurnContainer = oldTurn.querySelector('.chat-turn-container');
if (oldTurnContainer) {
oldTurnContainer.classList.remove('prompt-turn-highlight', 'response-turn-highlight');
}
}
if (this.listenedTurnElement) {
this.listenedTurnElement.removeEventListener('mouseenter', this._boundTurnMouseEnter);
this.listenedTurnElement.removeEventListener('mousemove', this._handleTurnMouseMove);
this.listenedTurnElement.removeEventListener('mouseleave', this._handleTurnMouseLeave);
this.listenedTurnElement = null;
this._cachedTurnRect = null;
}
const floater = document.getElementById('quicknav-badge-floater');
if (!floater) return;
if (newIndex > -1 && newIndex < ChatController.allTurns.length) {
const newTurn = ChatController.allTurns[newIndex];
const isPrompt = ChatController.isUserPrompt(newTurn);
const newContainer = newTurn.querySelector('.chat-turn-container');
if (newContainer) {
newContainer.classList.add(isPrompt ? 'prompt-turn-highlight' : 'response-turn-highlight');
}
const badgeIndex = document.getElementById('quicknav-badge-index');
const badgeClass = isPrompt ? 'prompt-badge-bg' : 'response-badge-bg';
if (badgeIndex) {
badgeIndex.textContent = newIndex + 1;
}
floater.className = badgeClass;
this.listenedTurnElement = newTurn;
if (!this._boundTurnMouseEnter) {
this._boundTurnMouseEnter = this._handleTurnMouseEnter.bind(this);
}
this.listenedTurnElement.addEventListener('mouseenter', this._boundTurnMouseEnter);
this.listenedTurnElement.addEventListener('mousemove', this._handleTurnMouseMove);
this.listenedTurnElement.addEventListener('mouseleave', this._handleTurnMouseLeave);
this._handleTurnMouseEnter();
}
},
updateCounterDisplay() {
let currentNumSpan = document.getElementById('chat-nav-current-num');
let totalNumSpan = document.getElementById('chat-nav-total-num');
if (!currentNumSpan || !totalNumSpan) return;
const current = ChatController.currentIndex > -1 ? ChatController.currentIndex + 1 : '-';
const total = ChatController.allTurns.length;
currentNumSpan.textContent = current;
totalNumSpan.textContent = total;
currentNumSpan.classList.remove('chat-nav-current-grey', 'chat-nav-current-blue');
if (ChatController.currentIndex === total - 1 && total > 0) {
currentNumSpan.classList.add('chat-nav-current-blue');
} else {
currentNumSpan.classList.add('chat-nav-current-grey');
}
},
toggleNavMenu() {
const menuContainer = document.getElementById('chat-nav-menu-container');
const counter = document.getElementById('chat-nav-counter');
if (!menuContainer || !counter) return;
const isVisible = menuContainer.classList.contains('visible');
if (isVisible) {
menuContainer.classList.remove('visible');
const menuList = document.getElementById('chat-nav-menu');
if (menuList) menuList.tabIndex = -1;
counter.setAttribute('aria-expanded', 'false');
document.removeEventListener('click', this.closeNavMenu, true);
ChatController.stopDynamicMenuLoading();
ChatController.focusChatContainer();
if (this.customTooltip) {
this.customTooltip.style.opacity = '0';
this.customTooltip.style.pointerEvents = 'none';
}
} else {
this.populateNavMenu();
const newMenuList = document.getElementById('chat-nav-menu');
if (newMenuList) newMenuList.tabIndex = 0;
const savedWidth = StyleManager._read('quicknav_menu_custom_width');
if (savedWidth) {
menuContainer.style.width = `${savedWidth}px`;
} else {
const chatContainer = document.querySelector('ms-chunk-editor');
if (chatContainer) {
const chatWidth = chatContainer.clientWidth;
const finalWidth = Math.max(300, Math.min(chatWidth, 800));
menuContainer.style.width = `${finalWidth}px`;
}
}
const counterRect = counter.getBoundingClientRect();
menuContainer.style.bottom = `${window.innerHeight - counterRect.top + 8}px`;
menuContainer.style.left = `${counterRect.left + (counterRect.width / 2)}px`;
menuContainer.style.transform = 'translateX(-50%)';
const availableSpace = counterRect.top - 18;
menuContainer.style.maxHeight = `${availableSpace}px`;
this._proactivelySyncMenuScroll();
menuContainer.classList.add('visible');
counter.setAttribute('aria-expanded', 'true');
const items = newMenuList ? newMenuList.querySelectorAll('.chat-nav-menu-item') : [];
const initialFocusIndex = ChatController.currentIndex > -1 ? ChatController.currentIndex : 0;
this.updateMenuFocus(items, initialFocusIndex, true);
const searchInput = menuContainer.querySelector('.chat-nav-filter-input');
if (searchInput) {
searchInput.focus();
} else if (newMenuList) {
newMenuList.focus();
}
setTimeout(() => document.addEventListener('click', this.closeNavMenu, true), 0);
ChatController.scheduleEnrichment();
}
},
setupMenuResizer(handle, direction, container) {
let startX, startWidth, rafId;
const onMouseMove = (e) => {
if (rafId) return;
rafId = requestAnimationFrame(() => {
const currentX = e.clientX;
const deltaX = (currentX - startX) * direction;
let newWidth = startWidth + (deltaX * 2);
const maxWidth = window.innerWidth - 32;
newWidth = Math.max(300, Math.min(newWidth, maxWidth));
container.style.width = `${newWidth}px`;
container.style.maxWidth = '100vw';
rafId = null;
});
};
const onMouseUp = () => {
if (rafId) cancelAnimationFrame(rafId);
rafId = null;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
document.body.style.cursor = '';
document.body.classList.remove('quicknav-resizing');
const currentWidth = parseInt(container.style.width, 10);
if (currentWidth) {
StyleManager._write('quicknav_menu_custom_width', currentWidth);
}
};
handle.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
e.preventDefault();
e.stopPropagation();
startX = e.clientX;
startWidth = container.getBoundingClientRect().width;
document.body.style.cursor = 'ew-resize';
document.body.classList.add('quicknav-resizing');
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
},
closeNavMenu(e) {
const menuContainer = document.getElementById('chat-nav-menu-container');
const counter = document.getElementById('chat-nav-counter');
if (menuContainer && counter && menuContainer.classList.contains('visible')) {
const isClickInsideMenu = menuContainer.contains(e.target);
const isClickOnCounter = counter.contains(e.target);
const isClickOnTooltip = this.customTooltip && this.customTooltip.contains(e.target);
if (!isClickInsideMenu && !isClickOnCounter && !isClickOnTooltip) {
this.toggleNavMenu();
}
}
},
_proactivelySyncMenuScroll() {
const menuList = document.getElementById('chat-nav-menu');
if (!menuList) return;
const items = menuList.querySelectorAll('.chat-nav-menu-item');
const targetIndex = ChatController.currentIndex;
if (targetIndex < 0 || targetIndex >= items.length) return;
const focusedItem = items[targetIndex];
if (focusedItem) {
menuList.style.scrollBehavior = 'auto';
menuList.scrollTop = focusedItem.offsetTop - (menuList.clientHeight / 2) + (focusedItem.clientHeight / 2);
menuList.style.scrollBehavior = '';
}
},
updateMenuItemContent(index) {
const menuContainer = document.getElementById('chat-nav-menu-container');
if (!menuContainer || !menuContainer.classList.contains('visible')) return;
const menuList = document.getElementById('chat-nav-menu');
if (!menuList) return;
const menuItem = menuList.children[index];
const turn = ChatController.allTurns[index];
if (!menuItem || !turn || !turn.cachedContent) return;
const textSpan = menuItem.querySelector('.menu-item-text');
if (textSpan) {
const { display, full } = turn.cachedContent;
const truncatedText = (display.length > 200) ? display.substring(0, 197) + '...' : display;
if (textSpan.textContent !== truncatedText) {
textSpan.textContent = truncatedText;
}
menuItem.dataset.tooltip = full.replace(/\s+/g, ' ');
}
},
updateMenuFocus(items, newIndex, shouldScroll = true) {
if (!items || items.length === 0 || newIndex < 0 || newIndex >= items.length) return;
if (ChatController.menuFocusedIndex > -1 && ChatController.menuFocusedIndex < items.length) {
items[ChatController.menuFocusedIndex].classList.remove('menu-item-focused');
}
items[newIndex].classList.add('menu-item-focused');
if (shouldScroll) {
const menuList = document.getElementById('chat-nav-menu');
const focusedItem = items[newIndex];
if (menuList && focusedItem) {
const itemRect = focusedItem.getBoundingClientRect();
const menuRect = menuList.getBoundingClientRect();
if (itemRect.bottom > menuRect.bottom) {
menuList.scrollTop += itemRect.bottom - menuRect.bottom;
} else if (itemRect.top < menuRect.top) {
menuList.scrollTop -= menuRect.top - itemRect.top;
}
}
}
ChatController.menuFocusedIndex = newIndex;
},
getTextFromTurn(turn) {
const contentContainer = turn.querySelector('.turn-content');
if (!contentContainer || contentContainer.children.length === 0) return { source: 'empty' };
const isGenerating = !!turn.querySelector('loading-indicator');
if (isGenerating) {
return { display: 'Generating...', full: 'Generating...', source: 'generating' };
}
let rawText = '';
const unwantedSelectors = 'ms-thought-chunk, .turn-information, .author-label, .turn-separator, .thought-collapsed-text-container, .turn-footer, ms-chat-turn-options, loading-indicator, .code-preview-text, ms-search-entry-point, ms-share-prompt, script, style, noscript, ms-callout';
const isBlock = (node) => {
const tag = node.tagName;
return tag === 'P' || tag === 'DIV' || tag === 'BR' || tag === 'LI' || tag === 'PRE' || tag === 'H1' || tag === 'H2' || tag === 'H3' || tag === 'MS-CODE-BLOCK' || tag === 'TR';
};
const walk = (node) => {
if (node.nodeType === 1) {
if (node.matches && node.matches(unwantedSelectors)) return;
if (isBlock(node)) rawText += '\n';
let child = node.firstChild;
while (child) {
walk(child);
child = child.nextSibling;
}
if (isBlock(node) && node.tagName !== 'BR') rawText += '\n';
} else if (node.nodeType === 3) {
rawText += node.nodeValue;
}
};
walk(contentContainer);
const fullText = rawText.replace(/\n\s*\n\s*\n/g, '\n\n').trim();
if (fullText.length > 0) {
const singleLineDisplay = fullText.replace(/\s+/g, ' ').trim();
return { display: singleLineDisplay, full: fullText, source: 'dom' };
}
return { source: 'empty' };
},
// Updates dynamic contents and builds DOM for navigation menu
populateNavMenu() {
const menuContainer = document.getElementById('chat-nav-menu-container');
if (!menuContainer) return;
Array.from(menuContainer.children).forEach(child => {
if (!child.classList.contains('quicknav-menu-resizer')) {
child.remove();
}
});
if (typeof SafeStyleInjector !== 'undefined') {
const btnStyles = `
.quicknav-btn-paste, .quicknav-btn-clear { transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); background-color: transparent !important; }
body:not(.dark-theme) .quicknav-btn-paste { color: #f57f17 !important; border: 1px solid rgba(245, 127, 23, 0.4) !important; }
body:not(.dark-theme) .quicknav-btn-paste:hover { background-color: rgba(255, 235, 59, 0.15) !important; border-color: #f57f17 !important; }
body:not(.dark-theme) .quicknav-btn-clear { color: #c5221f !important; border: 1px solid rgba(197, 34, 31, 0.3) !important; }
body:not(.dark-theme) .quicknav-btn-clear:hover { background-color: rgba(217, 48, 37, 0.06) !important; border-color: #c5221f !important; }
body.dark-theme .quicknav-btn-paste { color: #fdd663 !important; border: 1px solid rgba(253, 214, 99, 0.4) !important; }
body.dark-theme .quicknav-btn-paste:hover { background-color: rgba(253, 214, 99, 0.08) !important; border-color: #fdd663 !important; color: #fde293 !important; }
body.dark-theme .quicknav-btn-clear { color: #f28b82 !important; border: 1px solid rgba(242, 139, 130, 0.4) !important; }
body.dark-theme .quicknav-btn-clear:hover { background-color: rgba(242, 139, 130, 0.08) !important; border-color: #f28b82 !important; color: #f6aea9 !important; }
`;
SafeStyleInjector.inject('quicknav-btn-colors', btnStyles);
}
if (this.currentSearchTerm === undefined) {
this.currentSearchTerm = '';
}
const header = document.createElement('div');
header.className = 'chat-nav-menu-header';
const menuList = document.createElement('ul');
menuList.id = 'chat-nav-menu';
menuList.tabIndex = -1;
const searchContainer = document.createElement('div');
searchContainer.className = 'chat-nav-search-row';
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.placeholder = 'Search within turns...';
searchInput.className = 'chat-nav-filter-input';
searchInput.onclick = (e) => e.stopPropagation();
if (this.currentSearchTerm) {
searchInput.value = this.currentSearchTerm;
}
const updateActionButtons = (hasText) => {
if (hasText) {
clearBtn.style.display = 'flex';
pasteBtn.style.display = 'none';
} else {
clearBtn.style.display = 'none';
pasteBtn.style.display = 'flex';
}
};
const pasteBtn = document.createElement('div');
pasteBtn.className = 'chat-nav-search-clear quicknav-btn-paste';
pasteBtn.textContent = 'Paste';
pasteBtn.title = 'Paste from clipboard';
pasteBtn.onclick = async (e) => {
e.stopPropagation();
try {
const text = await navigator.clipboard.readText();
if (text) {
searchInput.value = text;
searchInput.dispatchEvent(new Event('input'));
searchInput.focus();
}
} catch (err) {
searchInput.focus();
}
};
const clearBtn = document.createElement('div');
clearBtn.className = 'chat-nav-search-clear quicknav-btn-clear';
clearBtn.textContent = 'Clear';
clearBtn.title = 'Clear search';
clearBtn.onclick = (e) => {
e.stopPropagation();
searchInput.value = '';
searchInput.dispatchEvent(new Event('input'));
searchInput.focus();
};
searchInput.addEventListener('input', (e) => {
const term = e.target.value.toLowerCase();
this.currentSearchTerm = term;
updateActionButtons(!!term);
const items = menuList.querySelectorAll('.chat-nav-menu-item');
let hasVisible = false;
items.forEach(item => {
const text = (item.dataset.tooltip || '').toLowerCase();
const visibleText = (item.textContent || '').toLowerCase();
if (!term || text.includes(term) || visibleText.includes(term)) {
item.style.display = '';
item.dataset.visible = 'true';
hasVisible = true;
} else {
item.style.display = 'none';
item.dataset.visible = 'false';
}
});
let noRes = menuList.querySelector('.quicknav-no-results');
if (!hasVisible && term) {
if (!noRes) {
noRes = document.createElement('li');
noRes.className = 'quicknav-no-results';
noRes.textContent = 'No turns match your search';
menuList.appendChild(noRes);
}
} else if (noRes) {
noRes.remove();
}
});
searchContainer.append(searchInput, pasteBtn, clearBtn);
if (this.customTooltip) {
this.customTooltip.style.pointerEvents = 'none';
this.customTooltip.style.maxHeight = '50vh';
this.customTooltip.onmouseenter = () => {
clearTimeout(this.hideTooltipTimer);
};
this.customTooltip.onmouseleave = () => {
this.customTooltip.style.opacity = '0';
this.customTooltip.style.pointerEvents = 'none';
};
}
menuList.addEventListener('click', async (e) => {
const item = e.target.closest('.chat-nav-menu-item');
if (item && item.dataset.index) {
const index = parseInt(item.dataset.index, 10);
if (!isNaN(index)) {
this.toggleNavMenu();
await ChatController.navigateToIndex(index);
if (this.currentSearchTerm && this.currentSearchTerm.length > 1) {
ChatController.highlightTextInTurn(index, this.currentSearchTerm);
}
}
}
});
const fragment = document.createDocumentFragment();
ChatController.allTurns.forEach((turn, index) => {
let displayContent = turn.cachedContent;
if (!displayContent || (displayContent.source !== 'dom' && turn._isStale)) {
const fastText = ChatController.getFastScrollbarText(turn);
if (!turn.cachedContent || turn.cachedContent.source !== 'dom') {
displayContent = { display: fastText, full: fastText, source: 'scrollbar' };
turn.cachedContent = displayContent;
} else {
displayContent = turn.cachedContent;
}
}
const { display, full } = displayContent;
const truncatedText = (display.length > 200) ? display.substring(0, 197) + '...' : display;
const item = document.createElement('li');
item.className = 'chat-nav-menu-item';
item.setAttribute('role', 'menuitem');
item.dataset.index = index;
item.dataset.visible = 'true';
const isPrompt = ChatController.isUserPrompt(turn);
item.classList.add(isPrompt ? 'prompt-item-bg' : 'response-item-bg');
const numberSpan = document.createElement('span');
numberSpan.className = `menu-item-number ${isPrompt ? 'prompt-number-color' : 'response-number-color'}`;
numberSpan.textContent = `${index + 1}.`;
const textSpan = document.createElement('span');
textSpan.className = 'menu-item-text';
textSpan.textContent = truncatedText;
item.append(numberSpan, textSpan);
item.dataset.tooltip = full.replace(/\s+/g, ' ');
fragment.appendChild(item);
});
menuList.appendChild(fragment);
if (this.currentSearchTerm) {
searchInput.dispatchEvent(new Event('input'));
}
updateActionButtons(!!this.currentSearchTerm);
menuList.addEventListener('wheel', (e) => {
const list = e.currentTarget;
const isScrollable = list.scrollHeight > list.clientHeight;
const isScrollingUp = e.deltaY < 0;
const isScrollingDown = e.deltaY > 0;
if (!isScrollable) {
e.preventDefault();
return;
}
const isAtTop = list.scrollTop === 0;
const isAtBottom = Math.ceil(list.scrollTop + list.clientHeight) >= list.scrollHeight;
if ((isAtTop && isScrollingUp) || (isAtBottom && isScrollingDown)) {
e.preventDefault();
}
});
let tooltipTarget = null;
let lastEvent = null;
let currentMatchIndex = 0;
const scrollToMatch = (index) => {
const bodyDiv = this.customTooltip.querySelector('.quicknav-tooltip-body');
if (!bodyDiv) return;
const marks = bodyDiv.querySelectorAll('mark');
if (marks.length === 0) return;
const safeIndex = (index + marks.length) % marks.length;
marks.forEach(m => m.classList.remove('active'));
const target = marks[safeIndex];
target.classList.add('active');
target.scrollIntoView({ block: 'center', behavior: 'auto' });
const counterSpan = this.customTooltip.querySelector('.quicknav-match-counter');
if (counterSpan) {
const totalMatches = marks.length;
counterSpan.textContent = `${safeIndex + 1}/${totalMatches}`;
}
currentMatchIndex = safeIndex;
};
const updateTooltipContent = (target) => {
if (!target) return;
const rawText = target.dataset.tooltip;
const term = this.currentSearchTerm ? this.currentSearchTerm.trim() : '';
this.customTooltip.replaceChildren();
const bodyDiv = document.createElement('div');
bodyDiv.className = 'quicknav-tooltip-body';
if (term && term.length > 1) {
const escapedTerm = ChatController.escapeRegExp(term);
const regex = new RegExp(`(${escapedTerm})`, 'gi');
const parts = rawText.split(regex);
let matchCount = 0;
const contentSpan = document.createElement('span');
parts.forEach(part => {
if (part.toLowerCase() === term) {
const mark = document.createElement('mark');
mark.className = 'quicknav-search-highlight';
mark.textContent = part;
contentSpan.appendChild(mark);
matchCount++;
} else {
contentSpan.appendChild(document.createTextNode(part));
}
});
if (matchCount > 0) {
const toolbar = document.createElement('div');
toolbar.className = 'quicknav-tooltip-toolbar';
const groupLeft = document.createElement('div');
groupLeft.className = 'quicknav-tooltip-left-group';
const btnPrev = document.createElement('button');
btnPrev.className = 'quicknav-tooltip-btn';
btnPrev.textContent = '‹';
btnPrev.onclick = (e) => { e.stopPropagation(); scrollToMatch(currentMatchIndex - 1); };
const matchCounter = document.createElement('span');
matchCounter.className = 'quicknav-match-counter';
matchCounter.textContent = `1/${matchCount}`;
const btnNext = document.createElement('button');
btnNext.className = 'quicknav-tooltip-btn';
btnNext.textContent = '›';
btnNext.onclick = (e) => { e.stopPropagation(); scrollToMatch(currentMatchIndex + 1); };
groupLeft.append(btnPrev, matchCounter, btnNext);
const msgTitle = document.createElement('span');
msgTitle.className = 'quicknav-msg-title';
const msgNum = parseInt(target.dataset.index) + 1;
msgTitle.textContent = `Msg ${msgNum}`;
const spacer = document.createElement('div');
toolbar.append(groupLeft, msgTitle, spacer);
this.customTooltip.appendChild(toolbar);
this.customTooltip.style.pointerEvents = 'auto';
}
bodyDiv.appendChild(contentSpan);
this.customTooltip.appendChild(bodyDiv);
currentMatchIndex = 0;
if (matchCount > 0) {
setTimeout(() => scrollToMatch(0), 50);
}
} else {
bodyDiv.textContent = rawText;
this.customTooltip.appendChild(bodyDiv);
bodyDiv.scrollTop = 0;
}
if (target.classList.contains('response-item-bg')) {
const isDarkMode = document.body.classList.contains('dark-theme');
this.customTooltip.style.color = isDarkMode ? 'var(--ms-primary, #8ab4f8)' : '#174ea6';
} else {
this.customTooltip.style.color = '';
}
};
menuList.addEventListener('mousemove', e => {
lastEvent = e;
const currentTarget = e.target.closest('.chat-nav-menu-item');
const delay = (typeof StyleManager !== 'undefined' && StyleManager.HOVER_DELAY) ? StyleManager.HOVER_DELAY.current : 500;
if (!currentTarget) {
if (tooltipTarget) {
clearTimeout(this.tooltipTimeout);
this.tooltipTimeout = null;
clearTimeout(this.hideTooltipTimer);
this.customTooltip.style.opacity = '0';
this.customTooltip.style.pointerEvents = 'none';
tooltipTarget = null;
}
return;
}
if (currentTarget === tooltipTarget) {
if (this.customTooltip.style.opacity === '1') {
this.positionTooltip(e);
} else if (!this.tooltipTimeout) {
this.tooltipTimeout = setTimeout(() => {
if (!tooltipTarget) return;
this.customTooltip.style.opacity = '1';
this.customTooltip.style.pointerEvents = 'auto';
this.customTooltip.replaceChildren();
const processingMsg = document.createElement('div');
processingMsg.style.padding = '8px';
processingMsg.style.fontStyle = 'italic';
processingMsg.style.color = '#5f6368';
processingMsg.textContent = 'Processing...';
this.customTooltip.appendChild(processingMsg);
if (lastEvent) requestAnimationFrame(() => this.positionTooltip(lastEvent));
setTimeout(() => { updateTooltipContent(tooltipTarget); }, 0);
this.tooltipTimeout = null;
}, delay);
}
} else {
clearTimeout(this.tooltipTimeout);
clearTimeout(this.hideTooltipTimer);
tooltipTarget = currentTarget;
this.tooltipTimeout = setTimeout(() => {
if (!tooltipTarget) return;
this.customTooltip.style.opacity = '1';
this.customTooltip.style.pointerEvents = 'auto';
this.customTooltip.replaceChildren();
const processingMsg = document.createElement('div');
processingMsg.style.padding = '8px';
processingMsg.style.fontStyle = 'italic';
processingMsg.style.color = '#5f6368';
processingMsg.textContent = 'Processing...';
this.customTooltip.appendChild(processingMsg);
if (lastEvent) requestAnimationFrame(() => this.positionTooltip(lastEvent));
setTimeout(() => { updateTooltipContent(tooltipTarget); }, 0);
this.tooltipTimeout = null;
}, delay);
}
});
menuList.addEventListener('mouseleave', () => {
clearTimeout(this.tooltipTimeout);
this.tooltipTimeout = null;
this.hideTooltipTimer = setTimeout(() => {
this.customTooltip.style.opacity = '0';
this.customTooltip.style.pointerEvents = 'none';
tooltipTarget = null;
}, 300);
});
const leftContainer = document.createElement('div');
leftContainer.className = 'header-controls left';
const loadButton = document.createElement('button');
loadButton.id = 'chat-nav-load-button';
loadButton.className = 'header-button';
loadButton.textContent = 'Load All';
loadButton.title = 'Load full text for all messages';
loadButton.addEventListener('click', (e) => {
e.stopPropagation();
ChatController.startDynamicMenuLoading();
});
const statusIndicator = document.createElement('span');
statusIndicator.id = 'chat-nav-loader-status';
leftContainer.append(loadButton, statusIndicator);
const titleElement = document.createElement('div');
titleElement.className = 'quicknav-title google-text-animated';
titleElement.textContent = 'QuickNav for Google AI Studio';
const rightContainer = document.createElement('div');
rightContainer.className = 'header-controls right';
const donateButton = document.createElement('a');
donateButton.id = 'chat-nav-donate-link';
donateButton.href = 'https://nowpayments.io/donation/axl_script';
donateButton.target = '_blank';
donateButton.rel = 'noopener noreferrer';
donateButton.className = 'header-button';
donateButton.title = 'Support the developer';
const donateText = document.createElement('span');
donateText.className = 'donate-button-animated';
donateText.textContent = 'Donate';
donateButton.appendChild(donateText);
donateButton.addEventListener('click', (e) => e.stopPropagation());
rightContainer.append(donateButton);
header.append(leftContainer, titleElement, rightContainer);
menuContainer.appendChild(header);
menuContainer.appendChild(searchContainer);
menuContainer.appendChild(menuList);
const handleInputArrow = (e) => {
if (document.activeElement === searchInput && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
menuList.focus();
}
};
menuContainer.addEventListener('keydown', handleInputArrow);
},
updateScrollPercentage() {
if (ChatController.isInputActive) return;
if (this._scrollUpdateRaf) return;
const now = performance.now();
if (now - this._lastScrollUpdate < 100) return;
this._scrollUpdateRaf = requestAnimationFrame(() => {
this._scrollUpdateRaf = null;
this._lastScrollUpdate = performance.now();
const floater = document.getElementById('quicknav-badge-floater');
const percentageBadge = document.getElementById('quicknav-badge-percentage');
const scrollContainer = document.querySelector('ms-autoscroll-container');
if (!floater || !percentageBadge || !scrollContainer || ChatController.currentIndex < 0) {
if (floater) this.hideBadge(0, true);
return;
}
const currentTurn = ChatController.allTurns[ChatController.currentIndex];
if (!currentTurn) {
this.hideBadge(0, true);
return;
}
const rect = currentTurn.getBoundingClientRect();
const scrollContainerRect = scrollContainer.getBoundingClientRect();
const turnHeight = rect.height;
const viewportHeight = window.innerHeight;
const MIN_VISIBLE_HEIGHT = 40;
const visibleHeight = Math.max(0, Math.min(rect.bottom, viewportHeight) - Math.max(rect.top, 0));
const isElementVisible = visibleHeight >= MIN_VISIBLE_HEIGHT;
if (floater.style.opacity === '1' && !isElementVisible) {
this.hideBadge(0, true);
return;
}
if (!isElementVisible || turnHeight <= 0) {
return;
}
const floaterHeight = floater.offsetHeight || 44;
const visibleTurnTop = Math.max(rect.top, scrollContainerRect.top);
const visibleTurnBottom = Math.min(rect.bottom, scrollContainerRect.bottom);
const idealTop = (visibleTurnTop + visibleTurnBottom) / 2 - (floaterHeight / 2);
const toolbarBottom = this.toolbarElement ? this.toolbarElement.getBoundingClientRect().bottom : 0;
const footerTop = this.footerElement ? this.footerElement.getBoundingClientRect().top : viewportHeight;
const navContainerTop = this.navContainerElement ? this.navContainerElement.getBoundingClientRect().top : viewportHeight;
const upperBound = Math.max(scrollContainerRect.top + 4, rect.top, toolbarBottom + 4);
const lowerBound = Math.min(scrollContainerRect.bottom, rect.bottom, footerTop, navContainerTop) - floaterHeight - 4;
let finalTop = Math.max(upperBound, Math.min(idealTop, lowerBound));
floater.style.top = `${finalTop}px`;
floater.style.left = `${rect.right - (floater.offsetWidth / 2)}px`;
const viewportCenterY = viewportHeight / 2;
const distanceScrolled = viewportCenterY - rect.top;
const percentage = (distanceScrolled / turnHeight) * 100;
const clampedPercentage = Math.max(0, Math.min(percentage, 100));
percentageBadge.textContent = `${Math.round(clampedPercentage)}%`;
});
},
positionTooltip(e) {
const tooltip = this.customTooltip;
if (!tooltip) return;
const winWidth = window.innerWidth;
const winHeight = window.innerHeight;
const tipHeight = tooltip.offsetHeight;
const tipWidth = tooltip.offsetWidth;
const margin = 12;
const cursorOffset = 20;
let top = e.clientY - (tipHeight / 2);
if (top < margin) {
top = margin;
} else if (top + tipHeight + margin > winHeight) {
top = winHeight - tipHeight - margin;
}
let left = e.clientX + cursorOffset;
if (left + tipWidth + margin > winWidth) {
left = winWidth - tipWidth - margin;
}
left = Math.max(margin, left);
tooltip.style.top = `${top}px`;
tooltip.style.left = `${left}px`;
}
};
UIManager.closeNavMenu = UIManager.closeNavMenu.bind(UIManager);
// Manages code block navigation with guaranteed injection stability and idle-time processing
const CodeBlockNavigator = {
monitoringQueue: new Set(),
processingTimer: null,
lastProcessTime: 0,
reset() {
this.monitoringQueue.clear();
if (this.processingTimer) {
clearTimeout(this.processingTimer);
this.processingTimer = null;
}
},
processTurns(allTurns) {
if (!allTurns || !Array.isArray(allTurns)) return;
let hasNewBlocks = false;
allTurns.forEach(turn => {
if (!turn || !turn.isConnected) return;
const blocks = turn.querySelectorAll('ms-code-block');
if (blocks.length > 0) {
blocks.forEach(block => this.monitoringQueue.add(block));
hasNewBlocks = true;
}
});
if (hasNewBlocks) {
this._scheduleProcessing();
}
},
togglePreviews(isEnabled) {
if (!isEnabled) {
document.querySelectorAll('.code-preview-text').forEach(el => el.remove());
} else {
if (typeof ChatController !== 'undefined' && ChatController.allTurns) {
this.processTurns(ChatController.allTurns);
}
}
},
_scheduleProcessing() {
if (this.processingTimer) return;
const now = Date.now();
const timeSinceLast = now - this.lastProcessTime;
const delay = Math.max(0, 1000 - timeSinceLast);
this.processingTimer = setTimeout(() => {
this.processingTimer = null;
this.lastProcessTime = Date.now();
if (window.requestIdleCallback) {
requestIdleCallback(() => this._processQueue(), { timeout: 2000 });
} else {
this._processQueue();
}
}, delay);
},
_processQueue() {
if (this.monitoringQueue.size === 0) return;
let retryNeeded = false;
const isPreviewEnabled = StyleManager.CODE_PREVIEW.enabled;
for (const block of this.monitoringQueue) {
if (!block.isConnected) {
this.monitoringQueue.delete(block);
continue;
}
try {
const header = block.querySelector('mat-expansion-panel-header .mat-content');
const actionsContainer = header ? header.querySelector('.actions-container') : null;
if (!header || !actionsContainer) {
retryNeeded = true;
continue;
}
const codeElement = block.querySelector('pre code');
const rawText = codeElement ? (codeElement.textContent || '') : '';
if (rawText.trim().length === 0) {
retryNeeded = true;
continue;
}
if (isPreviewEnabled && !header.querySelector('.code-preview-text')) {
const lines = rawText.split(/\r?\n/);
const nonEmptyLines = lines.filter(line => line.trim().length > 0);
const previewLines = nonEmptyLines.slice(0, 2);
const tooltipText = lines.slice(0, 10).join('\n');
if (previewLines.length > 0) {
const previewContainer = document.createElement('div');
previewContainer.className = 'code-preview-text';
previewContainer.title = tooltipText;
previewLines.forEach(line => {
let displayLine = line;
const MAX_DISPLAY_CHARS = 120;
if (displayLine.length > MAX_DISPLAY_CHARS) {
displayLine = displayLine.substring(0, MAX_DISPLAY_CHARS) + '...';
}
const lineSpan = document.createElement('span');
lineSpan.className = 'code-preview-line';
lineSpan.textContent = displayLine;
previewContainer.appendChild(lineSpan);
});
const title = header.querySelector('mat-panel-title');
if (title) {
title.appendChild(previewContainer);
}
}
}
if (!actionsContainer.querySelector('.code-block-nav-container')) {
const navContainer = document.createElement('div');
navContainer.className = 'code-block-nav-container';
const pathUp = 'M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z';
const pathDown = 'M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z';
const btnUp = this._createNavButton(pathUp);
const counter = document.createElement('div');
counter.className = 'code-nav-counter';
const btnDown = this._createNavButton(pathDown);
btnUp.addEventListener('click', (e) => this._handleNavClick(e));
btnDown.addEventListener('click', (e) => this._handleNavClick(e));
navContainer.append(btnUp, counter, btnDown);
actionsContainer.appendChild(navContainer);
const parentTurn = block.closest('ms-chat-turn');
if (parentTurn) {
this._updateSiblingCounters(parentTurn);
}
}
this.monitoringQueue.delete(block);
} catch (err) {
retryNeeded = true;
}
}
if (retryNeeded) {
this.processingTimer = setTimeout(() => {
this.processingTimer = null;
if (window.requestIdleCallback) {
requestIdleCallback(() => this._processQueue(), { timeout: 2000 });
} else {
this._processQueue();
}
}, 1000);
}
},
_updateSiblingCounters(turnElement) {
const allBlocks = Array.from(turnElement.querySelectorAll('ms-code-block'));
const total = allBlocks.length;
allBlocks.forEach((blk, index) => {
const nav = blk.querySelector('.code-block-nav-container');
if (nav) {
const counter = nav.querySelector('.code-nav-counter');
const btnUp = nav.querySelector('.code-nav-button:first-child');
const btnDown = nav.querySelector('.code-nav-button:last-child');
const current = index + 1;
if (counter) {
counter.textContent = `${current} / ${total}`;
counter.title = `Code block ${current} of ${total}`;
}
if (btnUp) {
if (current === 1) {
btnUp.dataset.navTarget = 'header';
btnUp.title = `Scroll to the header of this block (${current}/${total})`;
} else {
btnUp.dataset.navTarget = 'previous';
btnUp.title = `Go to previous block (${current - 1}/${total})`;
}
}
if (btnDown) {
if (current === total) {
btnDown.dataset.navTarget = 'footer';
btnDown.title = `Scroll to the footer of this block (${current}/${total})`;
} else {
btnDown.dataset.navTarget = 'next';
btnDown.title = `Go to next block (${current + 1}/${total})`;
}
}
}
});
},
_createNavButton(pathData) {
const button = document.createElement('button');
button.className = 'code-nav-button';
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute('height', '16px');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('width', '16px');
svg.setAttribute('fill', 'currentColor');
const path = document.createElementNS("http://www.w3.org/2000/svg", 'path');
path.setAttribute('d', pathData);
svg.appendChild(path);
button.appendChild(svg);
return button;
},
_handleNavClick(event) {
event.stopPropagation();
event.preventDefault();
const button = event.currentTarget;
const navTarget = button.dataset.navTarget;
const currentCodeBlock = button.closest('ms-code-block');
if (!currentCodeBlock) return;
const parentTurn = currentCodeBlock.closest('ms-chat-turn');
if (!parentTurn) return;
const turnCodeBlocks = Array.from(parentTurn.querySelectorAll('ms-code-block'));
const currentIndex = turnCodeBlocks.indexOf(currentCodeBlock);
let targetBlock = null;
switch (navTarget) {
case 'header':
this._scrollToElement(currentCodeBlock, 'header');
break;
case 'footer':
this._scrollToElement(currentCodeBlock, 'footer');
break;
case 'previous':
if (currentIndex > 0) {
targetBlock = turnCodeBlocks[currentIndex - 1];
this._scrollToElement(targetBlock, 'header');
}
break;
case 'next':
if (currentIndex < turnCodeBlocks.length - 1) {
targetBlock = turnCodeBlocks[currentIndex + 1];
this._scrollToElement(targetBlock, 'header');
}
break;
}
},
_scrollToElement(element, position = 'header') {
const scrollContainer = document.querySelector('ms-autoscroll-container');
if (!element || !scrollContainer) return;
ChatController.isScrollingProgrammatically = true;
const containerRect = scrollContainer.getBoundingClientRect();
const elementRect = element.getBoundingClientRect();
const currentScroll = scrollContainer.scrollTop;
const viewportHeight = scrollContainer.clientHeight;
let targetScrollTop;
if (position === 'header') {
const offset = viewportHeight * 0.35;
const relativeTop = elementRect.top - containerRect.top;
targetScrollTop = currentScroll + relativeTop - offset;
} else {
const relativeBottom = elementRect.bottom - containerRect.bottom;
targetScrollTop = currentScroll + relativeBottom + 20;
}
scrollContainer.scrollTo({
top: Math.max(0, targetScrollTop),
behavior: 'smooth'
});
setTimeout(() => {
ChatController.isScrollingProgrammatically = false;
}, 800);
}
};
const HotkeysManager = {
pressedKeys: new Set(),
lastStyleChange: 0,
keyMap: {
'Alt_PageUp': '#nav-up',
'Alt_PageDown': '#nav-down',
'Shift_Alt_PageUp': '#nav-top',
'Shift_Alt_PageDown': '#nav-bottom',
'Alt_KeyM': '#chat-nav-counter',
'Alt_KeyL': '#nav-action-lib',
'Shift_Alt_KeyT': '#quicknav-theme-trigger',
'Shift_Alt_KeyF': '#quicknav-fav-trigger'
},
init() {
document.addEventListener('keydown', this.handleKeyDown.bind(this));
document.addEventListener('keyup', this.handleKeyUp.bind(this));
window.addEventListener('blur', this.handleBlur.bind(this));
},
getKeyCombination(e) {
let key = '';
if (e.shiftKey) key += 'Shift_';
if (e.altKey) key += 'Alt_';
key += e.code;
return key;
},
handleKeyDown(e) {
if (e.altKey && e.code === 'KeyP') {
const promptInput = document.querySelector('ms-prompt-box textarea') || document.querySelector('.prompt-box-container textarea');
if (promptInput && document.activeElement !== promptInput) {
e.preventDefault();
e.stopPropagation();
ChatController.focusPromptInput();
return;
}
}
if (e.altKey && !e.shiftKey) {
if (e.code === 'ArrowUp') {
e.preventDefault();
e.stopPropagation();
PromptResizer.shiftState(1);
return;
}
if (e.code === 'ArrowDown') {
e.preventDefault();
e.stopPropagation();
PromptResizer.shiftState(-1);
return;
}
}
if (e.altKey) {
if (e.code === 'KeyC' && !e.shiftKey) {
e.preventDefault();
e.stopPropagation();
PromptActions.copy();
return;
}
if (e.code === 'KeyV' && !e.shiftKey) {
e.preventDefault();
e.stopPropagation();
PromptActions.paste();
return;
}
if (e.code === 'Backspace' && e.shiftKey) {
e.preventDefault();
e.stopPropagation();
PromptActions.clear(true);
return;
}
}
if (e.altKey && (e.code === 'Minus' || e.code === 'Equal')) {
e.preventDefault();
e.stopPropagation();
const now = Date.now();
if (now - this.lastStyleChange < 50) return;
this.lastStyleChange = now;
const isShift = e.shiftKey;
const isPlus = e.code === 'Equal';
const type = isShift ? 'WIDTH' : 'FONT';
const config = StyleManager[type];
const step = isPlus ? config.STEP : -config.STEP;
if (!config.enabled) {
StyleManager.toggleSetting(type, true);
}
StyleManager.changeValue(type, step);
return;
}
if (e.altKey && e.code === 'Digit0') {
e.preventDefault();
e.stopPropagation();
const type = e.shiftKey ? 'WIDTH' : 'FONT';
if (!StyleManager[type].enabled) {
StyleManager.toggleSetting(type, true);
}
StyleManager.resetValue(type);
return;
}
const isTypingArea = e.target.isContentEditable || e.target.closest('[contenteditable="true"]') || ['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName);
if (e.repeat || (!e.altKey && isTypingArea)) {
return;
}
const combination = this.getKeyCombination(e);
const targetSelector = this.keyMap[combination];
if (targetSelector) {
e.preventDefault();
e.stopPropagation();
if (!this.pressedKeys.has(combination)) {
this.pressedKeys.add(combination);
const targetElement = document.querySelector(targetSelector);
if (targetElement) {
targetElement.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window, buttons: 1 }));
}
}
}
},
handleKeyUp(e) {
const releasedCombos = [];
this.pressedKeys.forEach(combo => {
const parts = combo.split('_');
const mainKey = parts[parts.length - 1];
let isReleased = false;
if (mainKey === e.code) {
isReleased = true;
} else if ((parts.includes('Shift') && (e.code === 'ShiftLeft' || e.code === 'ShiftRight')) || (parts.includes('Alt') && (e.code === 'AltLeft' || e.code === 'AltRight'))) {
const requiredAlt = parts.includes('Alt');
const requiredShift = parts.includes('Shift');
if ((requiredAlt && !e.altKey) || (requiredShift && !e.shiftKey)) {
isReleased = true;
}
}
if (isReleased) {
releasedCombos.push(combo);
}
});
if (releasedCombos.length > 0) {
e.preventDefault();
e.stopPropagation();
releasedCombos.forEach(combo => {
this.pressedKeys.delete(combo);
const targetSelector = this.keyMap[combo];
if (targetSelector) {
const targetElement = document.querySelector(targetSelector);
if (targetElement) {
targetElement.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window, buttons: 0 }));
targetElement.click();
}
}
});
}
},
handleBlur() {
this.pressedKeys.forEach(combo => {
const targetSelector = this.keyMap[combo];
if (targetSelector) {
const targetElement = document.querySelector(targetSelector);
if (targetElement) {
targetElement.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window, buttons: 0 }));
}
}
});
this.pressedKeys.clear();
}
};
// Manages prompt textarea resizing and height enforcement
const PromptResizer = {
STATE_MIN: 0,
STATE_MID: 1,
STATE_MAX: 2,
currentState: 0,
textarea: null,
wrapper: null,
resizerHandle: null,
styleObserver: null,
mutationObserver: null,
previousLength: 0,
keys: {
mid: 'quicknav_h_collapsed',
max: 'quicknav_h_expanded'
},
cachedMidHeight: 150,
cachedMaxHeight: 500,
startHeight: 0,
startY: 0,
isInternalResize: false,
isManualExpansion: false,
isTyping: false,
inputTimer: null,
boundHandleClick: null,
boundStartDrag: null,
boundDoDrag: null,
boundStopDrag: null,
boundInputHandler: null,
boundDblClick: null,
expansionWatchdog: null,
hasActiveInteraction: false,
init(rootElement) {
this.injectLockStyles();
this.updateCache();
this.currentState = this.STATE_MIN;
this.isManualExpansion = false;
this.boundHandleClick = this.handleClick.bind(this);
this.boundStartDrag = this.startDrag.bind(this);
this.boundDoDrag = this.doDrag.bind(this);
this.boundStopDrag = this.stopDrag.bind(this);
this.boundInputHandler = this.onInput.bind(this);
this.boundDblClick = this.handleDblClick.bind(this);
document.addEventListener('click', this.boundHandleClick);
this.attachToRoot(rootElement || document.body);
},
updateCache() {
this.cachedMidHeight = StyleManager._read(this.keys.mid, 150);
this.cachedMaxHeight = StyleManager._read(this.keys.max, 500);
},
injectLockStyles() {
if (document.getElementById('quicknav-lock-styles')) return;
const style = document.createElement('style');
style.id = 'quicknav-lock-styles';
style.textContent = `
ms-prompt-box textarea.quicknav-locked-height,
.prompt-box-container textarea.quicknav-locked-height,
textarea.quicknav-locked-height {
height: var(--quicknav-prompt-height) !important;
max-height: 90vh !important;
min-height: 40px !important;
}
ms-prompt-box:has(textarea.quicknav-locked-height),
.prompt-box-container:has(textarea.quicknav-locked-height) {
max-height: none !important;
height: auto !important;
}
ms-prompt-box textarea.quicknav-limited-height,
.prompt-box-container textarea.quicknav-limited-height,
textarea.quicknav-limited-height {
max-height: var(--quicknav-limit-height) !important;
overflow-y: auto !important;
}
`;
document.head.appendChild(style);
},
destroy() {
this.disconnectObservers();
document.removeEventListener('click', this.boundHandleClick);
if (this.expansionWatchdog) {
clearInterval(this.expansionWatchdog);
this.expansionWatchdog = null;
}
if (this.textarea) {
this.textarea.removeEventListener('input', this.boundInputHandler);
this.textarea.classList.remove('quicknav-locked-height', 'quicknav-limited-height');
this.textarea.style.removeProperty('--quicknav-prompt-height');
this.textarea.style.removeProperty('--quicknav-limit-height');
}
if (this.resizerHandle) {
this.resizerHandle.removeEventListener('mousedown', this.boundStartDrag);
this.resizerHandle.removeEventListener('dblclick', this.boundDblClick);
this.resizerHandle.remove();
}
this.textarea = null;
this.wrapper = null;
},
attachToRoot(root) {
this.disconnectObservers();
const checkForTextarea = () => {
const found = root.querySelector('ms-prompt-box textarea') || root.querySelector('.prompt-box-container textarea');
if (found) {
this.attach(found);
}
};
checkForTextarea();
if (!this.textarea) {
this.mutationObserver = new MutationObserver((mutations) => {
checkForTextarea();
});
this.mutationObserver.observe(root, { childList: true, subtree: true });
}
},
disconnectObservers() {
if (this.mutationObserver) {
this.mutationObserver.disconnect();
this.mutationObserver = null;
}
if (this.styleObserver) {
this.styleObserver.disconnect();
this.styleObserver = null;
}
},
attach(element) {
if (this.textarea === element) return;
if (this.mutationObserver) {
this.mutationObserver.disconnect();
this.mutationObserver = null;
}
this.textarea = element;
this.wrapper = this.textarea.parentElement;
if (this.wrapper && this.textarea.isConnected) {
this.injectResizerHandle();
this.textarea.addEventListener('input', this.boundInputHandler);
this.updateCache();
this.previousLength = this.textarea.value.length;
this.hasActiveInteraction = this.textarea.value.length > 0;
if (this.previousLength === 0) {
this.currentState = this.STATE_MIN;
} else {
this.currentState = this.STATE_MID;
}
this.isManualExpansion = false;
this.applyState(this.currentState);
this.setupStyleObserver();
}
},
setupStyleObserver() {
if (this.styleObserver) this.styleObserver.disconnect();
this.styleObserver = new MutationObserver((mutations) => {
let shouldEnforce = false;
for (const mutation of mutations) {
if (mutation.attributeName === 'class') {
const hasLock = this.textarea.classList.contains('quicknav-locked-height');
const hasLimit = this.textarea.classList.contains('quicknav-limited-height');
if (this.currentState === this.STATE_MIN) {
if (!hasLimit) shouldEnforce = true;
} else {
if (!hasLock) shouldEnforce = true;
}
if (shouldEnforce) break;
}
}
if (shouldEnforce) this.enforceHeight();
});
this.styleObserver.observe(this.textarea, {
attributes: true,
attributeFilter: ['class']
});
},
onInput() {
if (!this.textarea) return;
if (this.textarea.value.length > 0) {
this.hasActiveInteraction = true;
}
this.isTyping = true;
clearTimeout(this.inputTimer);
this.inputTimer = setTimeout(() => {
this.isTyping = false;
this.handleInputIdle();
}, 150);
},
handleInputIdle() {
if (this.isInternalResize) return;
const currentLength = this.textarea.value.length;
this.previousLength = currentLength;
if (currentLength === 0) {
if (this.currentState !== this.STATE_MIN && this.hasActiveInteraction) {
this.isManualExpansion = false;
this.forceState(this.STATE_MIN);
}
return;
}
this.isManualExpansion = false;
if (this.currentState === this.STATE_MIN && currentLength > 0) {
if (this.textarea.scrollHeight >= (this.cachedMidHeight - 5)) {
this.forceState(this.STATE_MID);
}
} else {
this.enforceHeight();
}
},
enforceHeight() {
if (!this.textarea) return;
let targetHeight = 0;
if (this.currentState === this.STATE_MID) targetHeight = this.cachedMidHeight;
else if (this.currentState === this.STATE_MAX) targetHeight = this.cachedMaxHeight;
if (!targetHeight) return;
const currentVar = this.textarea.style.getPropertyValue('--quicknav-prompt-height');
const expectedVar = `${targetHeight}px`;
if (currentVar !== expectedVar || !this.textarea.classList.contains('quicknav-locked-height')) {
this.isInternalResize = true;
this.textarea.style.setProperty('--quicknav-prompt-height', expectedVar);
this.textarea.classList.add('quicknav-locked-height');
Promise.resolve().then(() => { this.isInternalResize = false; });
}
},
injectResizerHandle() {
if (!this.wrapper) return;
if (this.wrapper.querySelector('.quicknav-custom-resizer')) return;
const handle = document.createElement('div');
handle.className = 'quicknav-custom-resizer';
handle.title = 'Drag to resize, Double-click to reset';
handle.addEventListener('mousedown', this.boundStartDrag);
handle.addEventListener('dblclick', this.boundDblClick);
this.wrapper.appendChild(handle);
this.resizerHandle = handle;
},
handleDblClick(e) {
e.preventDefault();
e.stopPropagation();
if (!this.textarea) return;
if (this.currentState === this.STATE_MIN) return;
const viewportH = window.innerHeight;
let newHeight;
let key;
if (this.currentState === this.STATE_MAX) {
newHeight = Math.floor(viewportH * 0.80);
key = this.keys.max;
} else {
newHeight = Math.floor(viewportH * 0.15);
key = this.keys.mid;
}
newHeight = Math.max(40, Math.min(newHeight, viewportH * 0.9));
StyleManager._write(key, newHeight);
this.updateCache();
this.isInternalResize = true;
this.textarea.style.setProperty('--quicknav-prompt-height', `${newHeight}px`);
this.textarea.classList.add('quicknav-locked-height');
setTimeout(() => { this.isInternalResize = false; }, 50);
},
startDrag(e) {
if (!this.textarea) return;
e.preventDefault();
this.startY = e.clientY;
this.startHeight = parseInt(window.getComputedStyle(this.textarea).height, 10);
if (this.textarea.value.length === 0) {
this.hasActiveInteraction = false;
}
if (this.currentState === this.STATE_MIN) {
this.currentState = this.STATE_MID;
this.updateIcons();
}
this.isInternalResize = true;
document.documentElement.addEventListener('mousemove', this.boundDoDrag, false);
document.documentElement.addEventListener('mouseup', this.boundStopDrag, false);
},
doDrag(e) {
if (!this.textarea) return;
const delta = this.startY - e.clientY;
let newHeight = this.startHeight + delta;
const limit = window.innerHeight * 0.9;
if (newHeight > limit) newHeight = limit;
if (newHeight < 40) newHeight = 40;
this.textarea.style.setProperty('--quicknav-prompt-height', `${newHeight}px`);
this.textarea.classList.add('quicknav-locked-height');
},
stopDrag(e) {
document.documentElement.removeEventListener('mousemove', this.boundDoDrag, false);
document.documentElement.removeEventListener('mouseup', this.boundStopDrag, false);
if (this.textarea) {
const finalHeight = parseInt(this.textarea.style.getPropertyValue('--quicknav-prompt-height'), 10);
if (finalHeight) {
let message = "";
if (this.currentState === this.STATE_MID) {
StyleManager._write(this.keys.mid, finalHeight);
this.cachedMidHeight = finalHeight;
message = "Medium height saved";
if (this.cachedMidHeight >= this.cachedMaxHeight) {
this.cachedMaxHeight = Math.min(this.cachedMidHeight + 100, window.innerHeight * 0.9);
StyleManager._write(this.keys.max, this.cachedMaxHeight);
message += " (Max auto-increased)";
}
} else if (this.currentState === this.STATE_MAX) {
StyleManager._write(this.keys.max, finalHeight);
this.cachedMaxHeight = finalHeight;
message = "Maximum height saved";
if (this.cachedMaxHeight <= this.cachedMidHeight) {
this.cachedMidHeight = Math.max(40, this.cachedMaxHeight - 50);
StyleManager._write(this.keys.mid, this.cachedMidHeight);
message += " (Medium auto-adjusted)";
}
}
if (typeof PromptActions !== 'undefined') {
PromptActions.showToast(message);
}
}
}
setTimeout(() => { this.isInternalResize = false; }, 50);
},
handleClick(e) {
const btnDown = e.target.closest('#nav-expand-down');
const btnUp = e.target.closest('#nav-expand-up');
if (btnDown) {
e.preventDefault();
e.stopPropagation();
btnDown.blur();
this.shiftState(-1);
} else if (btnUp) {
e.preventDefault();
e.stopPropagation();
btnUp.blur();
this.shiftState(1);
}
},
shiftState(delta) {
let nextState = this.currentState + delta;
nextState = Math.max(this.STATE_MIN, Math.min(this.STATE_MAX, nextState));
if (nextState !== this.currentState) {
if (delta > 0) {
this.isManualExpansion = true;
} else {
this.isManualExpansion = false;
}
this.currentState = nextState;
this.applyState(this.currentState);
}
},
forceState(newState) {
if (newState < 0 || newState > 2) return;
this.currentState = newState;
this.applyState(this.currentState);
},
applyState(state) {
if (this.expansionWatchdog) {
clearInterval(this.expansionWatchdog);
this.expansionWatchdog = null;
}
this.updateIcons();
if (state === this.STATE_MIN) {
this.isInternalResize = true;
let midHeight = Math.max(this.cachedMidHeight, 60);
this.textarea.classList.remove('quicknav-locked-height');
this.textarea.style.setProperty('--quicknav-limit-height', `${midHeight}px`);
this.textarea.classList.add('quicknav-limited-height');
this.textarea.style.removeProperty('height');
this.textarea.style.removeProperty('min-height');
this.textarea.style.removeProperty('--quicknav-prompt-height');
this.hasActiveInteraction = false;
setTimeout(() => { this.isInternalResize = false; }, 100);
} else {
let defaultVal = (state === this.STATE_MAX) ? 500 : 150;
if (state === this.STATE_MAX) {
if (this.cachedMaxHeight && this.cachedMaxHeight <= this.cachedMidHeight) {
this.cachedMaxHeight = Math.max(this.cachedMidHeight + 100, 500);
StyleManager._write(this.keys.max, this.cachedMaxHeight);
}
}
let heightToApply = (state === this.STATE_MAX) ? this.cachedMaxHeight : this.cachedMidHeight;
if (!heightToApply) heightToApply = defaultVal;
this.isInternalResize = true;
this.textarea.classList.remove('quicknav-limited-height');
this.textarea.style.removeProperty('--quicknav-limit-height');
this.textarea.style.setProperty('--quicknav-prompt-height', `${heightToApply}px`);
this.textarea.classList.add('quicknav-locked-height');
setTimeout(() => { this.isInternalResize = false; }, 100);
if (this.textarea.value.length === 0) {
this.hasActiveInteraction = false;
}
this.expansionWatchdog = setInterval(() => {
if (this.textarea && this.textarea.value.length === 0 && this.hasActiveInteraction) {
this.isManualExpansion = false;
this.forceState(this.STATE_MIN);
}
}, 1000);
}
},
updateIcons() {
const btnDown = document.getElementById('nav-expand-down');
const btnUp = document.getElementById('nav-expand-up');
if (btnDown) {
btnDown.disabled = (this.currentState === this.STATE_MIN);
}
if (btnUp) {
btnUp.disabled = (this.currentState === this.STATE_MAX);
}
},
};
const DOMObserver = {
globalObserver: null,
editorObserver: null,
currentEditor: null,
hasSession: false,
callbacks: null,
EDITOR_TAG: 'MS-CHUNK-EDITOR',
SESSION_TAG: 'MS-CHAT-SESSION',
start(callbacks) {
this.callbacks = callbacks;
const existingEditor = document.querySelector(this.EDITOR_TAG);
if (existingEditor) {
this.connectEditor(existingEditor);
}
this.globalObserver = new MutationObserver(this.handleGlobalMutations.bind(this));
this.globalObserver.observe(document.body, { childList: true, subtree: true });
},
// Optimized to stop scanning once an editor is active
handleGlobalMutations(mutations) {
if (this.currentEditor && !this.currentEditor.isConnected) {
this.disconnectEditor();
}
if (this.currentEditor && this.currentEditor.isConnected) return;
for (const mutation of mutations) {
if (mutation.type === 'childList') {
for (const node of mutation.addedNodes) {
if (node.nodeType === 1) {
if (node.tagName === this.EDITOR_TAG) {
this.connectEditor(node);
return;
}
if (node.firstElementChild) {
const nestedEditor = node.querySelector(this.EDITOR_TAG);
if (nestedEditor) {
this.connectEditor(nestedEditor);
return;
}
}
}
}
}
}
},
connectEditor(editor) {
if (this.currentEditor === editor) return;
this.disconnectEditor();
this.currentEditor = editor;
if (this.callbacks && this.callbacks.onEditorReady) {
this.callbacks.onEditorReady(editor);
}
const session = editor.querySelector(this.SESSION_TAG);
if (session) {
this.activateSession(session);
}
this.editorObserver = new MutationObserver(this.handleEditorMutations.bind(this));
this.editorObserver.observe(editor, { childList: true, subtree: true });
},
disconnectEditor() {
if (this.hasSession) {
this.hasSession = false;
if (this.callbacks && this.callbacks.onSessionDestroyed) {
this.callbacks.onSessionDestroyed();
}
}
if (this.callbacks && this.callbacks.onEditorDestroyed) {
this.callbacks.onEditorDestroyed();
}
if (this.editorObserver) {
this.editorObserver.disconnect();
this.editorObserver = null;
}
this.currentEditor = null;
},
handleEditorMutations(mutations) {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
for (const node of mutation.removedNodes) {
if (node.nodeType === 1 && node.tagName === this.SESSION_TAG) {
this.hasSession = false;
if (this.callbacks && this.callbacks.onSessionDestroyed) {
this.callbacks.onSessionDestroyed();
}
}
}
for (const node of mutation.addedNodes) {
if (node.nodeType === 1 && node.tagName === this.SESSION_TAG) {
this.activateSession(node);
}
}
}
}
},
activateSession(session) {
if (this.hasSession) return;
this.hasSession = true;
requestAnimationFrame(() => {
if (this.callbacks && this.callbacks.onSessionReady) {
this.callbacks.onSessionReady(session);
}
});
}
};
const QuickNavApp = {
init() {
if (window.QuickNavActive) return;
window.QuickNavActive = true;
DOMObserver.start({
onEditorReady: this.handleEditorReady.bind(this),
onSessionReady: this.handleSessionReady.bind(this),
onEditorDestroyed: this.handleEditorDestroyed.bind(this),
onSessionDestroyed: this.handleSessionDestroyed.bind(this)
});
HotkeysManager.init();
PasteManager.init();
},
handleEditorReady(editorElement) {
try {
UIManager.create(editorElement);
PromptResizer.init(editorElement);
ThemeManager.init();
PasteManager.inject(editorElement);
} catch (e) {
}
},
handleSessionReady(sessionElement) {
try {
ChatController.init(sessionElement);
} catch (e) {
}
},
handleEditorDestroyed() {
UIManager.destroy();
PromptResizer.destroy();
PromptActions.reset();
StyleManager.disconnectWidthObserver();
},
handleSessionDestroyed() {
ChatController.destroy();
CodeBlockNavigator.reset();
}
};
QuickNavApp.init();
})();