Prompt Manager (Fixed Vertical Drag with Copy & Close)

在AI网站上保存并快速使用 Prompts,同时支持拖动按钮及位置保存 —— 修复按钮只能横向拖动的问题,增大关闭按钮点击区域并上移碰撞箱,复制后显示成功并自动关闭

// ==UserScript==
// @name         Prompt Manager (Fixed Vertical Drag with Copy & Close)
// @namespace    http://tampermonkey.net/
// @version      2.7.5
// @description  在AI网站上保存并快速使用 Prompts,同时支持拖动按钮及位置保存 —— 修复按钮只能横向拖动的问题,增大关闭按钮点击区域并上移碰撞箱,复制后显示成功并自动关闭
// @author       schweigen
// @match        https://chatgpt.com/*
// @match        https://claude.ai/*
// @match        https://chat.deepseek.com/*
// @match        https://www.perplexity.ai/*
// @match        https://chat.mistral.ai/*
// @match        https://app.nextchat.dev/*
// @match        https://chat01.ai/*
// @match        https://you.com/*
// @match        https://chatgpt.aicnm.cc/*
// @match        https://chatshare.xyz/*
// @match        https://chat.biggraph.net/*
// @match        https://grok.com/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_deleteValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // === 用户可编辑的 Prompts 列表 ===
    const prompts = [
        {
            title: "",
            content: ``
        },
        {
            title: "",
            content: ``
        },
        {
            title: "",
            content: ``
        },
        {
            title: "",
            content: ``
        },
        {
            title: "",
            content: ``
        },
        {
            title: "",
            content: ``
        },
        {
            title: "",
            content: ``
        },
        {
            title: "",
            content: ``
        },
        {
            title: "",
            content: ``
        },
        {
            title: "",
            content: ``
        },
    ];

    // 添加必要的样式
    GM_addStyle(`
        /* Prompt Manager 容器样式 */
        #prompt-manager {
            position: fixed !important;
            top: 80px !important;
            right: 20px !important;
            width: 350px !important;
            max-height: 80vh !important;
            overflow-y: auto !important;
            overflow-x: visible !important;
            background: #ffffff !important;
            border: 1px solid #e1e4e8 !important;
            border-radius: 12px !important;
            box-shadow: 0 4px 16px rgba(0,0,0,0.1) !important;
            z-index: 2147483647 !important;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
            display: block !important;
            color: #24292e !important;
            opacity: 1 !important;
            visibility: visible !important;
        }

        #prompt-manager.hidden {
            display: none !important;
        }

        /* 标题样式 */
        #prompt-manager h2 {
            margin: 0 !important;
            padding: 16px !important;
            background: #2c3e50 !important;
            color: #ffffff !important;
            border-radius: 12px 12px 0 0 !important;
            text-align: center !important;
            font-size: 18px !important;
            font-weight: 600 !important;
            position: relative !important;
        }

        /* 关闭按钮样式(碰撞箱上移) */
        #close-prompt-btn {
            position: absolute !important;
            top: -10px !important; /* 向上移动显示区域 */
            right: 0 !important;
            padding: 10px 16px !important;
            cursor: pointer !important;
            font-size: 20px !important;
            color: #ffffff !important;
            user-select: none !important;
        }

        /* Prompt 项样式 */
        .prompt-item {
            border-bottom: 1px solid #e1e4e8 !important;
            padding: 12px 16px !important;
            position: relative !important;
            transition: all 0.2s ease !important;
            background: #ffffff !important;
        }

        .prompt-item:hover {
            background: #f6f8fa !important;
        }

        .prompt-title {
            font-weight: 500 !important;
            cursor: pointer !important;
            position: relative !important;
            display: flex !important;
            justify-content: space-between !important;
            align-items: center !important;
            color: #2c3e50 !important;
        }

        .prompt-content {
            display: none !important;
            margin-top: 8px !important;
            white-space: pre-wrap !important;
            background: #f8f9fa !important;
            padding: 12px !important;
            border-radius: 6px !important;
            cursor: pointer !important;
            transition: background 0.2s ease !important;
            color: #2c3e50 !important;
            border: 1px solid #e1e4e8 !important;
        }

        .prompt-content:hover {
            background: #edf2f7 !important;
        }

        /* 复制按钮样式 */
        .copy-button {
            background: #3498db !important;
            color: #ffffff !important;
            border: none !important;
            padding: 6px 12px !important;
            border-radius: 4px !important;
            cursor: pointer !important;
            font-size: 12px !important;
            margin-left: 10px !important;
            transition: all 0.2s ease !important;
        }

        .copy-button:hover {
            background: #2980b9 !important;
            transform: translateY(-1px) !important;
        }

        /* Toggle 按钮样式 */
        #toggle-prompt-btn {
            position: fixed !important;
            top: 60px !important;
            right: 20px !important;
            width: 40px !important;
            height: 40px !important;
            background: #3498db !important;
            color: #ffffff !important;
            border: none !important;
            border-radius: 50% !important;
            cursor: pointer !important;
            font-size: 20px !important;
            display: flex !important;
            align-items: center !important;
            justify-content: center !important;
            z-index: 2147483647 !important;
            transition: all 0.2s ease !important;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1) !important;
            opacity: 1 !important;
            visibility: visible !important;
        }

        #toggle-prompt-btn:hover {
            background: #2980b9 !important;
            transform: translateY(-1px) !important;
        }

        /* 复制成功提示样式 */
        #copy-success {
            position: fixed !important;
            top: 100px !important;
            right: 20px !important;
            background: #2ecc71 !important;
            color: #ffffff !important;
            padding: 8px 16px !important;
            border-radius: 6px !important;
            opacity: 0 !important;
            transition: opacity 0.3s ease !important;
            z-index: 2147483647 !important;
            font-size: 14px !important;
            box-shadow: 0 2px 8px rgba(0,0,0,0.1) !important;
        }

        /* 内部成功提示样式 */
        .inner-success {
            background: #2ecc71 !important;
            color: #ffffff !important;
            padding: 8px 12px !important;
            margin-top: 8px !important;
            border-radius: 6px !important;
            text-align: center !important;
            font-size: 14px !important;
            display: none !important;
        }

        /* 搜索输入框样式 */
        #search-input {
            width: calc(100% - 32px) !important;
            padding: 10px 12px !important;
            margin: 16px !important;
            border: 1px solid #e1e4e8 !important;
            border-radius: 6px !important;
            background: #f8f9fa !important;
            color: #2c3e50 !important;
            font-size: 14px !important;
            transition: all 0.2s ease !important;
        }

        #search-input:focus {
            outline: none !important;
            border-color: #3498db !important;
            box-shadow: 0 0 0 2px rgba(52,152,219,0.2) !important;
        }

        #search-input::placeholder {
            color: #95a5a6 !important;
        }
    `);

    // 确保DOM加载完成后再创建元素
    function createElements() {
        // 创建 Toggle 按钮
        const toggleBtn = document.createElement('button');
        toggleBtn.id = 'toggle-prompt-btn';
        toggleBtn.title = '隐藏/显示 Prompt Manager';
        toggleBtn.innerHTML = '☰';
        document.body.appendChild(toggleBtn);

        // 如果用户之前拖动过,则恢复按钮保存的位置
        const savedX = GM_getValue('toggleBtnX', null);
        const savedY = GM_getValue('toggleBtnY', null);
        if (savedX !== null && savedY !== null) {
            toggleBtn.style.setProperty('left', savedX + 'px', 'important');
            toggleBtn.style.setProperty('top', savedY + 'px', 'important');
            toggleBtn.style.setProperty('right', 'auto', 'important');
        }

        // 创建 Prompt Manager 容器,增加了关闭叉号
        const manager = document.createElement('div');
        manager.id = 'prompt-manager';
        manager.classList.add('hidden'); // 默认隐藏
        manager.innerHTML = `
            <h2>
                Prompts
                <span id="close-prompt-btn" title="关闭">×</span>
            </h2>
            <input type="text" id="search-input" placeholder="搜索 Prompts...">
            <div id="prompt-list"></div>
        `;
        document.body.appendChild(manager);

        // 为关闭叉号添加点击事件
        const closeBtn = document.getElementById('close-prompt-btn');
        closeBtn.addEventListener('click', () => {
            manager.classList.add('hidden');
        });

        // 创建复制成功提示
        const copySuccess = document.createElement('div');
        copySuccess.id = 'copy-success';
        copySuccess.textContent = '复制成功';
        document.body.appendChild(copySuccess);

        // 创建一个 Prompt 项
        function createPromptItem(prompt, index) {
            const item = document.createElement('div');
            item.className = 'prompt-item';

            const title = document.createElement('div');
            title.className = 'prompt-title';

            const titleText = document.createElement('span');
            titleText.textContent = prompt.title || "无标题 Prompt";

            const copyTitleBtn = document.createElement('button');
            copyTitleBtn.className = 'copy-button';
            copyTitleBtn.textContent = '复制';
            copyTitleBtn.title = '复制整个 Prompt 内容';

            // 创建内部成功提示元素
            const innerSuccess = document.createElement('div');
            innerSuccess.className = 'inner-success';
            innerSuccess.textContent = '复制成功';
            innerSuccess.style.display = 'none';

            copyTitleBtn.onclick = (e) => {
                e.stopPropagation();
                if (prompt.content) {
                    copyToClipboard(prompt.content, item);
                } else {
                    showInnerSuccess(item, '内容为空,无法复制。');
                }
            };

            // 仅添加标题和复制按钮
            title.appendChild(titleText);
            title.appendChild(copyTitleBtn);

            const content = document.createElement('div');
            content.className = 'prompt-content';
            content.textContent = prompt.content || "无内容 Prompt";

            content.addEventListener('click', () => {
                if (prompt.content) {
                    copyToClipboard(prompt.content, item);
                } else {
                    showInnerSuccess(item, '内容为空,无法复制。');
                }
            });

            // 仅添加点击切换内容显示
            title.addEventListener('click', () => {
                const isVisible = content.style.display === 'block';
                content.style.display = isVisible ? 'none' : 'block';
            });

            item.appendChild(title);
            item.appendChild(content);
            item.appendChild(innerSuccess); // 添加内部成功提示

            return item;
        }

        // 渲染 Prompts 列表
        function renderPrompts(filter = '') {
            const promptList = document.getElementById('prompt-list');
            promptList.innerHTML = '';
            const filtered = prompts.filter(p =>
                (p.title && p.title.toLowerCase().includes(filter.toLowerCase())) ||
                (p.content && p.content.toLowerCase().includes(filter.toLowerCase()))
            );
            filtered.forEach((prompt, index) => {
                const item = createPromptItem(prompt, index);
                promptList.appendChild(item);
            });
        }

        // 复制到剪贴板并显示成功提示
        function copyToClipboard(text, promptItem) {
            navigator.clipboard.writeText(text).then(() => {
                showInnerSuccess(promptItem, '复制成功');
            }).catch(err => {
                console.error('复制失败: ', err);
                showInnerSuccess(promptItem, '复制失败,请手动复制。');
            });
        }

        // 显示成功提示并立即关闭面板
        function showInnerSuccess(promptItem, message = '复制成功') {
            // 直接关闭面板,不显示内部提示
            document.getElementById('prompt-manager').classList.add('hidden');

            // 在外部显示一个简短的提示
            showCopySuccess(message);
        }

        // 显示复制成功提示(保留旧函数以兼容)
        function showCopySuccess(message = '复制成功') {
            copySuccess.textContent = message;
            copySuccess.style.opacity = '1';
            setTimeout(() => {
                copySuccess.style.opacity = '0';
            }, 1500);
        }

        // ======= 以下为拖拽功能 =======
        let isDragging = false, justDragged = false, startX, startY, origLeft, origTop;

        toggleBtn.addEventListener('mousedown', function(e) {
            if (e.button !== 0) return; // 仅响应鼠标左键
            isDragging = false;
            startX = e.clientX;
            startY = e.clientY;
            // 获取当前按钮的位置(相对于视口)
            const rect = toggleBtn.getBoundingClientRect();
            origLeft = rect.left;
            origTop = rect.top;

            function onMouseMove(e) {
                const dx = e.clientX - startX;
                const dy = e.clientY - startY;
                if (!isDragging) {
                    // 超过 5px 视为拖拽操作
                    if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
                        isDragging = true;
                    }
                }
                if (isDragging) {
                    // 使用 setProperty 带上 'important' 以覆盖样式中的 !important
                    toggleBtn.style.setProperty('left', (origLeft + dx) + 'px', 'important');
                    toggleBtn.style.setProperty('top', (origTop + dy) + 'px', 'important');
                    toggleBtn.style.setProperty('right', 'auto', 'important');
                    e.preventDefault();
                }
            }

            function onMouseUp(e) {
                document.removeEventListener('mousemove', onMouseMove);
                document.removeEventListener('mouseup', onMouseUp);
                if (isDragging) {
                    justDragged = true;
                    // 保存新位置
                    const newLeft = parseInt(toggleBtn.style.left, 10);
                    const newTop = parseInt(toggleBtn.style.top, 10);
                    GM_setValue('toggleBtnX', newLeft);
                    GM_setValue('toggleBtnY', newTop);
                }
            }

            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', onMouseUp);
        });

        // 修改点击事件,避免拖拽后触发点击
        toggleBtn.addEventListener('click', (e) => {
            if (justDragged) {
                justDragged = false;
                return;
            }
            manager.classList.toggle('hidden');
        });

        // Toggle 按钮快捷键显示/隐藏 Prompt Manager (Ctrl/Command + O)
        document.addEventListener('keydown', (e) => {
            const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
            const modifier = isMac ? e.metaKey : e.ctrlKey;

            if (modifier && e.key.toLowerCase() === 'o') {
                e.preventDefault();
                manager.classList.toggle('hidden');
            }
        });

        // 搜索 Prompts
        const searchInput = document.getElementById('search-input');
        searchInput.addEventListener('input', () => {
            renderPrompts(searchInput.value);
        });

        // 初始渲染
        renderPrompts();
    }

    // 确保DOM加载完成后再创建元素
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', createElements);
    } else {
        createElements();
    }

    // 每隔一秒检查一次是否需要重新创建元素(用于处理某些网站的动态加载)
    let checkInterval = setInterval(() => {
        if (!document.getElementById('toggle-prompt-btn')) {
            createElements();
        }
    }, 1000);

    // 5分钟后停止检查,以避免无限循环
    setTimeout(() => {
        clearInterval(checkInterval);
    }, 300000); // 5分钟

    // ======= 添加油猴菜单命令,用于重置按钮默认位置 =======
    GM_registerMenuCommand("重置按钮默认位置", () => {
        GM_deleteValue('toggleBtnX');
        GM_deleteValue('toggleBtnY');
        const toggleBtn = document.getElementById('toggle-prompt-btn');
        if (toggleBtn) {
            toggleBtn.style.setProperty('top', '60px', 'important');
            toggleBtn.style.setProperty('right', '20px', 'important');
            toggleBtn.style.removeProperty('left');
        }
    });
})();