ChatGPT 超級提示詞便簽 (最終修正版)

修正:將「點擊貼上」改為「雙擊貼上」,避免編輯時焦點跳走。同時優化效能與程式碼結構。

// ==UserScript==
// @name         ChatGPT 超級提示詞便簽 (最終修正版)
// @namespace    http://tampermonkey.net/
// @version      3.5.2
// @description  修正:將「點擊貼上」改為「雙擊貼上」,避免編輯時焦點跳走。同時優化效能與程式碼結構。
// @author       AI Assistant (優化 by Gemini)
// @match        https://chatgpt.com/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    GM_addStyle(`
        :root {
            --note-bg: #fdfdfd;
            --note-border-color: #dcdcdc;
            --note-shadow-color: rgba(0,0,0,0.12);
            --note-text-color: #333;
            --header-bg: #f1f1f1;
            --tab-bg: #e9e9e9;
            --tab-hover-bg: #d8d8d8;
            --tab-active-bg: var(--note-bg);
            --control-icon-color: #888;
            --primary-action-color: #007bff;
            --minimized-bg: #5a6268;
        }
        .prompt-note-container {
            position: fixed; background-color: var(--note-bg); border: 1px solid var(--note-border-color);
            border-radius: 10px; box-shadow: 0 5px 15px var(--note-shadow-color); z-index: 9900;
            display: flex; flex-direction: column; font-family: sans-serif;
            min-width: 250px; min-height: 150px; color: var(--note-text-color);
            transition: all 0.2s ease-in-out, width 0.3s ease, height 0.3s ease, border-radius 0.3s ease;
        }
        .prompt-note-header {
            padding: 3px 12px 0 12px; background-color: var(--header-bg); cursor: move;
            border-bottom: 1px solid var(--note-border-color); border-radius: 10px 10px 0 0;
            display: flex; justify-content: space-between; align-items: flex-end;
        }
        .prompt-note-tabs-container { display: flex; flex-grow: 1; overflow-x: auto; -webkit-overflow-scrolling: touch; }
        .prompt-note-tab {
            padding: 8px 12px; cursor: pointer; border: 1px solid transparent;
            border-bottom: none; border-radius: 6px 6px 0 0; background-color: var(--tab-bg);
            margin-right: 4px; font-size: 13px; white-space: nowrap; max-width: 120px;
            overflow: hidden; text-overflow: ellipsis; transition: background-color 0.2s;
        }
        .prompt-note-tab:hover { background-color: var(--tab-hover-bg); }
        .prompt-note-tab.active { background-color: var(--tab-active-bg); border-color: var(--note-border-color); }
        .prompt-note-tab input { border: 1px solid var(--primary-action-color); padding: 2px 4px; font-size: 13px; }
        .add-tab-btn {
            background: none; border: none; font-size: 20px; cursor: pointer; padding: 4px 8px;
            color: var(--control-icon-color); border-radius: 4px;
        }
        .add-tab-btn:hover { background-color: var(--tab-hover-bg); }
        .prompt-note-controls { display: flex; align-items: center; padding-bottom: 4px; }
        .prompt-note-toggle {
            cursor: pointer; border: none; background: none; font-size: 18px;
            font-weight: bold; padding: 0 5px; color: #aaa;
        }
        .prompt-note-toggle:hover { color: #000; }
        .prompt-note-content-wrapper { flex-grow: 1; padding: 12px; display: flex; overflow: hidden; }
        .prompt-note-content {
            width: 100%; height: 100%; border: none; background: transparent;
            resize: none; outline: none; font-size: 14px; line-height: 1.6;
            color: #222;
        }
        .prompt-note-content::placeholder { color: #bbb; }
        .prompt-note-resizer {
            position: absolute; bottom: 0; right: 0; width: 15px; height: 15px;
            cursor: se-resize; background: repeating-linear-gradient(-45deg, transparent, transparent 2px, #ccc 2px, #ccc 4px);
            border-bottom-right-radius: 10px;
        }
        .prompt-note-container.collapsed { height: 42px !important; min-height: 42px; }
        .prompt-note-container.collapsed .prompt-note-content-wrapper,
        .prompt-note-container.collapsed .prompt-note-resizer { display: none; }
        .prompt-note-container.minimized {
            width: 42px !important; height: 42px !important; min-height: 0; min-width: 0;
            border-radius: 50%; cursor: pointer; justify-content: center; align-items: center;
            font-size: 18px; font-weight: bold; color: white; background-color: var(--minimized-bg);
            overflow: hidden;
        }
        .prompt-note-container.minimized > * { display: none; }
        .prompt-note-container.minimized .minimized-text { display: block !important; }
        #add-new-note-btn {
            position: fixed; bottom: 90px; right: 25px; width: 50px; height: 50px;
            background-color: var(--primary-action-color); color: white; border: none; border-radius: 50%;
            font-size: 28px; line-height: 50px; text-align: center; cursor: pointer;
            box-shadow: 0 4px 10px rgba(0,0,0,0.2); z-index: 9999;
        }
        #prompt-note-context-menu {
            position: fixed; display: none; background-color: var(--note-bg); border: 1px solid var(--note-border-color);
            border-radius: 6px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);
            padding: 5px 0; font-size: 14px;
        }
        .context-menu-item { padding: 8px 20px; cursor: pointer; }
        .context-menu-item.disabled { color: #aaa; cursor: not-allowed; }
        .context-menu-item:not(.disabled):hover { background-color: var(--header-bg); }
        .context-menu-separator { margin: 4px 0; border: none; border-top: 1px solid #eee; }
    `);

    let notesData = [];
    const Z_INDEX_MANAGER = { base: 9900, top: 9900 };

    function init() {
        createAddButton();
        createContextMenu();
        loadNotes();
        document.addEventListener('click', () => hideContextMenu());
    }

    async function saveNotes() { await GM_setValue('promptNotesData', notesData); }

    async function loadNotes() {
        const storedData = await GM_getValue('promptNotesData', []);
        notesData = Array.isArray(storedData) ? storedData : [];
        if (notesData.length === 0) {
            createNewNote();
        } else {
            notesData.forEach(noteData => {
                if (noteData.zIndex && noteData.zIndex > Z_INDEX_MANAGER.top) Z_INDEX_MANAGER.top = noteData.zIndex;
                renderNote(noteData.id);
            });
        }
    }

    const getNoteData = (id) => notesData.find(n => n.id === id);
    const bringToFront = (noteContainer) => {
        const noteData = getNoteData(noteContainer.dataset.noteId);
        Z_INDEX_MANAGER.top++;
        noteData.zIndex = Z_INDEX_MANAGER.top;
        noteContainer.style.zIndex = noteData.zIndex;
        saveNotes();
    };

    function createNewNote() {
        Z_INDEX_MANAGER.top++;
        const newNote = {
            id: `note_${Date.now()}`,
            top: `${40 + (notesData.length % 10) * 20}px`, left: `${40 + (notesData.length % 10) * 20}px`,
            width: '300px', height: '220px', zIndex: Z_INDEX_MANAGER.top,
            isCollapsed: false, isMinimized: false, activeTabIndex: 0,
            tabs: [{ id: `tab_${Date.now()}`, title: '提示詞 1', content: '' }]
        };
        notesData.push(newNote);
        renderNote(newNote.id);
        saveNotes();
    }

    function renderNote(id) {
        const noteData = getNoteData(id);
        if (!noteData) return;
        let noteContainer = document.querySelector(`.prompt-note-container[data-note-id="${id}"]`);
        if (!noteContainer) {
            noteContainer = document.createElement('div');
            noteContainer.className = 'prompt-note-container';
            noteContainer.dataset.noteId = id;
            noteContainer.innerHTML = `<div class="prompt-note-header"><div class="prompt-note-tabs-container"></div><div class="prompt-note-controls"><button class="add-tab-btn" title="新增分頁">+</button><button class="prompt-note-toggle" title="收合/展開">—</button></div></div><div class="prompt-note-content-wrapper"><textarea class="prompt-note-content" placeholder="雙擊此處可將內容貼至對話框..."></textarea></div><div class="prompt-note-resizer"></div><span class="minimized-text" style="display:none;"></span>`;
            document.body.appendChild(noteContainer);
            setupNoteInteractions(noteContainer);
        }
        Object.assign(noteContainer.style, {
            top: noteData.top, left: noteData.left, width: noteData.width,
            height: noteData.height, zIndex: noteData.zIndex || Z_INDEX_MANAGER.base
        });
        noteContainer.classList.toggle('minimized', noteData.isMinimized);
        noteContainer.classList.toggle('collapsed', noteData.isCollapsed && !noteData.isMinimized);
        if (noteData.isMinimized) {
            const firstLetter = (noteData.tabs[noteData.activeTabIndex]?.title || 'N').charAt(0);
            noteContainer.querySelector('.minimized-text').textContent = firstLetter;
        } else {
            noteContainer.querySelector('.prompt-note-toggle').textContent = noteData.isCollapsed ? '+' : '—';
            renderTabs(noteContainer);
        }
    }

    function renderTabs(noteContainer) {
        const id = noteContainer.dataset.noteId;
        const noteData = getNoteData(id);
        const tabsContainer = noteContainer.querySelector('.prompt-note-tabs-container');
        const contentTextarea = noteContainer.querySelector('.prompt-note-content');
        tabsContainer.innerHTML = '';
        noteData.tabs.forEach((tab, index) => {
            const tabEl = document.createElement('div');
            tabEl.className = 'prompt-note-tab';
            tabEl.textContent = tab.title;
            tabEl.dataset.tabId = tab.id;
            tabEl.dataset.tabIndex = index;
            if (index === noteData.activeTabIndex) {
                tabEl.classList.add('active');
                contentTextarea.value = tab.content;
            }
            tabsContainer.appendChild(tabEl);
        });
        const activeTabEl = tabsContainer.querySelector('.active');
        if (activeTabEl) activeTabEl.scrollIntoView({ block: 'nearest', inline: 'center' });
    }

    function setupNoteInteractions(noteContainer) {
        const id = noteContainer.dataset.noteId;
        const contentTextarea = noteContainer.querySelector('.prompt-note-content');

        setupDragging(noteContainer.querySelector('.prompt-note-header'), noteContainer);
        setupResizing(noteContainer.querySelector('.prompt-note-resizer'), noteContainer);

        noteContainer.addEventListener('mousedown', () => bringToFront(noteContainer), { capture: true });
        noteContainer.addEventListener('contextmenu', e => showContextMenu(e, id));

        contentTextarea.addEventListener('input', (e) => {
            const noteData = getNoteData(id);
            noteData.tabs[noteData.activeTabIndex].content = e.target.value;
            saveNotes();
        });

        // ✅✅✅ --- 這一段就是最終修正 --- ✅✅✅
        // 將原本的 'click' 事件改為 'dblclick' (雙擊)
        contentTextarea.addEventListener('dblclick', (e) => {
            const targetTextarea = document.querySelector('#prompt-textarea');
            if (targetTextarea && e.target.value.trim() !== "") {
                targetTextarea.value = e.target.value;
                targetTextarea.dispatchEvent(new Event('input', { bubbles: true }));
                targetTextarea.focus();
                setTimeout(() => {
                    targetTextarea.style.height = 'auto';
                    targetTextarea.style.height = `${targetTextarea.scrollHeight}px`;
                }, 0);
            }
        });
        // ✅✅✅ --- 修正結束 --- ✅✅✅

        noteContainer.querySelector('.add-tab-btn').addEventListener('click', () => addNewTab(id));
        noteContainer.querySelector('.prompt-note-toggle').addEventListener('click', () => toggleCollapse(id));
        noteContainer.addEventListener('click', (e) => { // 監聽整個便簽的點擊
            if(e.target.classList.contains('prompt-note-container') && getNoteData(id).isMinimized) {
                getNoteData(id).isMinimized = false;
                renderNote(id);
                saveNotes();
            }
        });

        const tabsContainer = noteContainer.querySelector('.prompt-note-tabs-container');
        tabsContainer.addEventListener('click', e => {
            const tabEl = e.target.closest('.prompt-note-tab');
            if (tabEl) switchTab(id, parseInt(tabEl.dataset.tabIndex));
        });
        tabsContainer.addEventListener('dblclick', e => {
            const tabEl = e.target.closest('.prompt-note-tab');
            if (tabEl) renameTab(tabEl);
        });
        tabsContainer.addEventListener('contextmenu', e => {
            const tabEl = e.target.closest('.prompt-note-tab');
            if (tabEl) showContextMenu(e, id, tabEl.dataset.tabId);
        });
    }

    function setupDragging(handle, target) {
        handle.addEventListener('mousedown', e => {
            if (e.target.closest('button, input, .prompt-note-tabs-container')) return;
            bringToFront(target);
            const rect = target.getBoundingClientRect();
            const offsetX = e.clientX - rect.left, offsetY = e.clientY - rect.top;
            const onMouseMove = (e) => {
                target.style.left = `${e.clientX - offsetX}px`;
                target.style.top = `${e.clientY - offsetY}px`;
            };
            const onMouseUp = () => {
                document.removeEventListener('mousemove', onMouseMove);
                document.removeEventListener('mouseup', onMouseUp);
                const noteData = getNoteData(target.dataset.noteId);
                noteData.left = target.style.left;
                noteData.top = target.style.top;
                saveNotes();
            };
            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', onMouseUp);
        });
    }

    function setupResizing(handle, target) {
        handle.addEventListener('mousedown', e => {
            e.preventDefault();
            const rect = target.getBoundingClientRect();
            const startX = e.clientX, startY = e.clientY;
            const onMouseMove = (e) => {
                target.style.width = `${rect.width + (e.clientX - startX)}px`;
                target.style.height = `${rect.height + (e.clientY - startY)}px`;
            };
            const onMouseUp = () => {
                document.removeEventListener('mousemove', onMouseMove);
                document.removeEventListener('mouseup', onMouseUp);
                const noteData = getNoteData(target.dataset.noteId);
                noteData.width = target.style.width;
                noteData.height = target.style.height;
                saveNotes();
            };
            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', onMouseUp);
        });
    }

    function switchTab(noteId, tabIndex) {
        const noteData = getNoteData(noteId);
        noteData.activeTabIndex = tabIndex;
        renderTabs(document.querySelector(`.prompt-note-container[data-note-id="${noteId}"]`));
        saveNotes();
    }

    function addNewTab(noteId) {
        const noteData = getNoteData(noteId);
        const newTab = { id: `tab_${Date.now()}`, title: `提示詞 ${noteData.tabs.length + 1}`, content: '' };
        noteData.tabs.push(newTab);
        noteData.activeTabIndex = noteData.tabs.length - 1;
        renderTabs(document.querySelector(`.prompt-note-container[data-note-id="${noteId}"]`));
        saveNotes();
    }

    function renameTab(tabEl) {
        const originalText = tabEl.textContent;
        tabEl.innerHTML = '';
        const input = document.createElement('input');
        input.type = 'text', input.value = originalText;
        tabEl.appendChild(input);
        input.focus(), input.select();
        const finishEditing = () => {
            const newTitle = input.value.trim() || originalText;
            tabEl.textContent = newTitle;
            const noteId = tabEl.closest('.prompt-note-container').dataset.noteId;
            const tabId = tabEl.dataset.tabId;
            const tabData = getNoteData(noteId).tabs.find(t => t.id === tabId);
            if (tabData) tabData.title = newTitle;
            saveNotes();
            input.removeEventListener('blur', finishEditing);
            input.removeEventListener('keydown', onKeydown);
        };
        const onKeydown = e => { if (e.key === 'Enter') finishEditing(); };
        input.addEventListener('blur', finishEditing);
        input.addEventListener('keydown', onKeydown);
    }

    function toggleCollapse(noteId) {
        const noteData = getNoteData(noteId);
        noteData.isCollapsed = !noteData.isCollapsed;
        const noteContainer = document.querySelector(`.prompt-note-container[data-note-id="${noteId}"]`);
        noteContainer.classList.toggle('collapsed', noteData.isCollapsed);
        noteContainer.querySelector('.prompt-note-toggle').textContent = noteData.isCollapsed ? '+' : '—';
        saveNotes();
    }

    const contextMenu = document.createElement('div');
    contextMenu.id = 'prompt-note-context-menu';

    function createContextMenu() {
        document.body.appendChild(contextMenu);
        contextMenu.addEventListener('click', handleContextMenuClick);
    }
    const hideContextMenu = () => contextMenu.style.display = 'none';

    function showContextMenu(e, noteId, tabId = null) {
        e.preventDefault(), e.stopPropagation();
        const noteData = getNoteData(noteId);
        let menuItems = tabId ? `<div class="context-menu-item" data-action="add-tab">新增分頁</div><div class="context-menu-item" data-action="rename-tab">重新命名</div><div class="context-menu-item ${noteData.tabs.length > 1 ? '' : 'disabled'}" data-action="close-tab">關閉分頁</div><hr class="context-menu-separator">` : '';
        menuItems += `<div class="context-menu-item" data-action="minimize">最小化便簽</div><div class="context-menu-item" data-action="close-note">關閉便簽</div>`;
        contextMenu.innerHTML = menuItems;
        contextMenu.style.zIndex = Z_INDEX_MANAGER.top + 10;
        contextMenu.style.display = 'block';
        const { clientX: mouseX, clientY: mouseY } = e;
        const { innerWidth: vpWidth, innerHeight: vpHeight } = window;
        const { offsetWidth: menuWidth, offsetHeight: menuHeight } = contextMenu;
        contextMenu.style.left = `${mouseX + menuWidth > vpWidth ? mouseX - menuWidth : mouseX}px`;
        contextMenu.style.top = `${mouseY + menuHeight > vpHeight ? mouseY - menuHeight : mouseY}px`;
        contextMenu.dataset.noteId = noteId;
        contextMenu.dataset.tabId = tabId || '';
    }

    function handleContextMenuClick(e) {
        const target = e.target.closest('.context-menu-item');
        if (!target || target.classList.contains('disabled')) return;
        const { action } = target.dataset;
        const { noteId, tabId } = contextMenu.dataset;
        const noteData = getNoteData(noteId);
        const noteEl = document.querySelector(`.prompt-note-container[data-note-id="${noteId}"]`);
        switch (action) {
            case 'add-tab': addNewTab(noteId); break;
            case 'rename-tab': renameTab(noteEl.querySelector(`.prompt-note-tab[data-tab-id="${tabId}"]`)); break;
            case 'close-tab':
                const tabIndex = noteData.tabs.findIndex(t => t.id === tabId);
                noteData.tabs.splice(tabIndex, 1);
                noteData.activeTabIndex = Math.max(0, noteData.activeTabIndex - (noteData.activeTabIndex >= tabIndex ? 1 : 0));
                renderTabs(noteEl);
                saveNotes();
                break;
            case 'minimize':
                noteData.isMinimized = true;
                renderNote(noteId);
                saveNotes();
                break;
            case 'close-note':
                noteEl.remove();
                notesData = notesData.filter(n => n.id !== noteId);
                saveNotes();
                break;
        }
        hideContextMenu();
    }

    function createAddButton() {
        const addButton = document.createElement('button');
        addButton.id = 'add-new-note-btn', addButton.textContent = '+', addButton.title = '新增便簽';
        document.body.appendChild(addButton);
        addButton.addEventListener('click', createNewNote);
    }

    window.addEventListener('load', init);
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址