// ==UserScript== // @name ChatGPT 提示词选择器 // @namespace http://tampermonkey.net/ // @version 1.0 // @description 在 ChatGPT 输入框附近添加图像比例、风格和提示词按钮,支持自定义与收藏。 // @author Claude Opus 4.5; Claude Sonnet 4.5; Gemini 3 Pro; // @match https://chatgpt.com/* // @run-at document-start // @grant none // ==/UserScript== (function () { 'use strict'; // ================= Night-Mode 扩展兼容修复 ================= // Night-Mode 扩展会把 #night-mask 插到 下(document.documentElement)。 // ChatGPT 采用 React/SSR 水合时, 结构变化可能触发重建,从而把该节点清掉。 // 这里做两件事: // 1) 发现 #night-mask 时尽量搬到 (降低被水合/重建“顺手清扫”的概率) // 2) 若它被删了,则用最后一次观测到的 style 复活一个同 id 节点(扩展后续仍能 querySelector 更新) (function setupNightMaskGuard() { const ID = 'night-mask'; let lastStyle = null; let started = false; function ensure() { try { const mask = document.getElementById(ID); if (mask) { const st = mask.getAttribute('style'); if (st) lastStyle = st; // 优先把遮罩放到 body 顶层(而不是 直接子节点) if (document.body && mask.parentElement !== document.body) { document.body.appendChild(mask); } return; } // mask 被页面重建清掉了:用最后一次样式复活(不猜配置,不瞎改) if (lastStyle && document.body) { const revived = document.createElement('div'); revived.id = ID; revived.setAttribute('style', lastStyle); document.body.appendChild(revived); } } catch (e) { // 静默:这里不应该影响脚本其它功能 } } function start() { if (started) return; started = true; // 监听整个文档树的变化:SPA 路由、重渲染都可能删节点 const mo = new MutationObserver(() => ensure()); mo.observe(document.documentElement, { childList: true, subtree: true }); // 兜底:某些情况下 mutation 合并/吞掉,周期性检查更稳 setInterval(ensure, 1000); // 立刻跑一次 ensure(); } if (document.readyState === 'loading') { // 尽可能早启动(但等 body 出现后再搬运) const moBody = new MutationObserver(() => { if (document.body) { moBody.disconnect(); start(); } }); moBody.observe(document.documentElement, { childList: true }); } else { start(); } })(); // ================= 基础数据 ================= const ASPECT_RATIOS = [ { ratio: '1:1', label: '1:1 正方形, 头像' }, { ratio: '9:16', label: '9:16 手机壁纸, 人像' }, { ratio: '16:9', label: '16:9 桌面壁纸, 风景' }, { ratio: '3:4', label: '3:4 经典比例, 拍照' }, { ratio: '4:3', label: '4:3 文章配图, 插画' }, { ratio: '3:2', label: '3:2 单反相机, 摄影' }, { ratio: '2:3', label: '2:3 社交媒体, 自拍' }, { ratio: '5:4', label: '5:4 艺术画作, 打印' }, { ratio: '4:5', label: '4:5 肖像模式' }, { ratio: '21:9', label: '21:9 电影宽屏' } ]; const DEFAULT_STYLES = [ { name: '写实照片', prompt: '高清写实照片风格,注重细节和真实感' }, { name: '油画', prompt: '古典油画风格,色彩浓郁,笔触可见' }, { name: '水彩', prompt: '清新水彩画风格,色彩柔和透明' }, { name: '赛博朋克', prompt: '赛博朋克风格,霓虹灯光,未来主义' }, { name: '极简主义', prompt: '极简主义设计,简洁线条,留白艺术' }, { name: '动漫', prompt: '日式动漫风格,明亮色彩,精致线条' }, { name: '复古', prompt: '复古怀旧风格,暖色调,胶片质感' }, { name: '科幻', prompt: '科幻未来风格,科技感,宇宙元素' } ]; const DEFAULT_PROMPTS = []; const ratioPattern = /,\s*(\d+:\d+)比例$/; // LocalStorage keys const STORAGE_KEY = 'chatgpt-custom-styles'; const STORAGE_KEY_FAV = 'chatgpt-favorite-styles'; const STORAGE_KEY_PROMPT = 'chatgpt-custom-prompts'; const STORAGE_KEY_FAV_PROMPT = 'chatgpt-favorite-prompts'; let currentRatio = null; let currentStyle = null; let currentPrompt = null; // ================= 样式注入 ================= function injectStyles() { const style = document.createElement('style'); style.textContent = ` /* 基础变量定义 - 默认深色模式 */ .ratio-selector-container, .style-selector-container, .prompt-selector-container, .ratio-dropdown, .style-dropdown, .prompt-dropdown, .custom-style-modal, .custom-prompt-modal { --g-selector-bg: rgba(255, 255, 255, 0.08); --g-selector-border: rgba(255, 255, 255, 0.18); --g-selector-text: #e3e3e3; --g-selector-hover-bg: rgba(255, 255, 255, 0.15); --g-selector-hover-border: rgba(255, 255, 255, 0.25); --g-selector-active-bg: rgba(138, 180, 248, 0.18); --g-selector-active-border: #8ab4f8; --g-dropdown-bg: #2d2d2d; --g-dropdown-border: rgba(255, 255, 255, 0.18); --g-dropdown-shadow: rgba(0, 0, 0, 0.35); --g-option-hover: rgba(255, 255, 255, 0.08); --g-option-selected-bg: rgba(138, 180, 248, 0.18); --g-option-selected-text: #8ab4f8; --g-option-text: #e3e3e3; --g-scrollbar-thumb: rgba(255, 255, 255, 0.25); --g-scrollbar-thumb-hover: rgba(255, 255, 255, 0.4); --g-btn-add-text: #8ab4f8; --g-btn-add-hover: rgba(138, 180, 248, 0.1); --g-btn-add-border: rgba(255, 255, 255, 0.1); --g-fav-star-inactive: #888888; --g-fav-star-active: #fbbc04; --g-fav-star-hover: #aaaaaa; --g-del-btn-inactive: #888888; --g-del-btn-hover: #ff6b6b; --g-modal-overlay: rgba(0, 0, 0, 0.65); --g-modal-bg: #2d2d2d; --g-modal-text: #e3e3e3; --g-modal-label: #b8b8b8; --g-modal-input-bg: #1e1e1e; --g-modal-input-border: rgba(255, 255, 255, 0.2); --g-modal-input-text: #e3e3e3; --g-modal-input-focus: #8ab4f8; --g-modal-btn-cancel-bg: rgba(255, 255, 255, 0.1); --g-modal-btn-cancel-text: #e3e3e3; --g-modal-btn-cancel-hover: rgba(255, 255, 255, 0.15); } .ratio-selector-container.light-theme, .style-selector-container.light-theme, .prompt-selector-container.light-theme, .ratio-dropdown.light-theme, .style-dropdown.light-theme, .prompt-dropdown.light-theme, .custom-style-modal.light-theme, .custom-prompt-modal.light-theme { --g-selector-bg: rgba(0, 0, 0, 0.05); --g-selector-border: rgba(0, 0, 0, 0.12); --g-selector-text: currentColor; --g-selector-hover-bg: rgba(0, 0, 0, 0.08); --g-selector-hover-border: rgba(0, 0, 0, 0.25); --g-selector-active-bg: rgba(11, 87, 208, 0.1); --g-selector-active-border: #0b57d0; --g-dropdown-bg: var(--token-main-surface-primary, #ffffff); --g-dropdown-border: rgba(0, 0, 0, 0.12); --g-dropdown-shadow: rgba(0, 0, 0, 0.14); --g-option-hover: rgba(0, 0, 0, 0.06); --g-option-selected-bg: rgba(11, 87, 208, 0.1); --g-option-selected-text: #0b57d0; --g-option-text: currentColor; --g-scrollbar-thumb: rgba(0, 0, 0, 0.22); --g-scrollbar-thumb-hover: rgba(0, 0, 0, 0.32); --g-btn-add-text: #0b57d0; --g-btn-add-hover: rgba(11, 87, 208, 0.08); --g-btn-add-border: rgba(0, 0, 0, 0.1); --g-fav-star-inactive: #9aa0a6; --g-fav-star-active: #fbbc04; --g-fav-star-hover: #5f6368; --g-del-btn-inactive: #9aa0a6; --g-del-btn-hover: #d93025; --g-modal-overlay: rgba(255, 255, 255, 0.6); --g-modal-bg: var(--token-main-surface-primary, #ffffff); --g-modal-text: currentColor; --g-modal-label: inherit; --g-modal-input-bg: rgba(0, 0, 0, 0.05); --g-modal-input-border: rgba(0, 0, 0, 0.12); --g-modal-input-text: currentColor; --g-modal-input-focus: #0b57d0; --g-modal-btn-cancel-bg: rgba(0, 0, 0, 0.05); --g-modal-btn-cancel-text: currentColor; --g-modal-btn-cancel-hover: rgba(0, 0, 0, 0.1); } .ratio-selector-container, .style-selector-container, .prompt-selector-container { display: inline-flex !important; align-items: center !important; position: relative !important; margin-left: 8px !important; vertical-align: middle !important; } .ratio-selector-btn, .style-selector-btn { display: flex; align-items: center; justify-content: center; padding: 6px 12px; border: 1px solid var(--g-selector-border); border-radius: 18px; background: var(--g-selector-bg); color: var(--g-selector-text); font-size: 13px; cursor: pointer; transition: all 0.18s ease; font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif; white-space: nowrap; } .ratio-selector-btn:hover, .style-selector-btn:hover { background: var(--g-selector-hover-bg); border-color: var(--g-selector-hover-border); } .ratio-selector-btn.active, .style-selector-btn.active { background: var(--g-selector-active-bg); border-color: var(--g-selector-active-border); color: var(--g-option-selected-text); } .ratio-selector-btn::after, .style-selector-btn::after, .prompt-selector-btn::after { content: '▼'; font-size: 10px; margin-left: 6px; } .ratio-selector-btn.expanded::after, .style-selector-btn.expanded::after, .prompt-selector-btn.expanded::after { content: '▲'; } .ratio-dropdown, .style-dropdown, .prompt-dropdown { position: fixed; left: 0; top: 0; margin-bottom: 0; background: var(--g-dropdown-bg); border: 1px solid var(--g-dropdown-border); border-radius: 12px; box-shadow: 0 4px 16px var(--g-dropdown-shadow); overflow: hidden; opacity: 0; visibility: hidden; transition: opacity 0.12s ease; z-index: 1000; min-width: 200px; } .ratio-dropdown.show, .style-dropdown.show, .prompt-dropdown.show { opacity: 1; visibility: visible; } .ratio-dropdown-scroll, .style-dropdown-scroll { max-height: 240px; overflow-y: auto; padding: 4px 0; } .ratio-dropdown-scroll::-webkit-scrollbar, .style-dropdown-scroll::-webkit-scrollbar { width: 6px; } .ratio-dropdown-scroll::-webkit-scrollbar-thumb, .style-dropdown-scroll::-webkit-scrollbar-thumb { background: var(--g-scrollbar-thumb); border-radius: 3px; } .ratio-option, .style-option { padding: 9px 12px; color: var(--g-option-text); font-size: 13px; cursor: pointer; transition: background 0.15s ease; font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif; display: flex; align-items: center; } .ratio-option:hover, .style-option:hover { background: var(--g-option-hover); } .ratio-option.selected, .style-option.selected { background: var(--g-option-selected-bg); color: var(--g-option-selected-text); } .ratio-check { width: 16px; margin-right: 6px; opacity: 0; font-weight: bold; flex-shrink: 0; } .ratio-option.selected .ratio-check { opacity: 1; } .ratio-value { width: 40px; text-align: left; margin-right: 10px; font-feature-settings: 'tnum'; font-weight: 500; flex-shrink: 0; } .ratio-desc { flex: 1; text-align: left; opacity: 0.92; overflow: hidden; text-overflow: ellipsis; } .style-option-content { display: flex; align-items: center; flex: 1; } .style-option.selected .style-option-content::before { content: '✓ '; margin-right: 4px; } .style-option { justify-content: space-between; gap: 6px; padding-left: 8px; } .style-fav-btn { width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; color: var(--g-fav-star-inactive); font-size: 16px; margin-right: 6px; cursor: pointer; transition: color 0.18s ease; user-select: none; } .style-fav-btn:hover { color: var(--g-fav-star-hover); } .style-fav-btn.active { color: var(--g-fav-star-active); } .style-fav-btn::before { content: '★'; } .style-del-btn { width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; color: var(--g-del-btn-inactive); font-size: 14px; margin-left: 4px; cursor: pointer; transition: all 0.18s ease; border-radius: 4px; flex-shrink: 0; } .style-del-btn:hover { color: #ff4444; background: rgba(255, 0, 0, 0.1); } .add-style-btn { padding: 12px 14px; color: var(--g-btn-add-text); font-size: 14px; font-weight: 500; cursor: pointer; transition: background 0.15s ease; border-bottom: 1px solid var(--g-btn-add-border); text-align: center; font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif; } .add-style-btn:hover { background: var(--g-btn-add-hover); } .add-style-btn::before { content: '+ '; font-size: 16px; } .custom-style-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: var(--g-modal-overlay); display: none; align-items: center; justify-content: center; z-index: 1001; } .custom-style-modal.show { display: flex; } .custom-style-modal .modal-content, .custom-prompt-modal .modal-content { background: var(--g-modal-bg); border-radius: 16px; padding: 22px; width: 90%; max-width: 440px; box-shadow: 0 8px 32px var(--g-dropdown-shadow); } .custom-style-modal .modal-header, .custom-prompt-modal .modal-header { font-size: 18px; font-weight: 600; color: var(--g-modal-text); margin-bottom: 18px; font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif; } .custom-style-modal .modal-form-group, .custom-prompt-modal .modal-form-group { margin-bottom: 14px; } .custom-style-modal .modal-label, .custom-prompt-modal .modal-label { display: block; font-size: 13px; color: var(--g-modal-label); margin-bottom: 6px; font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif; } .custom-style-modal .modal-input, .custom-prompt-modal .modal-input { width: 100%; padding: 10px 12px; font-size: 14px; background: var(--g-modal-input-bg); border: 1px solid var(--g-modal-input-border); border-radius: 8px; color: var(--g-modal-input-text); outline: none; transition: border-color 0.18s ease; font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif; box-sizing: border-box; } .modal-input:focus { border-color: var(--g-modal-input-focus); } .custom-style-modal .modal-textarea, .custom-prompt-modal .modal-textarea { width: 100%; min-height: 86px; padding: 10px 12px; font-size: 14px; background: var(--g-modal-input-bg); border: 1px solid var(--g-modal-input-border); border-radius: 8px; color: var(--g-modal-input-text); outline: none; transition: border-color 0.18s ease; resize: vertical; font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif; box-sizing: border-box; } .modal-textarea:focus { border-color: var(--g-modal-input-focus); } .custom-style-modal .modal-actions, .custom-prompt-modal .modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px; } .custom-style-modal .modal-btn, .custom-prompt-modal .modal-btn { padding: 8px 18px; font-size: 14px; border: none; border-radius: 18px; cursor: pointer; transition: all 0.18s ease; font-family: 'Inter', 'Helvetica Neue', Arial, sans-serif; font-weight: 600; } .custom-style-modal .modal-btn-cancel, .custom-prompt-modal .modal-btn-cancel { background: var(--g-modal-btn-cancel-bg); color: var(--g-modal-btn-cancel-text); } .modal-btn-cancel:hover { background: var(--g-modal-btn-cancel-hover); } .custom-style-modal .modal-btn-confirm, .custom-prompt-modal .modal-btn-confirm { background: #8ab4f8; color: #1e1e1e; } .modal-btn-confirm:hover { background: #a8c7fa; } `; document.head.appendChild(style); } // ================= LocalStorage 工具 ================= function loadCustomStyles() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); } catch (e) { console.error('加载自定义风格失败', e); return []; } } function saveCustomStyles(styles) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(styles)); } catch (e) { console.error('保存自定义风格失败', e); } } function getAllStyles() { return [...DEFAULT_STYLES, ...loadCustomStyles()]; } function addCustomStyle(name, prompt) { const list = loadCustomStyles(); list.push({ name, prompt }); saveCustomStyles(list); } function deleteCustomStyle(name) { const list = loadCustomStyles().filter(s => s.name !== name); saveCustomStyles(list); saveFavorites(loadFavorites().filter(f => f !== name)); } function loadFavorites() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY_FAV) || '[]'); } catch { return []; } } function saveFavorites(fav) { try { localStorage.setItem(STORAGE_KEY_FAV, JSON.stringify(fav)); } catch (e) { console.error('保存收藏失败', e); } } function toggleFavorite(styleName) { let fav = loadFavorites(); fav = fav.includes(styleName) ? fav.filter(n => n !== styleName) : [...fav, styleName]; saveFavorites(fav); return fav; } function loadCustomPrompts() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY_PROMPT) || '[]'); } catch { return []; } } function saveCustomPrompts(list) { try { localStorage.setItem(STORAGE_KEY_PROMPT, JSON.stringify(list)); } catch (e) { console.error('保存自定义提示词失败', e); } } function getAllPrompts() { return [...DEFAULT_PROMPTS, ...loadCustomPrompts()]; } function addCustomPrompt(name, prompt) { const list = loadCustomPrompts(); list.push({ name, prompt }); saveCustomPrompts(list); } function deleteCustomPrompt(name) { const list = loadCustomPrompts().filter(s => s.name !== name); saveCustomPrompts(list); saveFavoritePrompts(loadFavoritePrompts().filter(f => f !== name)); } function loadFavoritePrompts() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY_FAV_PROMPT) || '[]'); } catch { return []; } } function saveFavoritePrompts(list) { try { localStorage.setItem(STORAGE_KEY_FAV_PROMPT, JSON.stringify(list)); } catch (e) { console.error('保存收藏提示词失败', e); } } function toggleFavoritePrompt(name) { let fav = loadFavoritePrompts(); fav = fav.includes(name) ? fav.filter(n => n !== name) : [...fav, name]; saveFavoritePrompts(fav); return fav; } // ================= 输入框操作 ================= function getInputElement() { return document.querySelector('#prompt-textarea') || document.querySelector('div.ProseMirror') || document.querySelector('textarea[name="prompt-textarea"]'); } function getInputText() { const editor = getInputElement(); if (!editor) return ''; return editor.innerText.trim(); } function setInputText(text) { const editor = getInputElement(); if (!editor) return; while (editor.firstChild) editor.removeChild(editor.firstChild); const p = document.createElement('p'); p.textContent = text; editor.appendChild(p); const ev = new Event('input', { bubbles: true, cancelable: true }); editor.dispatchEvent(ev); const range = document.createRange(); const sel = window.getSelection(); range.selectNodeContents(editor); range.collapse(false); sel.removeAllRanges(); sel.addRange(range); } function updateRatioInInput(newRatio) { let text = getInputText(); text = ratioPattern.test(text) ? text.replace(ratioPattern, `, ${newRatio}比例`) : (text ? `${text}, ${newRatio}比例` : `${newRatio}比例`); setInputText(text); currentRatio = newRatio; } function updateStyleInInput(styleName, stylePrompt) { let text = getInputText(); text = text ? `${text}, ${stylePrompt}` : stylePrompt; setInputText(text); currentStyle = styleName; } function updatePromptInInput(promptName, promptContent) { let text = getInputText(); text = text ? `${text} ${promptContent}` : promptContent; setInputText(text); currentPrompt = promptName; } // ================= 组件构建 ================= function createRatioSelector() { const container = document.createElement('div'); container.className = 'ratio-selector-container'; const btn = document.createElement('button'); btn.className = 'ratio-selector-btn'; btn.type = 'button'; btn.textContent = '比例'; const dropdown = document.createElement('div'); dropdown.className = 'ratio-dropdown'; const scroll = document.createElement('div'); scroll.className = 'ratio-dropdown-scroll'; ASPECT_RATIOS.forEach(item => { const option = document.createElement('div'); option.className = 'ratio-option'; option.title = item.label; const check = document.createElement('span'); check.className = 'ratio-check'; check.textContent = '✓'; const value = document.createElement('span'); value.className = 'ratio-value'; value.textContent = item.ratio; const desc = document.createElement('span'); desc.className = 'ratio-desc'; desc.textContent = item.label.substring(item.ratio.length).trim(); option.append(check, value, desc); option.addEventListener('click', (e) => { e.stopPropagation(); scroll.querySelectorAll('.ratio-option').forEach(o => o.classList.remove('selected')); option.classList.add('selected'); updateRatioInInput(item.ratio); dropdown.classList.remove('show'); btn.classList.remove('expanded'); }); scroll.appendChild(option); }); dropdown.appendChild(scroll); // 将 dropdown 添加到 body 以避免被父元素 overflow 裁剪 document.body.appendChild(dropdown); btn.addEventListener('click', (e) => { e.stopPropagation(); const isExpanded = dropdown.classList.contains('show'); closeAllDropdowns(); if (!isExpanded) { positionDropdown(btn, dropdown); btn.classList.add('expanded'); } }); container.append(btn); return container; } function createCustomStyleModal(onStyleAdded) { const modal = document.createElement('div'); modal.className = 'custom-style-modal'; const content = document.createElement('div'); content.className = 'modal-content'; const header = document.createElement('div'); header.className = 'modal-header'; header.textContent = '添加自定义风格'; const group1 = document.createElement('div'); group1.className = 'modal-form-group'; const label1 = document.createElement('label'); label1.className = 'modal-label'; label1.textContent = '风格名称'; const nameInput = document.createElement('input'); nameInput.type = 'text'; nameInput.className = 'modal-input'; nameInput.placeholder = '例如:蒸汽波'; group1.append(label1, nameInput); const group2 = document.createElement('div'); group2.className = 'modal-form-group'; const label2 = document.createElement('label'); label2.className = 'modal-label'; label2.textContent = '风格提示词'; const promptInput = document.createElement('textarea'); promptInput.className = 'modal-textarea'; promptInput.placeholder = '例如:蒸汽波美学,粉紫色调,复古电脑图形,80年代风格'; group2.append(label2, promptInput); const actions = document.createElement('div'); actions.className = 'modal-actions'; const cancel = document.createElement('button'); cancel.className = 'modal-btn modal-btn-cancel'; cancel.textContent = '取消'; const confirm = document.createElement('button'); confirm.className = 'modal-btn modal-btn-confirm'; confirm.textContent = '确认'; actions.append(cancel, confirm); content.append(header, group1, group2, actions); modal.appendChild(content); const close = () => { modal.classList.remove('show'); nameInput.value = ''; promptInput.value = ''; }; modal.addEventListener('click', (e) => { if (e.target === modal) close(); }); cancel.addEventListener('click', close); confirm.addEventListener('click', () => { const name = nameInput.value.trim(); const prompt = promptInput.value.trim(); if (!name || !prompt) { alert('请填写完整的风格名称和提示词'); return; } addCustomStyle(name, prompt); onStyleAdded && onStyleAdded(); close(); }); const handleEnter = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); confirm.click(); } }; nameInput.addEventListener('keydown', handleEnter); promptInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && e.ctrlKey) confirm.click(); }); return modal; } function createStyleSelector() { const container = document.createElement('div'); container.className = 'style-selector-container'; const btn = document.createElement('button'); btn.className = 'style-selector-btn'; btn.type = 'button'; btn.textContent = '风格'; const dropdown = document.createElement('div'); dropdown.className = 'style-dropdown'; const scroll = document.createElement('div'); scroll.className = 'style-dropdown-scroll'; const modal = createCustomStyleModal(() => renderOptions()); document.body.appendChild(modal); const renderOptions = () => { while (scroll.firstChild) scroll.removeChild(scroll.firstChild); const favorites = loadFavorites(); const addBtn = document.createElement('div'); addBtn.className = 'add-style-btn'; addBtn.textContent = '添加自定义风格'; addBtn.addEventListener('click', (e) => { e.stopPropagation(); modal.classList.add('show'); dropdown.classList.remove('show'); btn.classList.remove('expanded'); }); scroll.appendChild(addBtn); const allStyles = getAllStyles().sort((a, b) => { const aFav = favorites.includes(a.name); const bFav = favorites.includes(b.name); if (aFav && !bFav) return -1; if (!aFav && bFav) return 1; return 0; }); allStyles.forEach(style => { const isFav = favorites.includes(style.name); const isCustom = !DEFAULT_STYLES.some(ds => ds.name === style.name); const option = document.createElement('div'); option.className = 'style-option'; const favBtn = document.createElement('div'); favBtn.className = `style-fav-btn ${isFav ? 'active' : ''}`; favBtn.title = isFav ? '取消置顶' : '收藏并置顶'; favBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleFavorite(style.name); renderOptions(); }); const content = document.createElement('div'); content.className = 'style-option-content'; content.textContent = style.name; option.append(favBtn, content); if (isCustom) { const delBtn = document.createElement('div'); delBtn.className = 'style-del-btn'; delBtn.title = '删除此自定义风格'; delBtn.textContent = '🗑'; delBtn.addEventListener('click', (e) => { e.stopPropagation(); if (confirm(`确定要删除自定义风格 "${style.name}" 吗?`)) { deleteCustomStyle(style.name); renderOptions(); } }); option.appendChild(delBtn); } option.addEventListener('click', (e) => { e.stopPropagation(); scroll.querySelectorAll('.style-option').forEach(o => o.classList.remove('selected')); option.classList.add('selected'); updateStyleInInput(style.name, style.prompt); dropdown.classList.remove('show'); btn.classList.remove('expanded'); }); scroll.appendChild(option); }); }; renderOptions(); dropdown.appendChild(scroll); // 将 dropdown 添加到 body 以避免被父元素 overflow 裁剪 document.body.appendChild(dropdown); btn.addEventListener('click', (e) => { e.stopPropagation(); const isExpanded = dropdown.classList.contains('show'); closeAllDropdowns(); if (!isExpanded) { positionDropdown(btn, dropdown); btn.classList.add('expanded'); } }); container.append(btn); return container; } function createCustomPromptModal(onPromptAdded) { const modal = document.createElement('div'); modal.className = 'custom-prompt-modal custom-style-modal'; const content = document.createElement('div'); content.className = 'modal-content'; const header = document.createElement('div'); header.className = 'modal-header'; header.textContent = '添加自定义提示词'; const group1 = document.createElement('div'); group1.className = 'modal-form-group'; const label1 = document.createElement('label'); label1.className = 'modal-label'; label1.textContent = '提示词名称'; const nameInput = document.createElement('input'); nameInput.type = 'text'; nameInput.className = 'modal-input'; nameInput.placeholder = '例如:润色'; group1.append(label1, nameInput); const group2 = document.createElement('div'); group2.className = 'modal-form-group'; const label2 = document.createElement('label'); label2.className = 'modal-label'; label2.textContent = '提示词内容'; const promptInput = document.createElement('textarea'); promptInput.className = 'modal-textarea'; promptInput.placeholder = '例如:请对上述文本进行润色,使其更加流畅。'; group2.append(label2, promptInput); const actions = document.createElement('div'); actions.className = 'modal-actions'; const cancel = document.createElement('button'); cancel.className = 'modal-btn modal-btn-cancel'; cancel.textContent = '取消'; const confirm = document.createElement('button'); confirm.className = 'modal-btn modal-btn-confirm'; confirm.textContent = '确认'; actions.append(cancel, confirm); content.append(header, group1, group2, actions); modal.appendChild(content); const close = () => { modal.classList.remove('show'); nameInput.value = ''; promptInput.value = ''; }; modal.addEventListener('click', (e) => { if (e.target === modal) close(); }); cancel.addEventListener('click', close); confirm.addEventListener('click', () => { const name = nameInput.value.trim(); const prompt = promptInput.value.trim(); if (!name || !prompt) { alert('请填写完整的提示词名称和内容'); return; } addCustomPrompt(name, prompt); onPromptAdded && onPromptAdded(); close(); }); const handleEnter = (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); confirm.click(); } }; nameInput.addEventListener('keydown', handleEnter); promptInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && e.ctrlKey) confirm.click(); }); return modal; } function createPromptSelector() { const container = document.createElement('div'); container.className = 'prompt-selector-container'; const btn = document.createElement('button'); btn.className = 'prompt-selector-btn style-selector-btn'; btn.type = 'button'; btn.textContent = '提示词'; const dropdown = document.createElement('div'); dropdown.className = 'prompt-dropdown style-dropdown'; const scroll = document.createElement('div'); scroll.className = 'style-dropdown-scroll'; const modal = createCustomPromptModal(() => renderOptions()); document.body.appendChild(modal); const renderOptions = () => { while (scroll.firstChild) scroll.removeChild(scroll.firstChild); const favorites = loadFavoritePrompts(); const addBtn = document.createElement('div'); addBtn.className = 'add-style-btn'; addBtn.textContent = '添加自定义提示词'; addBtn.addEventListener('click', (e) => { e.stopPropagation(); modal.classList.add('show'); dropdown.classList.remove('show'); btn.classList.remove('expanded'); }); scroll.appendChild(addBtn); const allPrompts = getAllPrompts().sort((a, b) => { const aFav = favorites.includes(a.name); const bFav = favorites.includes(b.name); if (aFav && !bFav) return -1; if (!aFav && bFav) return 1; return 0; }); allPrompts.forEach(p => { const isFav = favorites.includes(p.name); const isCustom = !DEFAULT_PROMPTS.some(dp => dp.name === p.name); const option = document.createElement('div'); option.className = 'style-option'; const favBtn = document.createElement('div'); favBtn.className = `style-fav-btn ${isFav ? 'active' : ''}`; favBtn.title = isFav ? '取消置顶' : '收藏并置顶'; favBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleFavoritePrompt(p.name); renderOptions(); }); const content = document.createElement('div'); content.className = 'style-option-content'; content.textContent = p.name; option.append(favBtn, content); if (isCustom) { const delBtn = document.createElement('div'); delBtn.className = 'style-del-btn'; delBtn.title = '删除此自定义提示词'; delBtn.textContent = '🗑'; delBtn.addEventListener('click', (e) => { e.stopPropagation(); if (confirm(`确定要删除自定义提示词 "${p.name}" 吗?`)) { deleteCustomPrompt(p.name); renderOptions(); } }); option.appendChild(delBtn); } option.addEventListener('click', (e) => { e.stopPropagation(); scroll.querySelectorAll('.style-option').forEach(o => o.classList.remove('selected')); option.classList.add('selected'); updatePromptInInput(p.name, p.prompt); dropdown.classList.remove('show'); btn.classList.remove('expanded'); }); scroll.appendChild(option); }); }; renderOptions(); dropdown.appendChild(scroll); // 将 dropdown 添加到 body 以避免被父元素 overflow 裁剪 document.body.appendChild(dropdown); btn.addEventListener('click', (e) => { e.stopPropagation(); const isExpanded = dropdown.classList.contains('show'); closeAllDropdowns(); if (!isExpanded) { positionDropdown(btn, dropdown); btn.classList.add('expanded'); } }); container.append(btn); return container; } // ================= 主题与通用 ================= let lastThemeIsLight = null; function updateTheme() { const bodyColor = window.getComputedStyle(document.body).backgroundColor; const rgb = bodyColor.match(/\d+/g); if (!rgb) return; const brightness = (parseInt(rgb[0]) * 299 + parseInt(rgb[1]) * 587 + parseInt(rgb[2]) * 114) / 1000; const isLight = brightness > 128; if (lastThemeIsLight === isLight) return; lastThemeIsLight = isLight; document.querySelectorAll('.ratio-selector-container, .style-selector-container, .prompt-selector-container, .custom-style-modal, .custom-prompt-modal, .ratio-dropdown, .style-dropdown, .prompt-dropdown').forEach(el => { if (isLight) el.classList.add('light-theme'); else el.classList.remove('light-theme'); }); } function debounce(fn, wait) { let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), wait); }; } function positionDropdown(btn, dropdown) { if (!btn || !dropdown) return; // 先显示以获取尺寸 dropdown.style.maxWidth = ''; dropdown.style.minWidth = '200px'; dropdown.classList.add('show'); void dropdown.offsetHeight; // 使用 fixed 定位,计算按钮位置 const rect = btn.getBoundingClientRect(); const dropdownWidth = dropdown.offsetWidth; const dropdownHeight = dropdown.offsetHeight; const viewportPadding = 8; // 默认显示在按钮上方 let left = rect.left; let top = rect.top - dropdownHeight - 6; // 如果上方空间不足,显示在下方 if (top < viewportPadding) { top = rect.bottom + 6; } // 确保不超出右边界 if (left + dropdownWidth > window.innerWidth - viewportPadding) { left = window.innerWidth - dropdownWidth - viewportPadding; } // 确保不超出左边界 if (left < viewportPadding) { left = viewportPadding; } dropdown.style.left = `${left}px`; dropdown.style.top = `${top}px`; dropdown.__anchorBtn = btn; } function repositionOpenDropdowns() { document.querySelectorAll('.ratio-dropdown.show, .style-dropdown.show, .prompt-dropdown.show').forEach(d => { if (d.__anchorBtn) positionDropdown(d.__anchorBtn, d); }); } function closeAllDropdowns() { document.querySelectorAll('.ratio-dropdown.show, .style-dropdown.show, .prompt-dropdown.show').forEach(d => { d.classList.remove('show'); }); document.querySelectorAll('.ratio-selector-btn.expanded, .style-selector-btn.expanded, .prompt-selector-btn.expanded').forEach(b => b.classList.remove('expanded')); } let globalClickHandlerRegistered = false; function setupGlobalClickHandler() { if (globalClickHandlerRegistered) return; globalClickHandlerRegistered = true; document.addEventListener('click', () => closeAllDropdowns()); } // ================= 插入逻辑 ================= function findInsertionTargets() { const footer = Array.from(document.querySelectorAll('[data-testid="composer-footer-actions"], .\\[grid-area\\:footer\\]')); const trailing = Array.from(document.querySelectorAll('.\\[grid-area\\:trailing\\]')); // 优先使用 trailing(右边按钮区域) return { targets: trailing.length ? trailing : footer, footer, trailing }; } function insertSelector() { const { targets, footer, trailing } = findInsertionTargets(); if (!targets.length) return false; let changed = false; targets.forEach(wrapper => { const hasRatio = wrapper.querySelector('.ratio-selector-container'); const hasStyle = wrapper.querySelector('.style-selector-container'); const hasPrompt = wrapper.querySelector('.prompt-selector-container'); const firstChild = wrapper.firstChild; if (!hasRatio) { wrapper.insertBefore(createRatioSelector(), firstChild); changed = true; } const afterRatio = wrapper.querySelector('.ratio-selector-container')?.nextSibling || wrapper.firstChild; if (!hasStyle) { wrapper.insertBefore(createStyleSelector(), afterRatio); changed = true; } const afterStyle = wrapper.querySelector('.style-selector-container')?.nextSibling || wrapper.firstChild; if (!hasPrompt) { wrapper.insertBefore(createPromptSelector(), afterStyle); changed = true; } }); // 如果 trailing 已出现,清理 footer 区域里的按钮,避免重复显示在左下角 if (trailing.length && footer.length) { footer.forEach(w => { w.querySelectorAll('.ratio-selector-container, .style-selector-container, .prompt-selector-container').forEach(el => el.remove()); }); } if (changed) { lastThemeIsLight = null; updateTheme(); } return changed; } // ================= 初始化 ================= const debouncedInsert = debounce(insertSelector, 100); const debouncedTheme = debounce(updateTheme, 50); const debouncedReposition = debounce(repositionOpenDropdowns, 80); function init() { injectStyles(); setupGlobalClickHandler(); insertSelector(); updateTheme(); const observer = new MutationObserver(() => { debouncedInsert(); debouncedTheme(); }); observer.observe(document.body, { childList: true, subtree: true }); const themeObserver = new MutationObserver(() => debouncedTheme()); themeObserver.observe(document.body, { attributes: true, attributeFilter: ['class', 'style', 'data-theme'] }); window.addEventListener('resize', debouncedReposition); window.addEventListener('scroll', debouncedReposition, true); setInterval(() => { debouncedInsert(); debouncedTheme(); debouncedReposition(); }, 1500); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();