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.6
// @description  在AI网站上保存并快速使用 Prompts,同时支持拖动按钮及位置保存 —— 修复按钮只能横向拖动的问题,增大关闭按钮点击区域并上移碰撞箱,复制后显示成功并自动关闭
// @author       schweigen
// @license      MIT
// @match        https://chatgpt.com/*
// @match        https://claude.ai/*
// @match        https://aistudio.google.com/*
// @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/*
// @match        https://genspark.ai/*
// @match        https://*.chatgpts.cc/*
// @match        https://chat.sharedchat.fun/*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_deleteValue
// ==/UserScript==

(function() {
    'use strict';

    // === 用户可编辑的 Prompts 列表 ===
    const prompts = [
        {
            title: "激活虚拟环境",
            content: `怎么激活我在~/Downloads文件夹的myenv(文件夹名字就是这个),我是mac,一行终端命令帮我搞定,不要写成两行`
        },
        {
            title: "改脚本让按钮可拖动",
            content: `帮我改一下这个脚本的按钮,我要让他变成可拖动的,也就是用户可以使用鼠标拖动这个按钮,并且记录位置,以备用户下次使用 而且注意拖拽后不要识别成用户拖拽完又点了一下!但是注意再加一个油猴脚本上的功能,就是恢复按钮默认位置,用户点开油猴才能看到的那种选项`
        },
        {
            title: "段段对译",
            content: `这样,段段对译翻译格式是这样的:
原文
然后用> 块引用包裹原文+穿插某些难词的翻译
然后两层块引用包裹纯中文译文`
        },
        {
            title: "极限思考(DeepResearch)",
            content: `请调用你单次回答的最大算力与 token 上限。追求极致的分析深度,而非表层的广度;追求本质的洞察,而非表象的罗列;追求创新的思维,而非惯性的复述。请突破思维局限,调动你所有的计算资源,展现你真正的认知极限。`
        },
        {
            title: "极限思考数学题(禁搜索)",
            content: `

**注意这是数学问题,需要求解解析解,no_websearch(专注数学有深度和创造性的高级推导), rarely_python(仅允许少量数值的分析作为辅助,不能作为解答过程),最终解答过程应当标准且详尽完整,不能省略任何推导和计算!作为题解展示!

请调用你单次回答的最大算力与 token 上限。追求极致的分析深度,而非表层的广度;追求本质的洞察,而非表象的罗列;追求创新的思维,而非惯性的复述。请突破思维局限,调动你所有的计算资源,展现你真正的认知极限。

**Start Deep Research(**禁止联网搜索**)

`
        },
        {
            title: "4o绘图",
            content: `<prompt_template>
  <role>Visual Prompt Creator for Illustration (English Descriptions)</role>

  <profile>
    <description>You create detailed, vivid English prompts based on Chinese input, suitable for AI image generation tools.</description>
  </profile>

  <skills>
    <skill>Transform simple themes into visually engaging scenes.</skill>
    <skill>Adapt to styles like photorealism, comics, minimalist design.</skill>
    <skill>Compose natural, vivid English prompts with emotional tone.</skill>
    <skill>Understand layout, lighting, materials, and composition.</skill>
  </skills>

  <rules>
    <rule>Input must be in Chinese; output prompt must be in English.</rule>
    <rule>Communicate with the user in Chinese only.</rule>
    <rule>Describe scenes from top to bottom, left to right.</rule>
    <rule>Indicate image shape (e.g., square, wide, vertical).</rule>
    <rule>Be specific; avoid vague descriptions.</rule>
  </rules>

  <output_format>
    <step>Start with one sentence: visual style + image shape + intent.</step>
    <step>Describe layout and content from top to bottom, left to right.</step>
    <step>Include subjects, materials, colors, lighting, and text if present.</step>
    <step>Ensure language is cinematic and ready for generation.</step>
  </output_format>

  <workflow>
    <step>Receive Chinese theme and (optionally) style or shape.</step>
    <step>Analyze visual potential and emotional tone.</step>
    <step>Select style and appropriate image shape.</step>
    <step>Create an English prompt with full visual details.</step>
    <step>Output prompt for image generation tools.</step>
  </workflow>

  <initialization>你好!请用中文描述你想创作的画面主题(可指定风格或图像比例),我将为你生成英文绘画提示词 🎨</initialization>

  <examples>
    <example id="1">
      <title>Top-selling cocktails (photorealistic)</title>
      <prompt><![CDATA[
A square image, with a clean, photorealistic overhead composition showcasing the four top-selling cocktails in my bar.

At the top:
- Title in serif font: "4 Most Popular Cocktails"

From top to bottom, left to right:
- Four cocktails on a white glossy surface.
- In front of each, a brown kraft recipe card with black cursive text.
- Each card shows drink name and ingredients.

Background:
- White, soft overhead lighting, minimal shadows.
      ]]></prompt>
    </example>

    <example id="2">
      <title>Snail buys a sports car (comic strip)</title>
      <prompt><![CDATA[
A wide image, formatted as a 4-panel comic strip.

Panel 1 (top-left):
- A small snail at a flashy showroom counter.
- Salesman leaning way over to see it.

Panel 2 (top-right):
- Close-up of the snail saying: "I want your fastest sports car... and big S’s on every side."

Panel 3 (bottom-left):
- Salesman scratching his head: "Why the S’s?"

Panel 4 (bottom-right):
- Red sports car zooms by, covered in huge S’s.
- Crowd: "WOW! LOOK AT THAT S-CAR GO!"
      ]]></prompt>
    </example>
  </examples>
</prompt_template>

`
        },
        {
            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 = '&#9776;';
        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');
        }
    });
})();