Last active
May 22, 2025 10:34
-
-
Save eric15342335/d6cce128f946d81e989d84cce96906c8 to your computer and use it in GitHub Desktop.
ai-studio-easy-use.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Google AI Studio easy use | |
// @namespace http://tampermonkey.net/ | |
// @version 1.1.4-fork | |
// @description Automatically set Google AI Studio system prompt; Increase chat content font size; Toggle Grounding with Ctrl/Cmd + i. Dark theme support for script UI. 自动设置 Google AI Studio 的系统提示词;增大聊天内容字号;快捷键 Ctrl/Cmd + i 开关Grounding;脚本UI支持深色主题。 | |
// @author Victor Cheng (Modified by User) | |
// @match https://aistudio.google.com/* | |
// @match https://ai.dev/* | |
// @grant none | |
// @license MIT | |
// @run-at document-end | |
// @downloadURL https://update.greasyfork.org/scripts/523344/Google%20AI%20Studio%20easy%20use.user.js | |
// @updateURL https://update.greasyfork.org/scripts/523344/Google%20AI%20Studio%20easy%20use.meta.js | |
// ==/UserScript== | |
(function() { | |
'use strict'; | |
//======================================= | |
// 常量管理 | |
//======================================= | |
const CONSTANTS = { | |
STORAGE_KEYS: { | |
SYSTEM_PROMPT: 'aiStudioSystemPrompt', | |
FONT_SIZE: 'aiStudioFontSize' | |
}, | |
DEFAULTS: { | |
SYSTEM_PROMPT: '1. Answer in the same language as the question.\n2. If web search is necessary, always search in English.', | |
FONT_SIZE: 'medium' | |
}, | |
SELECTORS: { | |
NAVIGATION: '[role="navigation"]', | |
SYSTEM_INSTRUCTIONS: '.toolbar-system-instructions', | |
SYSTEM_INSTRUCTIONS_BUTTON: 'button[aria-label="System instructions"]', | |
SYSTEM_TEXTAREA: '.toolbar-system-instructions textarea', | |
NEW_CHAT_LINK: 'a[href$="/prompts/new_chat"]', | |
SEARCH_TOGGLE: '.search-as-a-tool-toggle button', | |
CHAT_LINKS: '.nav-sub-items-wrapper a', | |
CHAT_CONTENT_PARAGRAPHS: 'ms-cmark-node p', // Selector for chat content paragraphs | |
}, | |
FONT_SIZES: [ | |
{ value: 'small', label: 'Small', size: '12px' }, | |
{ value: 'medium', label: 'Medium', size: '14px' }, | |
{ value: 'large', label: 'Large', size: '16px' }, | |
{ value: 'x-large', label: 'X-large', size: '18px' }, | |
{ value: 'xx-large', label: 'XX-large', size: '20px' } | |
], | |
SHORTCUTS: { | |
TOGGLE_GROUNDING: { key: 'i', requiresCmd: true }, | |
NEW_CHAT: { key: 'j', requiresCmd: true }, | |
SWITCH_CHAT: { key: '/', requiresCmd: true } | |
}, | |
THEME_COLORS: { | |
LIGHT: { | |
BACKGROUND_PRIMARY: 'white', | |
BACKGROUND_SECONDARY: '#f8f9fa', | |
BACKGROUND_INPUT: 'white', | |
TEXT_PRIMARY: '#202124', | |
TEXT_SECONDARY: '#3c4043', | |
TEXT_MUTED: '#5f6368', | |
TEXT_INPUT: '#202124', | |
BORDER_PRIMARY: '#dadce0', | |
BORDER_INPUT: '#dadce0', | |
ACCENT_PRIMARY: '#076eff', | |
ACCENT_PRIMARY_TEXT: 'white', | |
ACCENT_SECONDARY_BACKGROUND: '#e8f0fe', | |
BUTTON_SECONDARY_BG: '#f8f9fa', | |
BUTTON_SECONDARY_TEXT: '#3c4043', | |
BUTTON_SECONDARY_BORDER: '#dadce0', | |
KBD_BG: '#f1f3f4', | |
KBD_TEXT: '#202124', | |
KBD_BORDER: '#d1d5da', | |
DIALOG_SHADOW: '0 2px 10px rgba(0,0,0,0.1)', | |
}, | |
DARK: { | |
BACKGROUND_PRIMARY: '#2d2d2d', | |
BACKGROUND_SECONDARY: '#3c3c3c', | |
BACKGROUND_INPUT: '#383838', | |
TEXT_PRIMARY: '#e0e0e0', | |
TEXT_SECONDARY: '#b0b0b0', | |
TEXT_MUTED: '#888888', | |
TEXT_INPUT: '#e0e0e0', | |
BORDER_PRIMARY: '#4a4a4a', | |
BORDER_INPUT: '#555555', | |
ACCENT_PRIMARY: '#2979ff', | |
ACCENT_PRIMARY_TEXT: '#ffffff', | |
ACCENT_SECONDARY_BACKGROUND: '#00439e', | |
BUTTON_SECONDARY_BG: '#4a4a4a', | |
BUTTON_SECONDARY_TEXT: '#e0e0e0', | |
BUTTON_SECONDARY_BORDER: '#5f5f5f', | |
KBD_BG: '#4f4f4f', | |
KBD_TEXT: '#e0e0e0', | |
KBD_BORDER: '#666666', | |
DIALOG_SHADOW: '0 4px 15px rgba(0,0,0,0.3)', | |
} | |
} | |
}; | |
//======================================= | |
// 工具类 | |
//======================================= | |
class DOMUtils { | |
static createElement(tag, attributes = {}, styles = {}) { | |
const element = document.createElement(tag); | |
Object.entries(attributes).forEach(([key, value]) => { | |
if (key === 'textContent') { | |
element.textContent = value; | |
} else if (key === 'className') { | |
element.className = value; | |
} else { | |
element.setAttribute(key, value); | |
} | |
}); | |
Object.assign(element.style, styles); | |
return element; | |
} | |
static querySelector(selector, parent = document) { | |
return parent.querySelector(selector); | |
} | |
static querySelectorAll(selector, parent = document) { | |
return parent.querySelectorAll(selector); | |
} | |
static isElementVisible(element) { | |
if (!element) return false; | |
const style = window.getComputedStyle(element); | |
return style.display !== 'none' && style.visibility !== 'hidden' && element.offsetParent !== null; | |
} | |
static async waitForElement(selector, timeout = 3000, parent = document) { | |
return new Promise((resolve) => { | |
const intervalTime = 100; | |
let elapsedTime = 0; | |
const interval = setInterval(() => { | |
const element = DOMUtils.querySelector(selector, parent); | |
if (element && DOMUtils.isElementVisible(element)) { | |
clearInterval(interval); | |
resolve(element); | |
} else if (elapsedTime >= timeout) { | |
clearInterval(interval); | |
resolve(null); // Resolve with null if timeout | |
} | |
elapsedTime += intervalTime; | |
}, intervalTime); | |
}); | |
} | |
} | |
class StyleManager { | |
static createStyleSheet(id, css) { | |
let style = document.getElementById(id); | |
if (!style) { | |
style = DOMUtils.createElement('style', { id }); | |
document.head.appendChild(style); | |
} | |
style.textContent = css; | |
return style; | |
} | |
static updateFontSize(size) { | |
const fontSize = CONSTANTS.FONT_SIZES.find(s => s.value === size)?.size || CONSTANTS.DEFAULTS.FONT_SIZE.size; | |
this.createStyleSheet('aiStudioCustomFontSizeStyle', ` | |
${CONSTANTS.SELECTORS.CHAT_CONTENT_PARAGRAPHS} { | |
font-size: ${fontSize} !important; | |
} | |
`); | |
} | |
} | |
class SystemPromptManager { | |
static async isTAAccessible(textarea) { | |
return new Promise(resolve => { | |
if (!textarea || !DOMUtils.isElementVisible(textarea) || textarea.disabled || textarea.readOnly) { | |
resolve(false); | |
} | |
const isActiveElement = document.activeElement === textarea; | |
if (isActiveElement) { | |
resolve(true); | |
return; | |
} | |
const originalActiveElement = document.activeElement; | |
textarea.focus(); | |
setTimeout(() => { | |
const focused = document.activeElement === textarea; | |
if (originalActiveElement && typeof originalActiveElement.focus === 'function') { | |
originalActiveElement.focus(); | |
} else { | |
textarea.blur(); | |
} | |
resolve(focused); | |
}, 50); | |
}); | |
} | |
static async update(prompt) { | |
console.log("SystemPromptManager: Attempting to update system prompt."); | |
const systemInstructionsButton = await DOMUtils.waitForElement(CONSTANTS.SELECTORS.SYSTEM_INSTRUCTIONS_BUTTON, 2000); | |
if (!systemInstructionsButton) { | |
console.warn("SystemPromptManager: System instructions button not found or not visible. Cannot proceed with prompt update."); | |
return false; | |
} | |
console.log("SystemPromptManager: System instructions button found."); | |
let textarea = DOMUtils.querySelector(CONSTANTS.SELECTORS.SYSTEM_TEXTAREA); | |
let isTextareaCurrentlyAccessible = textarea ? await SystemPromptManager.isTAAccessible(textarea) : false; | |
// Case 1: Textarea is accessible AND prompt is already correct. | |
if (isTextareaCurrentlyAccessible && textarea.value === prompt) { | |
console.log("SystemPromptManager: Textarea accessible and prompt already matches. No update needed."); | |
return true; | |
} | |
// Case 2: Textarea is not accessible (panel likely closed), so we need to open it. | |
// Or, it is accessible, but the prompt is different (handled by falling through to Case 3). | |
if (!isTextareaCurrentlyAccessible) { | |
console.log("SystemPromptManager: Textarea not accessible. Clicking button to open panel."); | |
systemInstructionsButton.click(); | |
const maxAttempts = 30; // 30 * 100ms = 3 seconds | |
let attempts = 0; | |
let panelOpenedAndTextareaReady = false; | |
while (attempts < maxAttempts) { | |
textarea = DOMUtils.querySelector(CONSTANTS.SELECTORS.SYSTEM_TEXTAREA); // Re-query inside loop | |
if (textarea && await SystemPromptManager.isTAAccessible(textarea)) { | |
isTextareaCurrentlyAccessible = true; // Mark as accessible now | |
panelOpenedAndTextareaReady = true; | |
console.log("SystemPromptManager: Textarea became accessible after click."); | |
break; | |
} | |
await new Promise(resolve => setTimeout(resolve, 100)); | |
attempts++; | |
} | |
if (!panelOpenedAndTextareaReady) { | |
console.warn("SystemPromptManager: Failed to make textarea accessible after clicking button."); | |
return false; // Failed to open panel or find/access textarea | |
} | |
// After opening, it's possible the prompt is now correct (e.g., if opening triggered a default fill or a race condition resolved) | |
// or the textarea is now ready for update. | |
// We must use the 'textarea' variable that was confirmed inside the loop. | |
if (textarea && textarea.value === prompt) { | |
console.log("SystemPromptManager: Prompt matches after opening panel. No further update needed."); | |
return true; | |
} | |
} | |
// Case 3: Textarea is now accessible (either initially or after a successful click-to-open), | |
// and prompt needs to be set or updated. | |
if (textarea && isTextareaCurrentlyAccessible) { // 'isTextareaCurrentlyAccessible' should be true if we passed Case 2 successfully | |
console.log("SystemPromptManager: Textarea accessible. Setting/updating prompt value."); | |
textarea.focus(); // Ensure focus before changing value | |
textarea.value = prompt; | |
textarea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); | |
textarea.dispatchEvent(new Event('change', { bubbles: true, cancelable: true })); | |
console.log("SystemPromptManager: System prompt updated and events dispatched."); | |
// We will not automatically close the panel here. If the script opened it to set the prompt, | |
// and the prompt is now set, the main goal is achieved. | |
// Repeated calls should be caught by the "prompt already matches" check earlier. | |
return true; | |
} else { | |
// This state implies that even after attempting to open the panel, the textarea isn't usable. | |
console.warn("SystemPromptManager: Textarea not accessible when attempting to set value. This might indicate a persistent UI issue."); | |
return false; | |
} | |
} | |
} | |
//======================================= | |
// 功能类 | |
//======================================= | |
class ShortcutManager { | |
constructor() { | |
this.currentChatIndex = 0; | |
this.bindGlobalShortcuts(); | |
} | |
bindGlobalShortcuts() { | |
window.addEventListener('keydown', (e) => this.handleKeydown(e), { | |
capture: true, | |
passive: false | |
}); | |
} | |
handleKeydown(e) { | |
const isCmdOrCtrl = e.metaKey || e.ctrlKey; | |
if (!isCmdOrCtrl) return; | |
const key = e.key.toLowerCase(); | |
const shortcut = Object.entries(CONSTANTS.SHORTCUTS) | |
.find(([_, value]) => value.key === key && value.requiresCmd); | |
if (!shortcut) return; | |
e.preventDefault(); | |
e.stopPropagation(); | |
switch(shortcut[0]) { | |
case 'TOGGLE_GROUNDING': | |
this.toggleGrounding(); | |
break; | |
case 'NEW_CHAT': | |
this.createNewChat(); | |
break; | |
case 'SWITCH_CHAT': | |
this.switchToNextChat(); | |
break; | |
} | |
} | |
toggleGrounding() { | |
const searchToggle = DOMUtils.querySelector(CONSTANTS.SELECTORS.SEARCH_TOGGLE); | |
searchToggle?.click(); | |
} | |
createNewChat() { | |
const newChatLink = DOMUtils.querySelector(CONSTANTS.SELECTORS.NEW_CHAT_LINK); | |
if (newChatLink) { | |
newChatLink.click(); | |
this.currentChatIndex = 0; | |
} | |
} | |
switchToNextChat() { | |
const chatLinks = DOMUtils.querySelectorAll(CONSTANTS.SELECTORS.CHAT_LINKS); | |
if (chatLinks.length > 0) { | |
this.currentChatIndex = (this.currentChatIndex + 1) % chatLinks.length; | |
chatLinks[this.currentChatIndex].click(); | |
} | |
} | |
} | |
//======================================= | |
// UI相关类 | |
//======================================= | |
class UIComponents { | |
static createSettingLink() { | |
return DOMUtils.createElement('a', | |
{ textContent: '⚙️', className: 'easy-use-settings' }, | |
{ | |
display: 'flex', | |
color: CONSTANTS.THEME_COLORS.LIGHT.ACCENT_PRIMARY, | |
textDecoration: 'none', | |
fontSize: '20px', | |
marginBottom: '20px', | |
cursor: 'pointer', | |
'align-self': 'center' | |
} | |
); | |
} | |
static createShortcutsSection(colors) { | |
const shortcutsSection = DOMUtils.createElement('div', {}, { | |
marginBottom: '24px', | |
padding: '12px', | |
background: colors.BACKGROUND_SECONDARY, | |
borderRadius: '4px' | |
}); | |
const shortcutsTitle = DOMUtils.createElement('div', | |
{ textContent: 'Keyboard Shortcuts' }, | |
{ | |
fontWeight: '500', | |
marginBottom: '8px', | |
color: colors.TEXT_PRIMARY | |
} | |
); | |
const shortcutsList = DOMUtils.createElement('div', {}, { | |
fontSize: '14px', | |
color: colors.TEXT_MUTED | |
}); | |
const shortcuts = [ | |
{ key: 'Ctrl/Cmd + i', description: 'Toggle Grounding' }, | |
{ key: 'Ctrl/Cmd + j', description: 'New Chat' }, | |
{ key: 'Ctrl/Cmd + /', description: 'Switch Recent Chats' } | |
]; | |
shortcuts.forEach(({ key, description }) => { | |
const shortcutItem = DOMUtils.createElement('div', {}, { marginBottom: '4px'}); | |
shortcutItem.textContent = '• '; | |
const kbd = DOMUtils.createElement('kbd', { textContent: key }, { | |
padding: '2px 6px', | |
border: `1px solid ${colors.KBD_BORDER}`, | |
borderRadius: '3px', | |
background: colors.KBD_BG, | |
color: colors.KBD_TEXT, | |
fontFamily: 'monospace', | |
fontSize: '0.9em', | |
margin: '0 2px' | |
}); | |
const text = document.createTextNode(`: ${description}`); | |
shortcutItem.appendChild(kbd); | |
shortcutItem.appendChild(text); | |
shortcutsList.appendChild(shortcutItem); | |
}); | |
shortcutsSection.appendChild(shortcutsTitle); | |
shortcutsSection.appendChild(shortcutsList); | |
return shortcutsSection; | |
} | |
} | |
class DialogManager { | |
constructor(settingsManager) { | |
this.settingsManager = settingsManager; | |
this.dialog = null; | |
this.overlay = null; | |
this.currentColors = CONSTANTS.THEME_COLORS.LIGHT; | |
} | |
createOverlay() { | |
return DOMUtils.createElement('div', {}, { | |
position: 'fixed', | |
top: '0', | |
left: '0', | |
width: '100%', | |
height: '100%', | |
background: 'rgba(0,0,0,0.5)', | |
zIndex: '9999' | |
}); | |
} | |
createDialog() { | |
const settings = this.settingsManager.getSettings(); | |
const colors = this.currentColors; | |
const dialog = DOMUtils.createElement('div', {}, { | |
position: 'fixed', | |
top: '50%', | |
left: '50%', | |
transform: 'translate(-50%, -50%)', | |
background: colors.BACKGROUND_PRIMARY, | |
padding: '30px', | |
borderRadius: '8px', | |
boxShadow: colors.DIALOG_SHADOW, | |
zIndex: '10000', | |
minWidth: '450px', | |
maxWidth: '700px', | |
width: '50vw' | |
}); | |
const title = DOMUtils.createElement('h2', | |
{ textContent: '⚙️ Easy Use Settings' }, | |
{ | |
margin: '0 0 20px 0', | |
fontSize: '18px', | |
color: colors.TEXT_PRIMARY | |
} | |
); | |
dialog.appendChild(title); | |
const promptSection = this.createPromptSection(settings, colors); | |
dialog.appendChild(promptSection); | |
const fontSection = this.createFontSection(settings, colors); | |
dialog.appendChild(fontSection); | |
dialog.appendChild(UIComponents.createShortcutsSection(colors)); | |
const buttonContainer = this.createButtonContainer(colors); | |
dialog.appendChild(buttonContainer); | |
return dialog; | |
} | |
createPromptSection(settings, colors) { | |
const section = DOMUtils.createElement('div', {}, { | |
marginBottom: '24px' | |
}); | |
const label = DOMUtils.createElement('label', | |
{ textContent: 'Global System Prompt' }, | |
{ | |
display: 'block', | |
marginBottom: '8px', | |
fontWeight: '500', | |
color: colors.TEXT_PRIMARY | |
} | |
); | |
const textarea = document.createElement('textarea'); | |
textarea.value = settings.systemPrompt; | |
Object.assign(textarea.style, { | |
width: '100%', | |
minHeight: '100px', | |
marginBottom: '8px', | |
padding: '8px', | |
border: `1px solid ${colors.BORDER_INPUT}`, | |
borderRadius: '4px', | |
fontFamily: 'inherit', | |
resize: 'vertical', | |
background: colors.BACKGROUND_INPUT, | |
color: colors.TEXT_INPUT | |
}); | |
textarea.spellcheck = false; | |
const resetButton = DOMUtils.createElement('button', | |
{ textContent: 'Reset to Default' }, | |
{ | |
padding: '4px 8px', | |
backgroundColor: colors.BUTTON_SECONDARY_BG, | |
color: colors.BUTTON_SECONDARY_TEXT, | |
border: `1px solid ${colors.BUTTON_SECONDARY_BORDER}`, | |
borderRadius: '4px', | |
cursor: 'pointer', | |
fontSize: '12px', | |
marginBottom: '16px' | |
} | |
); | |
resetButton.addEventListener('click', () => { | |
textarea.value = CONSTANTS.DEFAULTS.SYSTEM_PROMPT; | |
}); | |
section.appendChild(label); | |
section.appendChild(textarea); | |
section.appendChild(resetButton); | |
return section; | |
} | |
createFontSection(settings, colors) { | |
const section = DOMUtils.createElement('div', {}, { | |
marginBottom: '24px' | |
}); | |
const label = DOMUtils.createElement('label', | |
{ textContent: 'Font Size' }, | |
{ | |
display: 'block', | |
marginBottom: '8px', | |
fontWeight: '500', | |
color: colors.TEXT_PRIMARY | |
} | |
); | |
const buttonGroup = DOMUtils.createElement('div', | |
{ className: 'font-button-group' }, | |
{ | |
display: 'flex', | |
gap: '8px', | |
width: '100%' | |
} | |
); | |
CONSTANTS.FONT_SIZES.forEach(size => { | |
const button = DOMUtils.createElement('button', | |
{ | |
type: 'button', | |
value: size.value, | |
textContent: size.label, | |
title: `${size.label} (${size.size})` | |
}, | |
{ | |
...this.getFontButtonStyles(size.value === settings.fontSize, colors), | |
fontSize: size.size | |
} | |
); | |
if (size.value === settings.fontSize) { | |
button.setAttribute('data-selected', 'true'); | |
} | |
button.addEventListener('click', () => this.handleFontButtonClick(button, buttonGroup, colors)); | |
buttonGroup.appendChild(button); | |
}); | |
section.appendChild(label); | |
section.appendChild(buttonGroup); | |
return section; | |
} | |
getFontButtonStyles(isSelected, colors) { | |
return { | |
flex: '1', | |
padding: '8px', | |
border: `1px solid ${isSelected ? colors.ACCENT_PRIMARY : colors.BORDER_PRIMARY}`, | |
borderRadius: '4px', | |
background: isSelected ? colors.ACCENT_SECONDARY_BACKGROUND : colors.BACKGROUND_INPUT, | |
color: isSelected ? colors.ACCENT_PRIMARY : colors.TEXT_SECONDARY, | |
cursor: 'pointer', | |
transition: 'all 0.2s', | |
fontFamily: 'inherit' | |
}; | |
} | |
handleFontButtonClick(clickedButton, buttonGroup, colors) { | |
buttonGroup.querySelectorAll('button').forEach(btn => { | |
const isThisButton = btn === clickedButton; | |
Object.assign(btn.style, { | |
...this.getFontButtonStyles(isThisButton, colors), | |
fontSize: CONSTANTS.FONT_SIZES.find(s => s.value === btn.value)?.size | |
}); | |
if (isThisButton) { | |
btn.setAttribute('data-selected', 'true'); | |
} else { | |
btn.removeAttribute('data-selected'); | |
} | |
}); | |
} | |
createButtonContainer(colors) { | |
const container = DOMUtils.createElement('div', { | |
className: 'dialog-buttons' | |
}, { | |
display: 'flex', | |
gap: '10px', | |
justifyContent: 'flex-end' | |
}); | |
const saveButton = DOMUtils.createElement('button', { | |
className: 'save-button', | |
textContent: 'Save' | |
}, { | |
padding: '8px 16px', | |
backgroundColor: colors.ACCENT_PRIMARY, | |
color: colors.ACCENT_PRIMARY_TEXT, | |
border: 'none', | |
borderRadius: '4px', | |
cursor: 'pointer', | |
fontWeight: '500' | |
}); | |
const cancelButton = DOMUtils.createElement('button', { | |
className: 'cancel-button', | |
textContent: 'Cancel' | |
}, { | |
padding: '8px 16px', | |
backgroundColor: colors.BUTTON_SECONDARY_BG, | |
color: colors.BUTTON_SECONDARY_TEXT, | |
border: `1px solid ${colors.BUTTON_SECONDARY_BORDER}`, | |
borderRadius: '4px', | |
cursor: 'pointer', | |
fontWeight: '500' | |
}); | |
container.appendChild(cancelButton); | |
container.appendChild(saveButton); | |
return container; | |
} | |
show() { | |
const isSystemDark = document.body.classList.contains('dark-theme'); | |
this.currentColors = isSystemDark ? CONSTANTS.THEME_COLORS.DARK : CONSTANTS.THEME_COLORS.LIGHT; | |
this.overlay = this.createOverlay(); | |
this.dialog = this.createDialog(); | |
document.body.appendChild(this.overlay); | |
document.body.appendChild(this.dialog); | |
this.bindEvents(); | |
} | |
hide() { | |
if (this.dialog && this.overlay) { | |
this.dialog.ownerDocument.removeEventListener('keydown', this.handleEscKey); | |
document.body.removeChild(this.dialog); | |
document.body.removeChild(this.overlay); | |
this.dialog = null; | |
this.overlay = null; | |
} | |
} | |
bindEvents() { | |
const saveButton = this.dialog.querySelector('.save-button'); | |
const cancelButton = this.dialog.querySelector('.cancel-button'); | |
if (saveButton && cancelButton) { | |
saveButton.addEventListener('click', () => this.handleSave()); | |
cancelButton.addEventListener('click', () => this.hide()); | |
this.dialog.ownerDocument.addEventListener('keydown', this.handleEscKey); | |
} | |
if (this.overlay) { | |
this.overlay.addEventListener('click', () => this.hide()); | |
} | |
} | |
handleEscKey = (event) => { | |
if (event.key === 'Escape') { | |
this.hide(); | |
} | |
} | |
handleSave() { | |
const textarea = this.dialog.querySelector('textarea'); | |
const selectedFontButton = this.dialog.querySelector('button[data-selected="true"]'); | |
if (!textarea || !selectedFontButton) return; | |
const newSettings = { | |
systemPrompt: textarea.value.trim(), | |
fontSize: selectedFontButton.value | |
}; | |
this.settingsManager.saveSettings(newSettings); | |
StyleManager.updateFontSize(newSettings.fontSize); | |
SystemPromptManager.update(newSettings.systemPrompt); // Re-apply/verify system prompt | |
this.hide(); | |
} | |
} | |
//======================================= | |
// 核心管理器类 | |
//======================================= | |
class SettingsManager { | |
constructor() { | |
this.settings = this.loadSettings(); | |
} | |
loadSettings() { | |
return { | |
systemPrompt: localStorage.getItem(CONSTANTS.STORAGE_KEYS.SYSTEM_PROMPT) || CONSTANTS.DEFAULTS.SYSTEM_PROMPT, | |
fontSize: localStorage.getItem(CONSTANTS.STORAGE_KEYS.FONT_SIZE) || CONSTANTS.DEFAULTS.FONT_SIZE | |
}; | |
} | |
saveSettings(settings) { | |
localStorage.setItem(CONSTANTS.STORAGE_KEYS.SYSTEM_PROMPT, settings.systemPrompt); | |
localStorage.setItem(CONSTANTS.STORAGE_KEYS.FONT_SIZE, settings.fontSize); | |
this.settings = settings; | |
} | |
getSettings() { | |
return { ...this.settings }; | |
} | |
} | |
class AppManager { | |
constructor() { | |
this.settingsManager = new SettingsManager(); | |
this.shortcutManager = new ShortcutManager(); | |
this.dialogManager = new DialogManager(this.settingsManager); | |
} | |
init() { | |
this.initSettingsLink(); | |
this.applyInitialSettings(); | |
this.observeRouteChanges(); | |
} | |
initSettingsLink() { | |
const link = UIComponents.createSettingLink(); | |
link.addEventListener('click', (e) => { | |
e.preventDefault(); | |
this.dialogManager.show(); | |
}); | |
this.observeNavigation(link); | |
} | |
observeNavigation(link) { | |
const observer = new MutationObserver((mutationsList, obs) => { | |
const nav = DOMUtils.querySelector(CONSTANTS.SELECTORS.NAVIGATION); | |
if (nav && !nav.querySelector('.easy-use-settings')) { | |
if (!link.parentNode) { | |
link.classList.add('easy-use-settings'); | |
nav.insertBefore(link, nav.firstChild); | |
} | |
} else if (nav && nav.querySelector('.easy-use-settings') && link.parentNode !== nav) { | |
const oldLink = nav.querySelector('.easy-use-settings'); | |
if (oldLink) oldLink.remove(); | |
nav.insertBefore(link, nav.firstChild); | |
} | |
}); | |
observer.observe(document.body, { | |
childList: true, | |
subtree: true | |
}); | |
} | |
applyInitialSettings() { | |
const settings = this.settingsManager.getSettings(); | |
StyleManager.updateFontSize(settings.fontSize); | |
this.initSystemPrompt(settings.systemPrompt); | |
} | |
async initSystemPrompt(prompt, maxRetries = 15, interval = 1000) { | |
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms)); | |
console.log("Initializing system prompt..."); | |
for (let i = 0; i < maxRetries; i++) { | |
if (document.readyState !== 'complete' && document.readyState !== 'interactive') { | |
console.log(`Attempt ${i + 1}: Document not ready. Waiting...`); | |
await wait(interval); | |
continue; | |
} | |
// Check for the button first, as SystemPromptManager.update relies on it. | |
// SystemPromptManager.update has its own waitForElement for the button, | |
// but this initial check ensures we don't even try if the button isn't there yet. | |
const systemInstructionsButton = await DOMUtils.waitForElement(CONSTANTS.SELECTORS.SYSTEM_INSTRUCTIONS_BUTTON, interval / 2); | |
if (!systemInstructionsButton) { | |
console.log(`Attempt ${i + 1}: System instructions button not found or not visible by AppManager. Retrying...`); | |
await wait(interval); | |
continue; | |
} | |
console.log(`Attempt ${i + 1}: System instructions button found by AppManager. Proceeding to SystemPromptManager.update.`); | |
const success = await SystemPromptManager.update(prompt); | |
if (success) { | |
console.log("System prompt initialization successful on attempt " + (i + 1)); | |
return; | |
} | |
console.log(`Attempt ${i + 1} to set system prompt via SystemPromptManager.update failed or indicated no update needed but was part of init. Retrying...`); | |
await wait(interval); | |
} | |
console.error(`Failed to initialize system prompt after ${maxRetries} attempts.`); | |
} | |
observeRouteChanges() { | |
let lastUrl = location.href; | |
const observer = new MutationObserver(() => { | |
const url = location.href; | |
if (url !== lastUrl) { | |
lastUrl = url; | |
console.log('Route changed to:', url); | |
// When route changes, re-apply settings, especially the system prompt. | |
setTimeout(() => { | |
// applyInitialSettings calls initSystemPrompt, which now has robust checks. | |
this.applyInitialSettings(); | |
}, 500); // Delay to allow new page elements to settle. | |
} | |
}); | |
observer.observe(document.body, { | |
childList: true, | |
subtree: true | |
}); | |
} | |
} | |
// Startup check | |
if (document.readyState === 'complete' || document.readyState === 'interactive') { | |
new AppManager().init(); | |
} else { | |
window.addEventListener('DOMContentLoaded', () => { | |
new AppManager().init(); | |
}); | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment