// ==UserScript==
// @name PlusAI Widescreen 🖥️
// @namespace lanvent
// @description Enables widescreen mode for ChatGPT automatically.
// @description:zh 启用 ChatGPT 宽屏模式,支持markdown源码查看、长消息折叠和对话快捷操作功能。
// @author lanvent
// @version 2025.9.20.1
// @license MIT
// @match *://cc.plusai.me/*
// @match *://cc2.plusai.me/*
// @grant GM_setValue
// @grant GM_getValue
// @noframes
// ==/UserScript==
var ChatGPTWidescreen = function () {
'use strict';
const CONFIG = {
VERSION: '2025.9.20.1',
STYLES: {},
UI: {
ANIMATIONS: {
scrollBehavior: 'smooth'
}
},
BUTTON_STYLES: {
borderWidth: '2px',
borderRadius: '8px',
width: '38px',
height: '38px',
background: 'inherit',
borderColor: 'rgba(100, 150, 250, 0.6)',
fontSize: '16px',
fontWeight: '700'
},
SCREEN_MARGIN: 50,
MESSAGE: {
LONG_MESSAGE_THRESHOLD: 1e3,
VISIBILITY_THRESHOLD: .05,
MAX_LINES: 6,
MIN_LINE_HEIGHT: 20,
ESTIMATED_CHARS_PER_LINE: 80
},
SELECTORS: {
TEXT_CONTAINER: 'main .mx-auto.text-base',
CONVERSATION: '[data-testid^="conversation-turn"]',
messages: {
user: '[data-message-author-role="user"]',
assistant: '[data-message-author-role="assistant"]',
content: '.markdown, .prose, [data-message-content], div[class*="markdown"]'
},
ui: {
turnActions: '[data-testid*="turn-action"]'
},
CODE_BLOCK: 'pre code, .bg-black',
FLOAT_PANEL: '.chatgpt-widescreen-float-panel'
},
STORAGE_KEYS: {
SETTINGS: 'chatgpt_widescreen_settings'
},
DEBUG: !1
}, DEFAULT_SETTINGS = {
widescreenMode: !0,
autoCollapse: !0,
showFloatButtons: !0,
enableScroll: !0,
enableConversationOps: !0,
enableMessageNavigation: !0
}, CSS_CLASSES_WIDESCREEN = 'chatgpt-widescreen', CSS_CLASSES_COLLAPSED = 'direct-collapse', CSS_CLASSES_FLOAT_PANEL = 'chatgpt-widescreen-float-panel', CSS_CLASSES_BUTTON = 'chatgpt-widescreen-btn', CSS_CLASSES_HIDDEN = 'chatgpt-hidden';
class Logger {
static debug(message, ...optionalParams) {}
static info(message, ...optionalParams) {
const timestamp = (new Date).toISOString();
console.info(`[${timestamp}] ${message}`, ...optionalParams);
}
static log(message, ...optionalParams) {
const timestamp = (new Date).toISOString();
console.log(`[${timestamp}] ${message}`, ...optionalParams);
}
}
const settingsManager = new class {
constructor() {
this.settings = null, this.init();
}
init() {
this.settings = this.loadSettings();
}
loadSettings() {
try {
let stored = null;
if (stored = 'undefined' != typeof GM_getValue ? GM_getValue(CONFIG.STORAGE_KEYS.SETTINGS, null) : localStorage.getItem(CONFIG.STORAGE_KEYS.SETTINGS),
stored) {
const parsed = 'string' == typeof stored ? JSON.parse(stored) : stored;
return {
...DEFAULT_SETTINGS,
...parsed
};
}
} catch (error) {
console.warn('[PlusAI Widescreen] 加载设置失败:', error);
}
return {
...DEFAULT_SETTINGS
};
}
saveSettings() {
try {
const settingsString = JSON.stringify(this.settings);
'undefined' != typeof GM_setValue ? GM_setValue(CONFIG.STORAGE_KEYS.SETTINGS, settingsString) : localStorage.setItem(CONFIG.STORAGE_KEYS.SETTINGS, settingsString),
Logger.info('[PlusAI Widescreen] 设置已保存:', this.settings);
} catch (error) {
Logger.error('[PlusAI Widescreen] 保存设置失败:', error);
}
}
get(key) {
return this.settings[key];
}
set(key, value) {
this.settings[key] = value, this.saveSettings();
}
getAll() {
return {
...this.settings
};
}
update(newSettings) {
this.settings = {
...this.settings,
...newSettings
}, this.saveSettings();
}
reset() {
this.settings = {
...DEFAULT_SETTINGS
}, this.saveSettings();
}
export() {
return JSON.stringify(this.settings, null, 2);
}
import(settingsJson) {
try {
const imported = JSON.parse(settingsJson);
return this.settings = {
...DEFAULT_SETTINGS,
...imported
}, this.saveSettings(), !0;
} catch (error) {
return console.error('[PlusAI Widescreen] 导入设置失败:', error), !1;
}
}
};
class DOMUtils {
static createElement(tagName, attributes = {}, textContent = '') {
const element = document.createElement(tagName);
return Object.entries(attributes).forEach(([key, value]) => {
'className' === key ? element.className = value : 'dataset' === key ? Object.entries(value).forEach(([dataKey, dataValue]) => {
element.dataset[dataKey] = dataValue;
}) : element.setAttribute(key, value);
}), textContent && (element.textContent = textContent), element;
}
static async waitForElement(selector, parent = document, timeout = 5e3) {
return new Promise((resolve, reject) => {
const element = parent.querySelector(selector);
if (element) return void resolve(element);
const observer = new MutationObserver(() => {
const element = parent.querySelector(selector);
element && (observer.disconnect(), resolve(element));
});
observer.observe(parent, {
childList: !0,
subtree: !0
}), setTimeout(() => {
observer.disconnect(), reject(new Error(`Element ${selector} not found within ${timeout}ms`));
}, timeout);
});
}
static isElementVisible(element, _threshold, bottomThreshold = 120, topThreshold = 50) {
const rect = element.getBoundingClientRect(), windowHeight = window.innerHeight || document.documentElement.clientHeight;
return !(rect.top > windowHeight - bottomThreshold || rect.bottom < topThreshold);
}
static getComputedStyle(element, property) {
return window.getComputedStyle(element).getPropertyValue(property);
}
static hide(element) {
element && element.classList.add('invisible');
}
static show(element) {
element && element.classList.remove('invisible');
}
static toggleVisibility(element, show) {
show ? this.show(element) : this.hide(element);
}
static isInDOM(element) {
return document.contains(element);
}
static getOrCreateId(element, prefix = 'element') {
if (element.id) return element.id;
const id = `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
return element.id = id, id;
}
static addClass(element, className) {
element && className && !element.classList.contains(className) && element.classList.add(className);
}
static removeClass(element, className) {
element && className && element.classList.contains(className) && element.classList.remove(className);
}
static toggleClass(element, className) {
return !(!element || !className) && (element.classList.toggle(className), element.classList.contains(className));
}
static hasClass(element, className) {
return element && element.classList.contains(className);
}
static findElement(selectors, container = document) {
if (Array.isArray(selectors)) {
for (const selector of selectors) {
const element = container.querySelector(selector);
if (element) return element;
}
return null;
}
return container.querySelector(selectors);
}
static findElements(selector, container = document) {
return Array.from(container.querySelectorAll(selector));
}
static removeElement(element) {
element && element.parentNode && element.parentNode.removeChild(element);
}
static debounce(func, delay) {
let timeoutId;
return function (...args) {
timeoutId && Logger.debug('Debounce: clearing timeout', timeoutId, func, args),
clearTimeout(timeoutId), timeoutId = setTimeout(() => func.apply(this, args), delay);
};
}
static throttle(func, delay) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= delay) return lastCall = now, func.apply(this, args);
};
}
}
class EventManager {
constructor() {
this.listeners = new Map;
}
static dispatch(eventType, data = null, target = window) {
const event = new CustomEvent(`chatgpt-${eventType}`, {
detail: data,
bubbles: !0,
cancelable: !0
});
Logger.debug(`[EventManager] 分发事件: ${eventType}`, data), target.dispatchEvent(event);
}
static listen(events, handler, target = window) {
Array.isArray(events) || (events = [events]);
const unlisten_funcs = [];
return events.forEach(eventType => {
if ('string' != typeof eventType) throw new Error('EventManager.listen: eventType must be a string or an array of strings');
const fullEventType = `chatgpt-${eventType}`, wrappedHandler = event => {
Logger.debug(`[EventManager] 收到事件: ${eventType}`, event.detail), handler(event.detail, event);
};
target.addEventListener(fullEventType, wrappedHandler), unlisten_funcs.push(() => {
target.removeEventListener(fullEventType, wrappedHandler);
});
}), () => {
unlisten_funcs.forEach(unlisten => unlisten());
};
}
static once(eventType, handler = null, target = window) {
return new Promise(resolve => {
const unlisten = this.listen(eventType, (data, event) => {
unlisten(), handler && handler(data, event), resolve(data);
}, target);
});
}
static removeAllListeners(eventType, _target = window) {
Logger.warn(`[EventManager] removeAllListeners(${eventType}) 需要手动管理监听器`);
}
}
const EVENTS_PAGE_NAVIGATION = 'page-navigation', EVENTS_PAGE_STYLE_CHANGED = 'page-style-changed', EVENTS_PAGE_RESIZE = 'page-resize', EVENTS_MESSAGE_ADDED = 'message-added', EVENTS_MESSAGE_COLLAPSED = 'message-collapsed', EVENTS_LONG_MESSAGES_UPDATED = 'long-messages-updated', EVENTS_BUTTON_CREATED = 'button-created', EVENTS_WIDESCREEN_TOGGLED = 'widescreen-toggled', EVENTS_SCROLL_DETECTED = 'scroll-detected', EVENTS_CONVERSATION_ACTION = 'conversation-action';
class UIUtils {
static getVisibleLineCount(element) {
if (!element) return 0;
try {
const style = window.getComputedStyle(element);
let lineHeight = parseFloat(style.lineHeight);
if (!lineHeight || isNaN(lineHeight)) {
const fontSize = parseFloat(style.fontSize) || 16;
lineHeight = Math.max(1.2 * fontSize, CONFIG.MESSAGE.MIN_LINE_HEIGHT);
}
const elementHeight = element.getBoundingClientRect().height, paddingTop = parseFloat(style.paddingTop) || 0, paddingBottom = parseFloat(style.paddingBottom) || 0, marginTop = parseFloat(style.marginTop) || 0, contentHeight = elementHeight - paddingTop - paddingBottom - marginTop - (parseFloat(style.marginBottom) || 0), lineCount = Math.floor(contentHeight / lineHeight);
return Math.max(0, lineCount);
} catch (error) {
Logger.debug('计算行数时出错:', error);
const textLength = element.textContent?.length || 0;
return Math.ceil(textLength / CONFIG.MESSAGE.ESTIMATED_CHARS_PER_LINE);
}
}
static isLongMessage(lineCount, textLength) {
return lineCount > CONFIG.MESSAGE.MAX_LINES || textLength >= CONFIG.MESSAGE.LONG_MESSAGE_THRESHOLD;
}
static analyzeMessage(messageEl) {
const content = messageEl.querySelector(CONFIG.SELECTORS.messages.content) || messageEl;
if (!content) return null;
const lineCount = this.getVisibleLineCount(content), textLength = content.textContent?.trim().length || 0, isLong = this.isLongMessage(lineCount, textLength), style = window.getComputedStyle(content), analysis = {
element: messageEl,
content: content,
lineCount: lineCount,
textLength: textLength,
isLongMessage: isLong,
dimensions: {
width: content.getBoundingClientRect().width,
height: content.getBoundingClientRect().height,
lineHeight: parseFloat(style.lineHeight) || 'auto',
fontSize: parseFloat(style.fontSize) || 16
},
thresholds: {
maxLines: CONFIG.MESSAGE.MAX_LINES,
longTextThreshold: CONFIG.MESSAGE.LONG_MESSAGE_THRESHOLD
}
};
return Logger.debug('消息分析结果:', analysis), analysis;
}
static scrollToMessageTop(messageElement, offset_ratio = 0) {
try {
const parentElement = messageElement.parentElement, messageEls = parentElement.querySelectorAll(CONFIG.SELECTORS.messages.content);
let focusElement;
focusElement = messageEls.length > 1 && messageEls[0] !== messageElement ? messageElement : parentElement;
const scrollContainer = this.findScrollableParent(focusElement);
if (scrollContainer === window) return;
'true' !== scrollContainer.dataset.scrollListenerAdded && (scrollContainer.dataset.scrollListenerAdded = 'true',
Logger.debug('Adding scroll listener to container:', scrollContainer), scrollContainer.addEventListener('scroll', () => {
EventManager.dispatch(EVENTS_SCROLL_DETECTED, {
source: 'element-scroll'
});
}));
const topBarHeight = scrollContainer.firstElementChild?.firstElementChild?.offsetHeight || 80;
let targetScrollTop;
if (scrollContainer === window) {
const rect = focusElement.getBoundingClientRect();
targetScrollTop = (window.pageYOffset || document.documentElement.scrollTop) + rect.top - topBarHeight + window.innerHeight * offset_ratio;
} else {
const containerRect = scrollContainer.getBoundingClientRect(), relativeTop = focusElement.getBoundingClientRect().top - containerRect.top + scrollContainer.clientHeight * offset_ratio;
targetScrollTop = scrollContainer.scrollTop + relativeTop - topBarHeight;
}
targetScrollTop = Math.max(0, targetScrollTop), scrollContainer.scrollTo({
top: targetScrollTop,
behavior: CONFIG.UI.ANIMATIONS.scrollBehavior
});
} catch (error) {
console.error('Scroll error:', error), messageElement.scrollIntoView({
behavior: CONFIG.UI.ANIMATIONS.scrollBehavior
});
}
}
static findScrollableParent(element) {
let parent = element.parentElement;
for (;parent && parent !== document.body;) {
const style = window.getComputedStyle(parent), overflow = style.overflow + style.overflowY + style.overflowX;
if ((overflow.includes('scroll') || overflow.includes('auto')) && (parent.scrollHeight > parent.clientHeight || parent.scrollWidth > parent.clientWidth)) return parent;
parent = parent.parentElement;
}
return window;
}
static getLongMessages(messages, needVisible = !0) {
const longMessages = [];
let hasEmptyContent = !1;
return 0 === messages.length ? (Logger.debug('未找到任何消息元素,可能页面尚未加载完成'), {
longMessages: longMessages,
hasEmptyContent: hasEmptyContent
}) : (messages.forEach((messageEl, _index) => {
const content = messageEl.querySelector(CONFIG.SELECTORS.messages.content) || messageEl, textLength = content ? content.textContent.trim().length : 0;
if (!!content && 0 === textLength && !hasEmptyContent && (hasEmptyContent = !0),
needVisible) {
if (!DOMUtils.isElementVisible(messageEl, CONFIG.MESSAGE.VISIBILITY_THRESHOLD)) return;
}
const lineCount = this.getVisibleLineCount(content);
if (this.isLongMessage(lineCount, textLength)) {
const rect = messageEl.getBoundingClientRect();
longMessages.push({
messageEl: messageEl,
textLength: textLength,
lineCount: lineCount,
rect: rect,
centerY: rect.top + rect.height / 2,
isUser: 'user' === messageEl.getAttribute('data-message-author-role')
});
}
}), {
longMessages: longMessages,
hasEmptyContent: hasEmptyContent
});
}
static triggerMouseEvents(element, type = 'click') {
if (!element) return !1;
Logger.debug('尝试触发事件,元素:', element);
try {
if ('click' === type) {
element.style.pointerEvents = 'auto', element.style.display = '';
try {
element.focus();
} catch (e) {
Logger.debug('元素无法获取焦点:', e);
}
const rect = element.getBoundingClientRect(), centerX = rect.left + rect.width / 2, centerY = rect.top + rect.height / 2, eventConfig = {
bubbles: !0,
cancelable: !0,
detail: 1,
button: 0,
buttons: 1,
clientX: centerX,
clientY: centerY,
screenX: centerX,
screenY: centerY,
shiftKey: !1,
ctrlKey: !1,
altKey: !1,
metaKey: !1
};
try {
['pointerdown', 'pointerup'].forEach(eventType => {
const event = new PointerEvent(eventType, {
...eventConfig,
pointerId: 1,
isPrimary: !0
});
element.dispatchEvent(event);
});
} catch (e) {
Logger.debug('PointerEvent触发失败:', e);
}
return !0;
}
const reactFiberKey = Object.keys(element).find(key => key.startsWith('__reactFiber') || key.startsWith('__reactInternalInstance'));
if (reactFiberKey) try {
const fiber = element[reactFiberKey], handler = fiber?.memoizedProps?.onClick;
handler && 'function' == typeof handler && (Logger.debug('调用React事件处理器'), handler({
preventDefault: () => {},
stopPropagation: () => {},
target: element,
currentTarget: element
}));
} catch (e) {
Logger.debug('React事件触发失败:', e);
}
return !0;
} catch (error) {
return console.error('触发鼠标事件失败:', error), !1;
}
}
static analyzeButton(button) {
if (!button) return;
Logger.debug('=== 按钮分析 ==='), Logger.debug('元素:', button), Logger.debug('标签名:', button.tagName),
Logger.debug('类名:', button.className), Logger.debug('ID:', button.id), Logger.debug('data-testid:', button.getAttribute('data-testid')),
Logger.debug('aria-label:', button.getAttribute('aria-label')), Logger.debug('disabled:', button.disabled),
Logger.debug('style.display:', button.style.display), Logger.debug('style.visibility:', button.style.visibility),
Logger.debug('offsetParent:', button.offsetParent), Logger.debug('getBoundingClientRect:', button.getBoundingClientRect());
const attrs = Array.from(button.attributes).map(attr => `${attr.name}="${attr.value}"`);
Logger.debug('所有属性:', attrs);
const events = window.getEventListeners ? window.getEventListeners(button) : {};
Logger.debug('事件监听器:', events);
const reactKeys = Object.keys(button).filter(key => key.includes('react') || key.includes('React'));
Logger.debug('React相关键:', reactKeys), Logger.debug('父元素:', button.parentElement),
Logger.debug('最近的可点击父元素:', button.closest('[role="button"], button, a'));
}
static createResizableCodeContainer(content) {
const codeContainer = DOMUtils.createElement('div', {
className: 'chatgpt-resizable-code-container'
}), codeBlock = DOMUtils.createElement('pre', {
className: 'bg-black rounded p-4 overflow-auto language-markdown chatgpt-code-block'
}), codeElement = DOMUtils.createElement('code', {
className: 'chatgpt-code-element'
}, content);
codeBlock.appendChild(codeElement);
const resizeHandle = this.createResizeHandle(codeContainer);
return codeContainer.appendChild(codeBlock), codeContainer.appendChild(resizeHandle),
codeContainer;
}
static createResizeHandle(container) {
const resizeHandle = DOMUtils.createElement('div', {
className: 'chatgpt-resize-handle'
});
resizeHandle.innerHTML = '<div class="chatgpt-resize-grip"></div>', resizeHandle.addEventListener('mouseenter', () => {
resizeHandle.classList.add('chatgpt-resize-handle-hover');
});
let isDragging = !1;
resizeHandle.addEventListener('mouseleave', () => {
isDragging || resizeHandle.classList.remove('chatgpt-resize-handle-hover');
});
let startY = 0, startHeight = 0;
return resizeHandle.addEventListener('mousedown', e => {
isDragging = !0, startY = e.clientY, startHeight = container.offsetHeight, document.body.style.cursor = 'ns-resize',
document.body.style.userSelect = 'none', e.preventDefault();
}), document.addEventListener('mousemove', e => {
if (!isDragging) return;
const deltaY = e.clientY - startY, newHeight = Math.max(400, startHeight + deltaY);
container.style.height = newHeight + 'px';
}), document.addEventListener('mouseup', () => {
isDragging && (isDragging = !1, document.body.style.cursor = '', document.body.style.userSelect = '',
resizeHandle.classList.remove('chatgpt-resize-handle-hover'));
}), resizeHandle;
}
static createFloatingPanel() {
return DOMUtils.createElement('div', {
className: CSS_CLASSES_FLOAT_PANEL
});
}
static createFloatingButton(text, className, title, onClick) {
const button = DOMUtils.createElement('button', {
className: `${CSS_CLASSES_BUTTON} ${className}`,
title: title
}, text);
return onClick && button.addEventListener('click', onClick), button;
}
static showNotification(message, type = 'info', duration = 3e3) {
const existing = document.querySelector('.chatgpt-notification');
existing && existing.remove();
const notification = DOMUtils.createElement('div', {
className: `chatgpt-notification chatgpt-notification-${type}`
}, message);
document.body.appendChild(notification), setTimeout(() => {
notification.classList.add('chatgpt-notification-show');
}, 10), setTimeout(() => {
notification.classList.remove('chatgpt-notification-show'), setTimeout(() => {
notification.parentNode && notification.remove();
}, 300);
}, duration);
}
static showConfirmDialog(message, onConfirm, onCancel) {
confirm(message) ? onConfirm && onConfirm() : onCancel && onCancel();
}
static scrollToBottom() {
window.scrollTo({
top: document.body.scrollHeight,
behavior: CONFIG.UI.ANIMATIONS.scrollBehavior
});
}
static isScrolledToBottom(threshold = 100) {
return document.body.scrollHeight - (window.pageYOffset || document.documentElement.scrollTop) - window.innerHeight <= threshold;
}
static findMainScrollContainer() {
const lastMessage = document.querySelector('[data-message-author-role]:last-of-type');
return lastMessage ? UIUtils.findScrollableParent(lastMessage) : window;
}
}
class StyleManager {
constructor() {
this.injectedStyles = new Map, this.styleOverrides = {};
}
injectCSS(id, css) {
if (this.injectedStyles.has(id)) return this.injectedStyles.get(id);
const style = document.createElement('style');
return style.id = `chatgpt-widescreen-${id}`, style.textContent = css, document.head.appendChild(style),
this.injectedStyles.set(id, style), style;
}
removeCSS(id) {
const style = this.injectedStyles.get(id);
style && style.parentNode && style.parentNode.removeChild(style), this.injectedStyles.delete(id);
}
setStyleOverrides(overrides = {}) {
this.styleOverrides = {
...this.styleOverrides,
...overrides
}, this.updateDynamicStyles();
}
getButtonStyles() {
return {
...CONFIG.BUTTON_STYLES,
...this.styleOverrides.buttons
};
}
updateDynamicStyles() {
const dynamicCSS = function (styleConfig = {}) {
return `\n ${function (config = {}) {
const { borderWidth: borderWidth = '2px', borderRadius: borderRadius = '8px', width: width = '38px', height: height = '38px', background: background = 'rgba(30, 30, 35, 0.95)', borderColor: borderColor = 'rgba(100, 150, 250, 0.6)', fontSize: fontSize = '16px', fontWeight: fontWeight = '700' } = config;
return `\n .chatgpt-widescreen-btn,\n .floating-collapse-btn,\n .floating-scroll-btn {\n border: ${borderWidth} solid;\n border-radius: ${borderRadius};\n width: ${width};\n height: ${height};\n background: ${background} !important;\n border-color: ${borderColor} !important;\n font-size: ${fontSize};\n font-weight: ${fontWeight};\n }\n `;
}(styleConfig.buttons || {})}\n \n /* 其他动态样式可以在这里添加 */\n `;
}({
buttons: this.getButtonStyles()
});
this.updateCSS('dynamic', dynamicCSS);
}
initAllStyles() {
this.injectCSS('base', '/* 基础样式重置和通用类 */\n.chatgpt-hidden {\n display: none !important;\n}\n\n.invisible {\n visibility: hidden !important;\n opacity: 0 !important;\n pointer-events: none !important;\n}\n\n.visible {\n visibility: visible !important;\n opacity: 1 !important;\n pointer-events: auto !important;\n}\n/* 通用过渡效果 */\n.transition-all {\n transition: all 0.2s ease-out;\n}\n\n.transition-opacity {\n transition: opacity 0.2s ease-out;\n}\n\n.transition-transform {\n transition: transform 0.2s ease-out;\n}\n\n/* 通用布局工具类 */\n.flex-center {\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.flex-between {\n display: flex;\n align-items: center;\n justify-content: space-between;\n}\n\n.absolute-center {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n}'),
this.injectCSS('widescreen', '/* ChatGPT 宽屏模式样式 */\n\n.chatgpt-widescreen {\n max-width: none !important;\n width: 90% !important;\n}\n\nhtml.light:root,\nhtml.dark:root {\n --user-chat-width: 100%;\n}\n/* 可调整大小的代码容器样式 */\n.chatgpt-resizable-code-container {\n position: relative;\n min-height: 400px;\n max-height: 1000px;\n height: 800px;\n border-radius: 6px;\n display: flex;\n flex-direction: column;\n}\n\n.chatgpt-code-block {\n background-color: transparent;\n color: #c9d1d9;\n font-family: Monaco, Menlo, "Ubuntu Mono", monospace;\n font-size: 14px;\n line-height: 1.45;\n border: none;\n margin: 0;\n padding: 16px;\n flex: 1;\n min-height: 0;\n white-space: pre-wrap;\n word-wrap: break-word;\n}\n\n.chatgpt-code-element {\n color: inherit;\n background-color: transparent;\n white-space: pre-wrap;\n word-wrap: break-word;\n}\n\n/* 调整大小手柄样式 */\n.chatgpt-resize-handle {\n height: 12px;\n cursor: ns-resize;\n display: flex;\n align-items: center;\n justify-content: center;\n border-bottom-left-radius: 6px;\n border-bottom-right-radius: 6px;\n transition: background-color 0.2s ease;\n}\n\n.chatgpt-resize-grip {\n width: 40px;\n height: 3px;\n background: #6e7681;\n border-radius: 2px;\n transition: background-color 0.2s ease;\n}\n\n.chatgpt-resize-handle-hover {\n background-color: #30363d;\n}\n\n.chatgpt-resize-handle-hover .chatgpt-resize-grip {\n background: #8b949e;\n}\n\n/* 通知样式 */\n.chatgpt-notification {\n position: fixed;\n top: 20px;\n right: 20px;\n color: white;\n padding: 12px 20px;\n border-radius: 8px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);\n z-index: 100000;\n font-size: 14px;\n font-weight: 500;\n max-width: 300px;\n transform: translateX(100%);\n transition: transform 0.3s ease-out;\n backdrop-filter: blur(10px);\n}\n\n.chatgpt-notification-success {\n background: rgba(34, 197, 94, 0.9);\n}\n\n.chatgpt-notification-error {\n background: rgba(239, 68, 68, 0.9);\n}\n\n.chatgpt-notification-warning {\n background: rgba(245, 158, 11, 0.9);\n}\n\n.chatgpt-notification-info {\n background: rgba(59, 130, 246, 0.9);\n}\n\n.chatgpt-notification-show {\n transform: translateX(0);\n}'),
this.injectCSS('buttons', '/* 浮动按钮容器样式 */\n.chatgpt-widescreen-float-panel {\n position: fixed;\n top: 50%;\n right: 20px;\n transform: translateY(-50%);\n z-index: 10000;\n display: flex;\n flex-direction: column;\n gap: 6px;\n pointer-events: none;\n max-height: 80vh;\n overflow-y: auto;\n scrollbar-width: none;\n -ms-overflow-style: none;\n}\n\n/* 浮动按钮基础样式 */\n.chatgpt-widescreen-btn,\n.floating-collapse-btn,\n.floating-scroll-btn {\n position: fixed;\n top: 50%;\n cursor: pointer;\n transition: all 0.2s ease-out;\n z-index: 10000 !important;\n backdrop-filter: blur(10px);\n opacity: 1 !important;\n pointer-events: auto !important;\n display: block;\n text-align: center;\n line-height: 1;\n padding: 0;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8);\n}\n.chatgpt-widescreen-btn {\n position: relative;\n}\n\n/* 折叠按钮专用样式 */\n.floating-collapse-btn {\n right: 70px;\n box-shadow: 0 3px 12px rgba(0, 0, 0, 0.4), 0 1px 3px rgba(100, 150, 250, 0.3) !important;\n}\n\n/* 跳转按钮专用样式 */\n.floating-scroll-btn {\n right: 20px;\n box-shadow: 0 3px 12px rgba(0, 0, 0, 0.4), 0 1px 3px rgba(150, 100, 250, 0.3) !important;\n}\n\n/* 通用悬停效果 */\n.chatgpt-widescreen-btn:hover,\n.floating-collapse-btn:hover,\n.floating-scroll-btn:hover {\n background: rgba(20, 20, 25, 0.98) !important;\n color: #ffffff !important;\n border-color: rgba(255, 255, 255, 0.9) !important;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5), 0 2px 8px rgba(200, 140, 240, 0.5) !important;\n}\n\n/* 用户消息按钮专用紫色样式 */\n.user-message-btn {\n border-color: rgba(180, 120, 220, 0.4) !important;\n box-shadow: 0 3px 12px rgba(0, 0, 0, 0.4), 0 1px 3px rgba(180, 120, 220, 0.4) !important;\n}\n\n.floating-collapse-btn.visible,\n.floating-scroll-btn.visible {\n opacity: 1 !important;\n pointer-events: auto !important;\n}\n\n/* 被控制的对话高亮效果 */\n.conversation-highlighted {\n position: relative;\n}\n\n.conversation-highlighted::before {\n content: \'\';\n position: absolute;\n left: -8px;\n top: 0;\n bottom: 0;\n width: 3px;\n background: linear-gradient(180deg, #64B5F6, #42A5F5);\n border-radius: 1.5px;\n opacity: 0.8;\n}\n\n/* 用户消息的折叠线显示在右边 - 紫色系优雅配色 */\n.conversation-highlighted[data-message-author-role="user"]::before {\n left: auto;\n right: -8px;\n background: linear-gradient(180deg, #BA68C8, #AB47BC);\n}\n\n/* 对话操作按钮样式 */\n.conversation-item-actions {\n opacity: 0;\n display: flex;\n gap: 4px;\n margin-left: auto;\n margin-right: 8px;\n transition: opacity 0.2s ease;\n z-index: 100;\n position: absolute;\n right: 8px;\n top: 50%;\n transform: translateY(-50%);\n}\n\n.conversation-item:hover .conversation-item-actions,\nnav a[href*="/c/"]:hover .conversation-item-actions,\nnav li:hover .conversation-item-actions {\n opacity: 1;\n}\n\n/* 始终显示按钮的选项(可选) */\n.conversation-item-actions.always-visible {\n opacity: 1;\n}\n\n.conversation-action-btn {\n width: 26px;\n height: 26px;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 13px;\n transition: all 0.2s ease;\n z-index: 101;\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);\n min-width: 26px;\n flex-shrink: 0;\n}\n\n.conversation-action-btn:hover {\n background: rgba(65, 65, 75, 0.95);\n color: rgba(220, 220, 235, 1);\n transform: scale(1.1);\n}\n\n/* 确保对话项有足够的空间容纳按钮 */\n.conversation-item {\n position: relative !important;\n align-items: center !important;\n}\n\n/* 通用对话项选择器 */\nnav a[href*="/c/"],\nnav div[role="menuitem"],\n.conversation-list-item,\n[data-testid*="conversation"]:has(a[href*="/c/"]) {\n position: relative !important;\n display: flex !important;\n align-items: center !important;\n min-height: 44px !important;\n padding-right: 80px !important;\n}\n\n/* 暗色主题样式 */\nhtml.dark .conversation-action-btn {\n background: rgba(35, 35, 40, 0.8);\n color: rgba(200, 200, 210, 0.9);\n}\n\nhtml.dark .conversation-action-btn:hover {\n background: rgba(55, 55, 65, 0.95);\n color: rgba(220, 220, 235, 1);\n}\n\n/* Markdown切换按钮样式 */\n.markdown-toggle-button {\n transition: all 0.2s ease;\n}\n\n.markdown-toggle-button:hover {\n background-color: rgba(0, 0, 0, 0.05) !important;\n transform: scale(1.05);\n}\n\nhtml.dark .markdown-toggle-button:hover {\n background-color: rgba(255, 255, 255, 0.1) !important;\n}\n\n.markdown-toggle-button .icon-md-heavy {\n transition: all 0.2s ease;\n}\n\n.markdown-toggle-button:active .icon-md-heavy {\n transform: scale(0.95);\n}\n\n/* 消息跳转高亮效果 */\n.message-navigation-highlight {\n position: relative;\n animation: messageHighlight 3s ease-out;\n}\n\n.message-navigation-highlight::before {\n content: \'\';\n position: absolute;\n left: -12px;\n top: -4px;\n bottom: -4px;\n width: 4px;\n background: linear-gradient(180deg, #4CAF50, #45a049);\n border-radius: 2px;\n opacity: 0.9;\n animation: highlightPulse 3s ease-out;\n}\n\n/* 用户消息的高亮显示在右边 */\n.message-navigation-highlight[data-message-author-role="user"]::before {\n left: auto;\n right: -12px;\n /* background: linear-gradient(180deg, #FF9800, #F57C00); */\n}\n\n@keyframes messageHighlight {\n 0% {\n background-color: rgba(76, 175, 80, 0.15);\n }\n\n 20% {\n background-color: rgba(76, 175, 80, 0.1);\n }\n\n 100% {\n background-color: transparent;\n }\n\n \n}\n@keyframes highlightPulse {\n 0% {\n opacity: 0.9;\n width: 4px;\n }\n\n 20% {\n opacity: 1;\n width: 6px;\n }\n\n 100% {\n opacity: 0.7;\n width: 4px;\n }\n}'),
this.injectCSS('messages', '/* 消息折叠和展开样式 */\n\n.direct-collapse pre {\n max-width: 100% !important;\n width: 100% !important;\n box-sizing: border-box !important;\n overflow-x: auto !important;\n white-space: pre-wrap !important;\n word-wrap: break-word !important;\n margin: 0 !important;\n}\n\n.direct-collapse code {\n white-space: pre-wrap !important;\n word-wrap: break-word !important;\n}\n\n.direct-collapse {\n max-height: 160px;\n overflow: hidden;\n position: relative;\n cursor: pointer;\n transition: all 0.3s ease;\n border-radius: 8px;\n}\n\n.direct-collapse:hover {\n background-color: rgba(255, 255, 255, 0.03);\n border-color: rgba(100, 150, 250, 0.3);\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n}\n\nhtml.dark .direct-collapse:hover {\n background-color: rgba(255, 255, 255, 0.08);\n border-color: rgba(100, 150, 250, 0.4);\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);\n}\n\n.direct-collapse::after {\n content: "点击展开完整内容 ↓";\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 100px;\n background: linear-gradient(transparent 20%, rgba(0, 0, 0, 0.7) 70%, rgba(0, 0, 0, 0.85) 100%);\n pointer-events: none;\n z-index: 10;\n display: flex;\n align-items: flex-end;\n justify-content: center;\n padding: 12px;\n font-size: 14px;\n color: #ffffff;\n font-weight: 600;\n text-shadow: 0 1px 3px rgba(0, 0, 0, 0.8);\n backdrop-filter: blur(0.5px);\n border-radius: 0 0 8px 8px;\n}\n\nhtml.dark .direct-collapse::after {\n background: linear-gradient(transparent 20%, rgba(0, 0, 0, 0.8) 70%, rgba(0, 0, 0, 0.9) 100%);\n color: #ffffff;\n text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);\n}\n\n/* 折叠区域动画效果 */\n.direct-collapse::before {\n content: "";\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n background: linear-gradient(45deg, \n rgba(100, 150, 250, 0.05) 0%, \n rgba(150, 100, 250, 0.05) 50%, \n rgba(100, 150, 250, 0.05) 100%);\n opacity: 0;\n transition: opacity 0.3s ease;\n z-index: 5;\n pointer-events: none;\n}\n\n.direct-collapse:hover::before {\n opacity: 1;\n}'),
this.updateDynamicStyles();
}
updateCSS(id, css) {
this.removeCSS(id), this.injectCSS(id, css);
}
static parseCSSValue(cssValue) {
return 'string' == typeof cssValue ? parseFloat(cssValue.replace(/px$/, '')) || 0 : cssValue || 0;
}
cleanup() {
this.injectedStyles.forEach((style, id) => {
this.removeCSS(id);
});
}
}
class MessageHandler {
constructor(app = null) {
this.messageButtons = new Map, this.hasAutoCollapsed = !settingsManager.get('autoCollapse'),
this.retryCount = 0, this.app = app, this.init();
}
init() {
this.setupEventListeners(), this.setupScrollObserver(), this.handleInitialMessages();
}
immediateScrollHandler(source) {
EventManager.dispatch(EVENTS_SCROLL_DETECTED, {
source: source
});
}
setupEventListeners() {
this.unlistenMessageUpdated = EventManager.listen([EVENTS_LONG_MESSAGES_UPDATED, EVENTS_SCROLL_DETECTED, EVENTS_PAGE_RESIZE], (data, event) => {
Logger.debug('按钮更新触发:' + event.type + ' source:' + data?.source), this.updateMessageButtonsState();
}), this.unlistenButtonCreated = EventManager.listen(EVENTS_BUTTON_CREATED, () => this.addMarkdownButtonToAllMessages()),
this.unlistenMessageCollapsed = EventManager.listen(EVENTS_MESSAGE_COLLAPSED, data => {
data && data.messages && (Logger.debug('检测到消息内容变化,重置相关Markdown按钮状态,影响消息数:', data.messages.length),
this.resetMarkdownButtonsForMessages(data.messages));
});
}
setupScrollObserver() {
window.addEventListener('wheel', () => this.immediateScrollHandler('wheel'), {
passive: !0
});
const debounceHandler = DOMUtils.debounce(() => this.immediateScrollHandler('intersection-observer'), 100), observer = new IntersectionObserver(entries => {
let hasChanges = !1;
entries.forEach(entry => {
!entry.isIntersecting && entry.isIntersecting || (hasChanges = !0);
}), hasChanges && debounceHandler();
}, {
threshold: [0, .1, .5, .9, 1],
rootMargin: '50px'
});
setTimeout(() => {
MessageHandler.getAllMessages().forEach(msg => observer.observe(msg));
}, 2e3), window.addEventListener('resize', DOMUtils.debounce(() => {
EventManager.dispatch(EVENTS_PAGE_RESIZE);
}, 500));
}
handleInitialMessages() {
setTimeout(() => {
this.updateMessageButtonsState(), this.addMarkdownButtonToAllMessages();
}, 1e3);
}
isMessageCollapsed(messageEl) {
return messageEl && messageEl.classList && messageEl.classList.contains(CSS_CLASSES_COLLAPSED);
}
collapseMessage(messageEl) {
messageEl && !this.isMessageCollapsed(messageEl) && (messageEl.classList.add(CSS_CLASSES_COLLAPSED),
this.addClickToExpandFeature(messageEl));
}
uncollapseMessage(messageEl) {
messageEl && this.isMessageCollapsed(messageEl) && messageEl.classList.remove(CSS_CLASSES_COLLAPSED);
}
autoCollapseMessage(messageEl) {
'true' !== messageEl.dataset.autoCollapsed && (this.collapseMessage(messageEl),
messageEl.dataset.autoCollapsed = 'true');
}
addClickToExpandFeature(messageEl) {
if (!messageEl || 'true' === messageEl.dataset.clickToExpandAdded) return;
messageEl.addEventListener('click', e => {
const target = e.target;
if ('BUTTON' !== target.tagName && 'A' !== target.tagName && !target.closest('button') && this.isMessageCollapsed(messageEl)) {
e.preventDefault(), e.stopPropagation(), this.uncollapseMessage(messageEl);
const buttons = this.messageButtons.get(messageEl);
buttons && buttons.collapseBtn && (buttons.collapseBtn.innerHTML = '▲'), setTimeout(() => {
this.immediateScrollHandler('click-to-expand');
}, 50);
}
}, !0), messageEl.dataset.clickToExpandAdded = 'true';
}
updateMessageButtonsState() {
const allMessages = MessageHandler.getAllMessages(), { longMessages: longMessages, hasEmptyContent: hasEmptyContent } = UIUtils.getLongMessages(allMessages, this.hasAutoCollapsed);
this.messageButtons.forEach((buttons, _messageEl) => {
buttons.collapseBtn && (buttons.collapseBtn.style.display = 'none'), buttons.scrollBtn && (buttons.scrollBtn.style.display = 'none');
}), longMessages.forEach((msgData, index) => {
const { messageEl: messageEl, _textLength: _textLength, _lineCount: _lineCount, _rect: _rect, _centerY: _centerY, _isUser: _isUser } = msgData;
let buttons = this.messageButtons.get(messageEl);
const container = UIUtils.findScrollableParent(messageEl);
container !== window && (buttons || (buttons = this.createFloatingButtonForMessage(container, messageEl)),
this.positionButtons(buttons, msgData, index, longMessages.length), this.hasAutoCollapsed || this.autoCollapseMessage(messageEl));
}), hasEmptyContent && this.retryCount < 10 && (this.updateMessageButtonsStateDebounceHandler || (this.updateMessageButtonsStateDebounceHandler = DOMUtils.debounce(() => {
this.retryCount++, this.updateMessageButtonsState();
}, 1e3)), this.updateMessageButtonsStateDebounceHandler()), !this.hasAutoCollapsed && (allMessages.length > 0 && !hasEmptyContent || this.retryCount >= 10) && (this.hasAutoCollapsed = !0,
this.retryCount >= 10 && hasEmptyContent && Logger.debug('内容加载重试已达到最大次数(10次),停止重试')),
this.cleanupRemovedMessages();
}
createFloatingButtonForMessage(container, messageEl) {
const collapseBtn = DOMUtils.createElement('button', {
className: 'floating-collapse-btn',
title: '点击折叠/展开这条回复'
}), scrollBtn = DOMUtils.createElement('button', {
className: 'floating-scroll-btn',
title: '跳转到消息顶部'
}, '↑'), actuallyCollapsed = this.isMessageCollapsed(messageEl);
collapseBtn.innerHTML = actuallyCollapsed ? '▼' : '▲', collapseBtn.addEventListener('click', e => {
e.preventDefault(), e.stopPropagation(), this.handleMessageButtonClick(messageEl, collapseBtn),
this.immediateScrollHandler('button-click');
}), scrollBtn.addEventListener('click', e => {
e.preventDefault(), e.stopPropagation(), UIUtils.scrollToMessageTop(messageEl),
this.immediateScrollHandler('button-click');
}), container.appendChild(collapseBtn), container.appendChild(scrollBtn);
const buttons = {
collapseBtn: collapseBtn,
scrollBtn: scrollBtn
};
return this.messageButtons.set(messageEl, buttons), buttons;
}
positionButtons(buttons, msgData, index, totalCount) {
const { messageEl: messageEl, rect: rect, isUser: isUser } = msgData, { collapseBtn: collapseBtn, scrollBtn: scrollBtn } = buttons;
collapseBtn.style.display = 'block', scrollBtn.style.display = 'block', collapseBtn.style.pointerEvents = 'auto',
scrollBtn.style.pointerEvents = 'auto', isUser && (collapseBtn.classList.add('user-message-btn'),
scrollBtn.classList.add('user-message-btn')), DOMUtils.addClass(messageEl, 'conversation-highlighted'),
this.calculateButtonPosition(buttons, rect, index, totalCount);
const isCollapsed = this.isMessageCollapsed(messageEl);
collapseBtn.textContent = isCollapsed ? '▼' : '▲', scrollBtn.textContent = '↑';
}
calculateButtonPosition(buttons, rect, _index, _totalCount) {
this.setAttachedPosition(buttons, rect);
}
setAttachedPosition(buttons, rect) {
const { collapseBtn: collapseBtn, scrollBtn: scrollBtn } = buttons;
collapseBtn.style.position = 'fixed', scrollBtn.style.position = 'fixed';
const buttonStyles = this.app.getButtonStyles(), buttonWidth = StyleManager.parseCSSValue(buttonStyles.width) + 2 * StyleManager.parseCSSValue(buttonStyles.borderWidth), buttonHeight = StyleManager.parseCSSValue(buttonStyles.height) + 2 * StyleManager.parseCSSValue(buttonStyles.borderWidth), buttonTop = Math.max(CONFIG.SCREEN_MARGIN, rect.top - buttonHeight), buttonBottom = buttonTop + buttonHeight;
collapseBtn.style.top = `${buttonTop}px`, scrollBtn.style.top = `${buttonTop}px`;
const viewportWidth = window.innerWidth;
let scrollButtonRight = 0, collapseButtonRight = 0;
const floatingPanelRect = document.querySelector(`.${CSS_CLASSES_FLOAT_PANEL}`)?.getBoundingClientRect();
floatingPanelRect && floatingPanelRect.top <= buttonBottom + 10 && floatingPanelRect.bottom >= buttonTop - 10 ? (scrollButtonRight = Math.max(viewportWidth - floatingPanelRect.right, 0) - 2 * buttonWidth,
scrollButtonRight < 0 && (scrollButtonRight = Math.max(viewportWidth - floatingPanelRect.right, 0) + buttonWidth)) : (scrollButtonRight = Math.max(viewportWidth - rect.right, 0) - 2 * buttonWidth - 10,
scrollButtonRight < 0 && (scrollButtonRight = Math.max(Math.max(viewportWidth - rect.right, 0) - buttonWidth, 0))),
collapseButtonRight = scrollButtonRight + buttonWidth, collapseBtn.style.right = collapseButtonRight + 'px',
scrollBtn.style.right = scrollButtonRight + 'px', collapseBtn.style.transform = 'none',
scrollBtn.style.transform = 'none';
}
handleMessageButtonClick(messageEl, button) {
'true' !== messageEl.dataset.processing && (messageEl.dataset.processing = 'true',
this.performToggleOperation(messageEl, button));
}
performToggleOperation(messageEl, button) {
this.isMessageCollapsed(messageEl) ? (this.uncollapseMessage(messageEl), button.innerHTML = '▲',
requestAnimationFrame(() => {
requestAnimationFrame(() => {
UIUtils.scrollToMessageTop(messageEl);
});
})) : (this.collapseMessage(messageEl), button.innerHTML = '▼'), setTimeout(() => delete messageEl.dataset.processing, 100);
}
cleanupRemovedMessages() {
this.messageButtons.forEach((buttons, messageEl) => {
DOMUtils.isInDOM(messageEl) || (buttons.collapseBtn && buttons.collapseBtn.remove(),
buttons.scrollBtn && buttons.scrollBtn.remove(), this.messageButtons.delete(messageEl));
});
}
static getAllMessages() {
const messages = [];
return document.querySelectorAll(`${CONFIG.SELECTORS.messages.user}, ${CONFIG.SELECTORS.messages.assistant}`).forEach(msg => {
msg.getAttribute('data-message-id').includes('0000-0000-0000') || messages.push(msg);
}), messages.sort((a, b) => a.compareDocumentPosition(b) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1);
}
static getCurrentMainMessage() {
const messages = MessageHandler.getAllMessages();
if (0 === messages.length) return null;
const scrollContainer = UIUtils.findMainScrollContainer();
Logger.debug('Finding current main message in container:', scrollContainer);
const viewportHeight = scrollContainer === window ? window.innerHeight : scrollContainer.clientHeight, scrollTop = scrollContainer === window ? window.scrollY : scrollContainer.scrollTop;
let bestMessage = null, bestScore = -1;
return messages.forEach(message => {
const rect = message.getBoundingClientRect(), messageTop = scrollContainer === window ? rect.top + scrollTop : rect.top + scrollTop - scrollContainer.getBoundingClientRect().top, messageBottom = messageTop + rect.height, visibleTop = Math.max(messageTop, scrollTop), visibleBottom = Math.min(messageBottom, scrollTop + viewportHeight), visibleHeight = Math.max(0, visibleBottom - visibleTop);
let score = visibleHeight / rect.height + visibleHeight / viewportHeight;
const messageCenter = (visibleTop + visibleBottom) / 2, viewportCenter = scrollTop + viewportHeight / 2;
score += .5 * (1 - Math.abs(messageCenter - viewportCenter) / (viewportHeight / 2)),
score > bestScore && (bestScore = score, bestMessage = message);
}), Logger.debug('Current main message selected with score:', bestScore, bestMessage),
bestMessage;
}
resetMarkdownButtonForMessage(messageEl) {
if (!messageEl) return;
const buttonsContainer = messageEl?.parentElement?.parentElement.querySelector('[data-testid*="turn-action"]')?.parentElement?.parentElement;
if (!buttonsContainer) return;
const markdownButton = buttonsContainer.querySelector('.markdown-toggle-button');
if (!markdownButton) return;
const state = markdownButton._markdownState;
if (!state) return;
const currentMessageId = messageEl.getAttribute('data-message-id');
if (currentMessageId && currentMessageId === state.lastMessageId) return;
if (state.isMarkdownView && state.originalContent && state.contentContainer) try {
state.contentContainer.innerHTML = state.originalContent;
} catch (error) {
console.warn('恢复原始内容时出错:', error);
}
markdownButton.setAttribute('aria-label', '显示Markdown源码'), markdownButton.title = '切换显示Markdown源码';
const buttonSpan = markdownButton.querySelector('span');
buttonSpan && (buttonSpan.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon-md-heavy">\n <path fill-rule="evenodd" clip-rule="evenodd" d="M7 5C7 3.89543 7.89543 3 9 3H15C16.1046 3 17 3.89543 17 5V7H15V5H9V19H15V17H17V19C17 20.1046 16.1046 21 15 21H9C7.89543 21 7 20.1046 7 19V5Z" fill="currentColor"/>\n <path d="M11 9H13V15H11V9Z" fill="currentColor"/>\n <path d="M9 11H11V13H9V11Z" fill="currentColor"/>\n <path d="M13 11H15V13H13V11Z" fill="currentColor"/>\n </svg>'),
state.isMarkdownView = !1, state.originalContent = null, state.lastMessageId = messageEl.getAttribute('data-message-id'),
state.messageEl = messageEl, Logger.debug('已重置消息相关的Markdown按钮状态:', messageEl.getAttribute('data-message-id'));
}
resetMarkdownButtonsForMessages(messageElements) {
messageElements.forEach(messageEl => {
this.resetMarkdownButtonForMessage(messageEl);
});
}
getOriginalMessageContent(messageEl) {
try {
const reactFiber = Object.keys(messageEl).find(key => key.startsWith('__reactFiber'));
if (!reactFiber) return console.warn('No React fiber found'), null;
const fiber = messageEl[reactFiber], searchFiberTree = (currentFiber, depth = 0) => {
if (depth > 15 || !currentFiber) return null;
const props = currentFiber.memoizedProps || currentFiber.pendingProps;
if (props && props.message && props.message.content && props.message.content.parts) return props.message.content.parts.join('\\n');
if (props && (props.conversationTurn || props.turn)) {
const turn = props.conversationTurn || props.turn;
if (turn.messages && Array.isArray(turn.messages)) for (const msg of turn.messages) if (msg.message && msg.message.content && msg.message.content.parts) return msg.message.content.parts.join('\\n');
}
return searchFiberTree(currentFiber.return, depth + 1) || searchFiberTree(currentFiber.child, depth + 1) || searchFiberTree(currentFiber.sibling, depth + 1);
}, content = searchFiberTree(fiber);
if (content) return Logger.debug('成功获取到消息内容,长度:', content.length), content;
const textContent = messageEl.textContent || messageEl.innerText || '';
return textContent && textContent.length > 20 ? (Logger.debug('React Fiber失败,使用文本内容,长度:', textContent.length),
textContent.trim()) : null;
} catch (error) {
return console.error('获取原始消息内容时出错:', error), null;
}
}
addMarkdownButtonToAllMessages() {
document.querySelectorAll(CONFIG.SELECTORS.messages.assistant).forEach(messageEl => {
this.addMarkdownButton(messageEl);
});
}
addMarkdownButton(messageEl, retry = 0) {
if ('assistant' !== messageEl.getAttribute('data-message-author-role')) return;
const buttonsContainer = messageEl?.parentElement?.parentElement.querySelector(CONFIG.SELECTORS.ui.turnActions)?.parentElement?.parentElement;
if (!buttonsContainer) return;
if (buttonsContainer.querySelector('.markdown-toggle-button')) return;
const markdownButton = this.createMarkdownButton(messageEl);
markdownButton ? this.insertMarkdownButton(buttonsContainer, markdownButton) : retry < 10 && (Logger.debug('消息内容可能未完全加载,稍后重试添加Markdown按钮,重试次数:', retry + 1),
retry++, setTimeout(() => {
this.addMarkdownButton(messageEl, retry);
}, 2e3 * retry));
}
createMarkdownButton(messageEl) {
const markdownButton = DOMUtils.createElement('span', {
'data-state': 'closed'
}), button = DOMUtils.createElement('button', {
className: 'rounded-lg text-token-text-secondary hover:bg-token-main-surface-secondary markdown-toggle-button',
'aria-label': '显示Markdown源码',
'data-testid': 'markdown-toggle-turn-action-button',
title: '切换显示Markdown源码'
}), buttonSpan = DOMUtils.createElement('span', {
className: 'flex h-[30px] w-[30px] items-center justify-center'
});
return buttonSpan.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon-md-heavy">\n <path fill-rule="evenodd" clip-rule="evenodd" d="M7 5C7 3.89543 7.89543 3 9 3H15C16.1046 3 17 3.89543 17 5V7H15V5H9V19H15V17H17V19C17 20.1046 16.1046 21 15 21H9C7.89543 21 7 20.1046 7 19V5Z" fill="currentColor"/>\n <path d="M11 9H13V15H11V9Z" fill="currentColor"/>\n <path d="M9 11H11V13H9V11Z" fill="currentColor"/>\n <path d="M13 11H15V13H13V11Z" fill="currentColor"/>\n </svg>',
button.appendChild(buttonSpan), markdownButton.appendChild(button), this.setupMarkdownButton(button, messageEl) ? markdownButton : null;
}
setupMarkdownButton(button, messageEl) {
const contentContainer = messageEl.querySelector('.markdown') || messageEl.querySelector('.prose') || messageEl.querySelector('[data-message-content]') || messageEl.querySelector('div[class*="markdown"]');
return contentContainer ? (button._markdownState = {
isMarkdownView: !1,
originalContent: null,
lastMessageId: messageEl.getAttribute('data-message-id'),
messageEl: messageEl,
contentContainer: contentContainer
}, button.addEventListener('click', e => {
this.handleMarkdownButtonClick(e, button);
}), !0) : (Logger.debug('找不到消息内容容器,跳过添加Markdown按钮', messageEl), !1);
}
handleMarkdownButtonClick(e, button) {
e.preventDefault(), e.stopPropagation();
const state = button._markdownState;
state.isMarkdownView ? this.showNormalView(button, state) : this.showMarkdownView(button, state),
state.messageEl && UIUtils.scrollToMessageTop(state.messageEl);
}
showMarkdownView(button, state) {
const currentMessageEl = this.findCurrentMessageElement(state.messageEl, button);
if (!currentMessageEl) return console.error('无法找到当前消息元素'), void alert('消息元素已改变,请刷新页面后重试');
this.uncollapseMessage(currentMessageEl);
const currentContentContainer = currentMessageEl.firstElementChild;
currentContentContainer.querySelectorAll('.dark, .light').forEach(el => {
el.classList.remove('dark', 'light');
}), state.originalContent = currentContentContainer.innerHTML;
const markdownContent = this.getOriginalMessageContent(currentMessageEl);
if (!markdownContent) return void alert('无法获取Markdown源码。可能原因:\\n1. 消息刚刚生成,请稍等再试\\n2. 页面需要刷新\\n3. 先展开消息再转换');
const codeContainer = UIUtils.createResizableCodeContainer(markdownContent);
currentContentContainer.innerHTML = '', currentContentContainer.appendChild(codeContainer),
state.contentContainer = currentContentContainer, state.messageEl = currentMessageEl,
state.lastMessageId = currentMessageEl.getAttribute('data-message-id'), this.updateMarkdownButtonState(button, !0),
state.isMarkdownView = !0;
}
showNormalView(button, state) {
if (state.originalContent) {
const currentMessageEl = this.findCurrentMessageElement(state.messageEl, button);
if (currentMessageEl) {
const currentContentContainer = currentMessageEl.querySelector('.markdown') || currentMessageEl.querySelector('.prose') || currentMessageEl.querySelector('[data-message-content]') || currentMessageEl.querySelector('div[class*="markdown"]');
currentContentContainer ? (currentContentContainer.innerHTML = state.originalContent,
state.contentContainer = currentContentContainer, state.messageEl = currentMessageEl,
state.lastMessageId = currentMessageEl.getAttribute('data-message-id')) : state.contentContainer.innerHTML = state.originalContent;
} else state.contentContainer.innerHTML = state.originalContent;
this.updateMarkdownButtonState(button, !1), state.isMarkdownView = !1;
}
}
updateMarkdownButtonState(button, isMarkdownView) {
const buttonSpan = button.querySelector('span');
isMarkdownView ? (button.setAttribute('aria-label', '显示正常内容'), button.title = '切换回正常显示',
buttonSpan.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon-md-heavy">\n <path fill-rule="evenodd" clip-rule="evenodd" d="M3 5C3 3.89543 3.89543 3 5 3H19C20.1046 3 21 3.89543 21 5V19C21 20.1046 20.1046 21 19 21H5C3.89543 21 3 20.1046 3 19V5ZM5 5H19V19H5V5Z" fill="currentColor"/>\n <path d="M7 7H17V9H7V7Z" fill="currentColor"/>\n <path d="M7 11H14V13H7V11Z" fill="currentColor"/>\n <path d="M7 15H12V17H7V15Z" fill="currentColor"/>\n </svg>') : (button.setAttribute('aria-label', '显示Markdown源码'),
button.title = '切换显示Markdown源码', buttonSpan.innerHTML = '<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="icon-md-heavy">\n <path fill-rule="evenodd" clip-rule="evenodd" d="M7 5C7 3.89543 7.89543 3 9 3H15C16.1046 3 17 3.89543 17 5V7H15V5H9V19H15V17H17V19C17 20.1046 16.1046 21 15 21H9C7.89543 21 7 20.1046 7 19V5Z" fill="currentColor"/>\n <path d="M11 9H13V15H11V9Z" fill="currentColor"/>\n <path d="M9 11H11V13H9V11Z" fill="currentColor"/>\n <path d="M13 11H15V13H13V11Z" fill="currentColor"/>\n </svg>');
}
insertMarkdownButton(buttonsContainer, markdownButton) {
const copyButtonContainer = buttonsContainer.querySelector('[data-testid="copy-turn-action-button"]')?.parentElement;
if (copyButtonContainer) copyButtonContainer.parentElement.insertBefore(markdownButton, copyButtonContainer.nextSibling); else {
const firstButtonGroup = buttonsContainer.querySelector('.flex.items-center') || buttonsContainer.firstElementChild;
firstButtonGroup ? firstButtonGroup.appendChild(markdownButton) : buttonsContainer.appendChild(markdownButton);
}
}
findCurrentMessageElement(originalMessageEl, button) {
try {
let currentElement = button;
for (;currentElement;) if (currentElement = currentElement.parentElement, currentElement) {
const elem = currentElement.querySelector('[data-message-author-role="assistant"]');
if (elem) return Logger.debug('通过按钮位置找到消息元素'), elem;
}
if (DOMUtils.isInDOM(originalMessageEl)) return Logger.debug('原消息元素仍然有效'), originalMessageEl;
} catch (error) {
Logger.error('查找消息元素时出错:', error);
}
return null;
}
destroy() {
this.unlistenMessageUpdated && (this.unlistenMessageUpdated(), this.unlistenMessageUpdated = null),
this.unlistenButtonCreated && (this.unlistenButtonCreated(), this.unlistenButtonCreated = null),
this.unlistenMessageCollapsed && (this.unlistenMessageCollapsed(), this.unlistenMessageCollapsed = null),
this.messageButtons.forEach(buttons => {
buttons.collapseBtn && buttons.collapseBtn.remove(), buttons.scrollBtn && buttons.scrollBtn.remove();
}), this.messageButtons.clear(), document.querySelectorAll('.conversation-highlighted').forEach(el => {
el.classList.remove('conversation-highlighted');
}), document.querySelectorAll(`.${CSS_CLASSES_COLLAPSED}`).forEach(el => {
el.classList.remove(CSS_CLASSES_COLLAPSED);
});
}
}
class ButtonManager {
constructor(app = null) {
this.floatPanel = null, this.currentMessage = null, this.app = app, this.buttons = new Map,
this.init();
}
init() {
this.createFloatPanel(), this.createButtons();
}
createFloatPanel() {
const existingPanel = document.querySelector(`.${CSS_CLASSES_FLOAT_PANEL}`);
existingPanel && DOMUtils.removeElement(existingPanel), this.floatPanel = UIUtils.createFloatingPanel(),
document.body.appendChild(this.floatPanel), this.unlistenFloatPanelEvents = EventManager.listen([EVENTS_PAGE_STYLE_CHANGED, EVENTS_PAGE_RESIZE], () => {
this.updateFloatPanelState();
});
}
updateFloatPanelState() {
if (!this.floatPanel) return;
const messageEl = MessageHandler.getCurrentMainMessage();
if (!messageEl) return;
const rect = messageEl.getBoundingClientRect(), buttonStyles = this.app.getButtonStyles(), buttonWidth = StyleManager.parseCSSValue(buttonStyles.width) + 2 * StyleManager.parseCSSValue(buttonStyles.borderWidth);
this.floatPanel.style.right = Math.max(window.innerWidth - rect.right - buttonWidth - 10, 0) + 'px';
}
createButtons() {
settingsManager.get('showFloatButtons') && (this.createWidescreenToggleButton(),
settingsManager.get('enableScroll') && this.createScrollButton(), settingsManager.get('enableMessageNavigation') && this.createMessageNavigationButtons());
}
createWidescreenToggleButton() {
const isActive = settingsManager.get('widescreenMode'), button = UIUtils.createFloatingButton(isActive ? '📱' : '🖥️', 'toggle-widescreen ' + (isActive ? 'active' : ''), isActive ? '退出宽屏模式' : '启用宽屏模式', () => this.toggleWidescreenMode());
this.buttons.set('widescreen', button), this.floatPanel.appendChild(button);
}
createScrollButton() {
const bottomButton = UIUtils.createFloatingButton('⬇️', 'scroll-bottom', '滚动到底部', () => this.scrollToBottom()), topButton = UIUtils.createFloatingButton('⬆️', 'scroll-top', '滚动到顶部', () => this.scrollToTop());
this.buttons.set('scroll-top', topButton), this.floatPanel.appendChild(topButton),
this.buttons.set('scroll-bottom', bottomButton), this.floatPanel.appendChild(bottomButton);
}
createMessageNavigationButtons() {
const prevButton = UIUtils.createFloatingButton('⬅️', 'message-prev', '跳转到上一则消息', () => this.navigateToMessage('prev')), nextButton = UIUtils.createFloatingButton('➡️', 'message-next', '跳转到下一则消息', () => this.navigateToMessage('next'));
this.buttons.set('message-prev', prevButton), this.floatPanel.appendChild(prevButton),
this.buttons.set('message-next', nextButton), this.floatPanel.appendChild(nextButton);
}
navigateToMessage(direction) {
const messages = MessageHandler.getAllMessages();
if (0 === messages.length) return void UIUtils.showNotification('没有找到消息', 'warning');
let currentMessage = this.currentMessage, targetMessage = null;
if (!currentMessage || document.body.contains(currentMessage) && DOMUtils.isElementVisible(currentMessage) || (Logger.debug('当前消息不在视图中,重置为null', currentMessage),
currentMessage = MessageHandler.getCurrentMainMessage(), this.currentMessage = currentMessage),
currentMessage) {
const currentIndex = messages.indexOf(currentMessage);
if (-1 === currentIndex) return;
targetMessage = 'prev' === direction ? currentIndex > 0 ? messages[currentIndex - 1] : messages[messages.length - 1] : currentIndex < messages.length - 1 ? messages[currentIndex + 1] : messages[0];
} else targetMessage = 'prev' === direction ? messages[messages.length - 1] : messages[0];
targetMessage && (this.currentMessage = targetMessage, this.scrollToMessage(targetMessage, direction));
}
scrollToMessage(message, direction) {
message && (Logger.debug('Scrolling to message:', message, 'Direction:', direction),
UIUtils.scrollToMessageTop(message, -.05), this.highlightMessage(message));
}
highlightMessage(message) {
document.querySelectorAll('.message-navigation-highlight').forEach(el => {
el.classList.remove('message-navigation-highlight');
}), message.classList.add('message-navigation-highlight'), setTimeout(() => {
message.classList.remove('message-navigation-highlight');
}, 3e3);
}
toggleWidescreenMode() {
const newMode = !settingsManager.get('widescreenMode');
settingsManager.set('widescreenMode', newMode);
const button = this.buttons.get('widescreen');
button && (button.textContent = newMode ? '📱' : '🖥️', button.title = newMode ? '退出宽屏模式' : '启用宽屏模式',
DOMUtils.toggleClass(button, 'active')), UIUtils.showNotification(newMode ? '宽屏模式已启用' : '宽屏模式已关闭', 'success'),
EventManager.dispatch(EVENTS_WIDESCREEN_TOGGLED, {
enabled: newMode
});
}
scrollToBottom() {
const scrollContainer = UIUtils.findMainScrollContainer();
scrollContainer && scrollContainer !== window ? scrollContainer.scrollTo({
top: scrollContainer.scrollHeight,
behavior: CONFIG.UI.ANIMATIONS.scrollBehavior
}) : UIUtils.scrollToBottom();
}
scrollToTop() {
const scrollContainer = UIUtils.findMainScrollContainer();
scrollContainer && scrollContainer !== window ? scrollContainer.scrollTo({
top: 0,
behavior: CONFIG.UI.ANIMATIONS.scrollBehavior
}) : UIUtils.scrollToTop();
}
startNewChat() {
const newChatButton = DOMUtils.findElement(['a[href="/"]', 'button[data-testid="new-chat-button"]', '[data-testid="new-chat"]', 'nav a[href="/"]']);
newChatButton ? (newChatButton.click(), UIUtils.showNotification('正在创建新对话...', 'info')) : window.location.href = '/';
}
toggleFloatPanel(visible = null) {
if (!this.floatPanel) return;
(null !== visible ? visible : DOMUtils.hasClass(this.floatPanel, CSS_CLASSES_HIDDEN)) ? DOMUtils.removeClass(this.floatPanel, CSS_CLASSES_HIDDEN) : DOMUtils.addClass(this.floatPanel, CSS_CLASSES_HIDDEN);
}
destroy() {
this.floatPanel && (DOMUtils.removeElement(this.floatPanel), this.floatPanel = null),
this.unlistenFloatPanelEvents && (this.unlistenFloatPanelEvents(), this.unlistenFloatPanelEvents = null),
this.buttons.clear();
}
}
class ConversationManager {
constructor() {
this.conversationButtons = new Map, this.init();
}
init() {
this.addConversationActionButtons(), this.bindEvents();
}
bindEvents() {
this.unlistenConversationAction = EventManager.listen(EVENTS_CONVERSATION_ACTION, data => {
data && 'check' === data.type && this.addConversationActionButtons();
});
}
addConversationActionButtons() {
const conversationItems = document.querySelectorAll('li[data-testid^="history-item-"]');
conversationItems.forEach(item => {
if ('true' === item.dataset.buttonsAdded) return;
const a = item.querySelector('a');
if (!(a.href && (a.href.includes('/c/') || a.href.includes('/chat/')))) return;
item.classList.add('conversation-item');
const actionsContainer = document.createElement('div');
actionsContainer.className = 'conversation-item-actions';
const renameBtn = this.createActionButton('重命名对话', 'rename', '\n <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0">\n <path fill-rule="evenodd" clip-rule="evenodd" d="M13.2929 4.29291C15.0641 2.52167 17.9359 2.52167 19.7071 4.2929C21.4784 6.06414 21.4784 8.93588 19.7071 10.7071L18.7073 11.7069L11.6135 18.8007C10.8766 19.5376 9.92793 20.0258 8.89999 20.1971L4.16441 20.9864C3.84585 21.0395 3.52127 20.9355 3.29291 20.7071C3.06454 20.4788 2.96053 20.1542 3.01362 19.8356L3.80288 15.1C3.9742 14.0721 4.46243 13.1234 5.19932 12.3865L13.2929 4.29291ZM13 7.41422L6.61353 13.8007C6.1714 14.2428 5.87846 14.8121 5.77567 15.4288L5.21656 18.7835L8.57119 18.2244C9.18795 18.1216 9.75719 17.8286 10.1993 17.3865L16.5858 11L13 7.41422ZM18 9.5858L14.4142 6.00001L14.7071 5.70712C15.6973 4.71693 17.3027 4.71693 18.2929 5.70712C19.2831 6.69731 19.2831 8.30272 18.2929 9.29291L18 9.5858Z" fill="currentColor"></path>\n </svg>\n ');
renameBtn.onclick = e => {
e.preventDefault(), e.stopPropagation(), this.handleRenameConversation(item);
};
const deleteBtn = this.createActionButton('删除对话', 'delete', '\n <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0">\n <path fill-rule="evenodd" clip-rule="evenodd" d="M10.5555 4C10.099 4 9.70052 4.30906 9.58693 4.75114L9.29382 5.8919H14.715L14.4219 4.75114C14.3083 4.30906 13.9098 4 13.4533 4H10.5555ZM16.7799 5.8919L16.3589 4.25342C16.0182 2.92719 14.8226 2 13.4533 2H10.5555C9.18616 2 7.99062 2.92719 7.64985 4.25342L7.22886 5.8919H4C3.44772 5.8919 3 6.33961 3 6.8919C3 7.44418 3.44772 7.8919 4 7.8919H4.10069L5.31544 19.3172C5.47763 20.8427 6.76455 22 8.29863 22H15.7014C17.2354 22 18.5224 20.8427 18.6846 19.3172L19.8993 7.8919H20C20.5523 7.8919 21 7.44418 21 6.8919C21 6.33961 20.5523 5.8919 20 5.8919H16.7799ZM17.888 7.8919H6.11196L7.30423 19.1057C7.3583 19.6142 7.78727 20 8.29863 20H15.7014C16.2127 20 16.6417 19.6142 16.6958 19.1057L17.888 7.8919ZM10 10C10.5523 10 11 10.4477 11 11V16C11 16.5523 10.5523 17 10 17C9.44772 17 9 16.5523 9 16V11C9 10.4477 9.44772 10 10 10ZM14 10C14.5523 10 15 10.4477 15 11V16C15 16.5523 14.5523 17 14 17C13.4477 17 13 16.5523 13 16V11C13 10.4477 13.4477 10 14 10Z" fill="currentColor"></path>\n </svg>\n ');
deleteBtn.onclick = e => {
e.preventDefault(), e.stopPropagation(), this.handleDeleteConversation(item);
};
const archiveBtn = this.createActionButton('归档对话', 'archive', '\n <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 shrink-0">\n <path fill-rule="evenodd" clip-rule="evenodd" d="M4.82918 4.10557C5.16796 3.428 5.86049 3 6.61803 3H17.382C18.1395 3 18.832 3.428 19.1708 4.10557L20.7889 7.34164C20.9277 7.61935 21 7.92558 21 8.23607V18C21 19.6569 19.6569 21 18 21H6C4.34315 21 3 19.6569 3 18V8.23607C3 7.92558 3.07229 7.61935 3.21115 7.34164L4.82918 4.10557ZM17.382 5H6.61803L5.61803 7H18.382L17.382 5ZM19 9H5V18C5 18.5523 5.44772 19 6 19H18C18.5523 19 19 18.5523 19 18V9ZM9 12C9 11.4477 9.44772 11 10 11H14C14.5523 11 15 11.4477 15 12C15 12.5523 14.5523 13 14 13H10C9.44772 13 9 12.5523 9 12Z" fill="currentColor"></path>\n </svg>\n ');
archiveBtn.onclick = e => {
e.preventDefault(), e.stopPropagation(), this.handleArchiveConversation(item);
}, actionsContainer.appendChild(renameBtn), actionsContainer.appendChild(archiveBtn),
actionsContainer.appendChild(deleteBtn);
const nextSibling = a.nextSibling;
nextSibling && DOMUtils.hide(nextSibling), a.parentNode.insertBefore(actionsContainer, nextSibling),
item.dataset.buttonsAdded = 'true';
const conversationId = this.extractConversationId(a);
conversationId && this.conversationButtons.set(conversationId, {
container: actionsContainer,
renameBtn: renameBtn,
deleteBtn: deleteBtn,
archiveBtn: archiveBtn,
item: item
});
}), conversationItems.length > 0 && Logger.debug(`[ConversationManager] 共处理了 ${conversationItems.length} 个对话项`);
}
createActionButton(title, type, svgIcon) {
const button = document.createElement('button');
return button.className = `conversation-action-btn ${type}-btn`, button.title = title,
button.innerHTML = svgIcon, button;
}
extractConversationId(linkElement) {
if (linkElement.href) {
const match = linkElement.href.match(/\/c\/([^\/\?]+)/);
return match ? match[1] : null;
}
return null;
}
handleRenameConversation(item) {
try {
const originalMenuBtn = this.findOriginalMenuButton(item);
Logger.debug('Original Menu Button for Rename:', originalMenuBtn), originalMenuBtn && (UIUtils.triggerMouseEvents(originalMenuBtn),
setTimeout(() => {
const menuItems = document.querySelectorAll('[role="menuitem"]');
Logger.debug('菜单项数量:', menuItems.length, menuItems);
for (const menuItem of menuItems) if (menuItem.textContent.includes('重命名') || menuItem.textContent.includes('Rename') || menuItem.textContent.includes('编辑')) {
UIUtils.triggerMouseEvents(menuItem, 'fiber');
break;
}
}, 50));
} catch (error) {
console.error('重命名对话时出错:', error), UIUtils.showNotification('重命名失败,请重试', 'error');
}
}
handleDeleteConversation(item) {
try {
const originalMenuBtn = this.findOriginalMenuButton(item);
Logger.debug('开始触发删除菜单按钮事件'), UIUtils.analyzeButton(originalMenuBtn), UIUtils.triggerMouseEvents(originalMenuBtn),
setTimeout(() => {
const menuItems = document.querySelectorAll('[role="menuitem"], .menu-item, [data-testid*="delete"]');
for (const menuItem of menuItems) if (Logger.debug('找到菜单项:', menuItem.textContent),
menuItem.textContent.includes('删除') || menuItem.textContent.includes('Delete') || menuItem.textContent.includes('移除')) {
UIUtils.triggerMouseEvents(menuItem, 'fiber'), setTimeout(() => {
const confirmBtn = document.querySelector('[data-testid*="delete-conversation-confirm-button"]');
confirmBtn && (Logger.debug('确认按钮:', confirmBtn), UIUtils.triggerMouseEvents(confirmBtn, 'fiber'));
}, 200);
break;
}
}, 100);
} catch (error) {
console.error('删除对话时出错:', error), UIUtils.showNotification('删除失败,请重试', 'error');
}
}
handleArchiveConversation(item) {
try {
const originalMenuBtn = this.findOriginalMenuButton(item);
UIUtils.analyzeButton(originalMenuBtn), UIUtils.triggerMouseEvents(originalMenuBtn, 'click'),
setTimeout(() => {
const menuItems = document.querySelectorAll('[role="menuitem"], .menu-item, [data-testid*="archive"]');
for (const menuItem of menuItems) if (menuItem.textContent.includes('归档') || menuItem.textContent.includes('Archive') || menuItem.textContent.includes('存档')) {
UIUtils.triggerMouseEvents(menuItem, 'fiber');
break;
}
}, 200);
} catch (error) {
Logger.error('归档对话时出错:', error), UIUtils.showNotification('归档失败,请重试', 'error');
}
}
findOriginalMenuButton(conversationItem) {
return Logger.debug('查找菜单按钮,对话项:', conversationItem), conversationItem.querySelector('button[data-testid*="options"]');
}
destroy() {
this.unlistenConversationAction && (this.unlistenConversationAction(), this.unlistenConversationAction = null),
this.conversationButtons.clear();
}
}
const app = new class {
constructor() {
this.buttonManager = null, this.messageHandler = null, this.conversationManager = null,
this.styleManager = null, this.initialized = !1, this.init = this.init.bind(this),
this.reinit = this.reinit.bind(this);
}
async init() {
if (!this.initialized) try {
Logger.info('[PlusAI Widescreen] 开始初始化...'), await this.waitForPageReady(), this.initializeComponents(),
this.applyInitialSettings(), this.bindGlobalEvents(), this.initialized = !0, Logger.info('[PlusAI Widescreen] 初始化完成');
} catch (error) {
console.error('[PlusAI Widescreen] 初始化失败:', error), UIUtils.showNotification('初始化失败,请刷新页面重试', 'error');
}
}
async waitForPageReady() {
await DOMUtils.waitForElement('main, [role="main"], #app', document, 1e4);
const promises = ['textarea[data-id]', '[data-testid]', '.text-base'].map(selector => DOMUtils.waitForElement(selector, document, 3e3).catch(() => null));
await Promise.race(promises), await new Promise(resolve => setTimeout(resolve, 1e3));
}
getButtonStyles() {
return this.styleManager ? this.styleManager.getButtonStyles() : {};
}
initializeComponents() {
Logger.info('[PlusAI Widescreen] 初始化组件...'), this.styleManager || (this.styleManager = new StyleManager,
this.styleManager.initAllStyles()), this.buttonManager || (this.buttonManager = new ButtonManager(this)),
this.messageHandler || (this.messageHandler = new MessageHandler(this)), this.conversationManager || (this.conversationManager = new ConversationManager),
Logger.info('[PlusAI Widescreen] 组件初始化完成');
}
applyInitialSettings() {
Logger.info('[PlusAI Widescreen] 应用初始设置...'), settingsManager.get('widescreenMode') ? (this.widescreenMode = !0,
this.applyWidescreenMode()) : this.widescreenMode = !1, Logger.info('[PlusAI Widescreen] 初始设置应用完成');
}
applyWidescreenMode() {
DOMUtils.findElements([CONFIG.SELECTORS.TEXT_CONTAINER].join(', ')).forEach(container => {
this.widescreenMode ? DOMUtils.addClass(container, CSS_CLASSES_WIDESCREEN) : DOMUtils.removeClass(container, CSS_CLASSES_WIDESCREEN);
}), EventManager.dispatch(EVENTS_PAGE_STYLE_CHANGED), EventManager.dispatch(EVENTS_SCROLL_DETECTED, {
source: 'widescreenModeChange'
});
}
bindGlobalEvents() {
this.mutationObserver || this.setupUnifiedMutationObserver(), this.unlisten || (this.unlisten = EventManager.listen(EVENTS_WIDESCREEN_TOGGLED, e => {
this.widescreenMode = e.enabled, this.applyWidescreenMode();
}));
}
setupUnifiedMutationObserver() {
let hasNewMessages = !1, needsButtonCheck = !1, needsConversationCheck = !1;
const changedMessages = new Set;
let currentUrl = window.location.href;
const observer = new MutationObserver(mutations => {
window.location.href !== currentUrl && (currentUrl = window.location.href, Logger.info('[PlusAI Widescreen] URL变化,重新初始化...'),
EventManager.dispatch(EVENTS_PAGE_NAVIGATION, {
from: currentUrl,
to: window.location.href
}), setTimeout(() => this.reinit(), 1e3)), mutations.forEach(mutation => {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE && node.querySelector) {
node.querySelector('[data-message-author-role]') && (hasNewMessages = !0, EventManager.dispatch(EVENTS_MESSAGE_ADDED, {
element: node
}));
if (node.querySelector('[data-testid*="action-button"]') && (needsButtonCheck = !0,
hasNewMessages = !0), settingsManager.get('enableConversationOps') && node.querySelector('[data-testid^="history-item"]') && (needsConversationCheck = !0),
node.querySelector('.markdown') || node.querySelector('.prose') || node.getAttribute('data-message-author-role')) {
const messageEl = node.closest ? node.closest('[data-message-author-role="assistant"]') : null;
!messageEl && node.getAttribute && 'assistant' === node.getAttribute('data-message-author-role') ? changedMessages.add(node) : messageEl && changedMessages.add(messageEl);
}
}
}), mutation.removedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE && node.querySelector && (node.querySelector('.markdown') || node.querySelector('.prose') || node.getAttribute('data-message-author-role'))) {
const parentMessageEl = mutation.target.closest('[data-message-author-role="assistant"]');
parentMessageEl && changedMessages.add(parentMessageEl);
}
});
}), (hasNewMessages || needsButtonCheck || changedMessages.size > 0) && setTimeout(() => {
hasNewMessages && (EventManager.dispatch(EVENTS_LONG_MESSAGES_UPDATED), hasNewMessages = !1),
needsButtonCheck && (EventManager.dispatch(EVENTS_BUTTON_CREATED), needsButtonCheck = !1),
changedMessages.size > 0 && (EventManager.dispatch(EVENTS_MESSAGE_COLLAPSED, {
messages: Array.from(changedMessages)
}), changedMessages.clear()), this.applyWidescreenMode();
}, 100), needsConversationCheck && setTimeout(() => {
EventManager.dispatch(EVENTS_CONVERSATION_ACTION, {
type: 'check'
}), needsConversationCheck = !1;
}, 200);
});
observer.observe(document.body, {
childList: !0,
subtree: !0
}), this.mutationObserver = observer;
}
checkAndReinit() {
const floatPanel = document.querySelector(`.${CSS_CLASSES_FLOAT_PANEL}`), hasMessages = document.querySelector(CONFIG.SELECTORS.CONVERSATION);
(!floatPanel || hasMessages && !floatPanel.children.length) && (Logger.info('[PlusAI Widescreen] 检测到组件丢失,重新初始化...'),
this.reinit());
}
async reinit() {
Logger.info('[PlusAI Widescreen] 重新初始化...');
try {
this.destroy(), this.initialized = !1, await new Promise(resolve => setTimeout(resolve, 500)),
await this.init();
} catch (error) {
console.error('[PlusAI Widescreen] 重新初始化失败:', error);
}
}
destroy() {
Logger.info('[PlusAI Widescreen] 清理资源...'), this.initialized = !1;
}
getStatus() {
return {
initialized: this.initialized,
widescreenMode: settingsManager.get('widescreenMode'),
settings: settingsManager.getAll(),
components: {
buttonManager: !!this.buttonManager,
messageHandler: !!this.messageHandler,
conversationManager: !!this.conversationManager
}
};
}
};
return 'loading' === document.readyState ? document.addEventListener('DOMContentLoaded', app.init) : setTimeout(app.init, 100),
'undefined' != typeof window && (window.ChatGPTWidescreenApp = app), Logger.info('[PlusAI Widescreen] 应用已加载,版本:', CONFIG.VERSION),
Logger.debug('[PlusAI Widescreen] 调试模式已启用'), app;
}();