ChatGPT Prompt Presets

Enhance ChatGPT experience by adding customizable prompt presets.

От 06.04.2025. Виж последната версия.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         ChatGPT Prompt Presets
// @namespace    http://tampermonkey.net/
// @version      1.4
// @description  Enhance ChatGPT experience by adding customizable prompt presets.
// @author       Konhz
// @match        https://chatgpt.com/*
// @grant        GM_xmlhttpRequest
// @connect      api.github.com
// ==/UserScript==


(function () {
    'use strict';

    const i18nMap = {
        zh: {
            settingsTitle: "ChatGPT 自定义设置",
            chatWidthLabel: "对话区域宽度",
            reset: "恢复默认",
            promptDataTitle: "📦 Prompt 数据管理",
            export: "📤 导出",
            import: "📥 导入",
            gistId: "Gist ID",
            gistToken: "GitHub Token",
            gistIdPlaceholder: "请输入 GitHub Gist ID",
            gistTokenPlaceholder: "可选,支持私有 Gist",
            upload: "⬆️ 上传",
            download: "⬇️ 拉取",
            addPrompt: "➕ 添加",
            deleteConfirm: title => `是否删除 Prompt「${title}」?`,
            importOverwriteConfirm: count => `导入将覆盖当前 ${count} 条 prompt,是否继续?`,
            uploadSuccess: "上传成功",
            uploadFail: (status, msg) => `上传失败: ${status}\n${msg}`,
            uploadFail_onerror: "上传失败",
            fetchSuccess: "同步成功",
            fetchFail: (status, msg) => `拉取失败: ${status}\n${msg}`,
            fetchFail_onerror: "拉取失败",
            parseError: msg => `解析失败: ${msg}`,
            importSuccess: "导入成功",
            importFail: msg => `导入失败:${msg}`,
            titleEmpty: "标题和内容不能为空",
            lengthExceeded: "长度超限",
            fileNotFound: '未找到 chatgpt_prompts.json 文件',
            formatInvalid: '格式不正确',
            formatNotArray: "格式错误:不是数组",
            formatInvalidField: "格式错误:字段不合法",
            openSettings: "打开设置",
            titlePlaceholder: "题目 (≤10字)",
            contentPlaceholder: "内容 (≤1000字)",
            editPrompt: "✏️ 编辑",
            deletePrompt: "🗑️ 删除",
            promptTips: "提示:请在浮动按钮中右键编辑或删除 Prompt",
            duplicateTitle: "标题已存在,请修改",
            save: "保存",
            cancel: "取消",
            promptBulkDeleteTitle: "🧹 批量删除",
            promptBulkDeleteButton: "删除所选",
            promptBulkDeleteConfirm: count => `确认删除 ${count} 条 Prompt?`,
            promptBulkDeleteNone: "未选择任何 Prompt",
        },
        en: {
            settingsTitle: "ChatGPT Custom Settings",
            chatWidthLabel: "Chat Width",
            reset: "Reset",
            promptDataTitle: "📦 Prompt Management",
            export: "📤 Export",
            import: "📥 Import",
            gistId: "Gist ID",
            gistToken: "GitHub Token",
            gistIdPlaceholder: "Enter GitHub Gist ID",
            gistTokenPlaceholder: "Optional, supports private Gists",
            upload: "⬆️ Upload",
            download: "⬇️ Download",
            addPrompt: "➕ Add",
            deleteConfirm: title => `Delete prompt \"${title}\"?`,
            importOverwriteConfirm: count => `Import will overwrite ${count} prompts. Continue?`,
            uploadSuccess: "Upload successful",
            uploadFail: (status, msg) => `Upload failed: ${status}\n${msg}`,
            uploadFail_onerror: "Upload failed",
            fetchSuccess: "Sync successful",
            fetchFail: (status, msg) => `Download failed: ${status}\n${msg}`,
            fetchFail_onerror: "Download failed",
            parseError: msg => `Parse error: ${msg}`,
            importFail: msg => `Import failed: ${msg}`,
            importSuccess: "Import Success",
            titleEmpty: "Title and content cannot be empty",
            lengthExceeded: "Length exceeded",
            fileNotFound: 'chatgpt_prompts.json not found',
            formatInvalid: 'Invalid format',
            formatNotArray: "Format error: not an array",
            formatInvalidField: "Format error: invalid field structure",
            openSettings: "Open settings",
            titlePlaceholder: "Title (≤10 chars)",
            contentPlaceholder: "Content (≤1000 chars)",
            editPrompt: "✏️ Edit",
            deletePrompt: "🗑️ Delete",
            gistId: "Gist ID:",
            gistToken: "GitHub Token:",
            promptTips: "Tip: Right-click a floating button to edit or delete a prompt",
            duplicateTitle: "Title already exists. Please choose another.",
            save: "Save",
            cancel: "Cancel",
            promptBulkDeleteTitle: "🧹 Bulk Delete",
            promptBulkDeleteButton: "Delete Selected",
            promptBulkDeleteConfirm: count => `Are you sure you want to delete ${count} prompts?`,
            promptBulkDeleteNone: "No prompts selected",
        }
    };

    const lang = navigator.language?.split('-')[0] || 'en';
    const t = i18nMap[lang] || i18nMap.en;

    const STORAGE_KEY = 'chatgpt_enhancer_config';

    const defaultConfig = {
        customChatWidthPercent: 50,
        prompts: [],
        gistId: localStorage.getItem('gist_id') || '',
        gistToken: '',
    };


    const config = loadConfig();
    let settingsPanel = null;

    function loadConfig() {
        const saved = localStorage.getItem(STORAGE_KEY);
        return saved ? { ...defaultConfig, ...JSON.parse(saved) } : { ...defaultConfig };
    }

    function saveConfig() {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
    }

    function uploadPromptsToGist(gistId, token) {
        const url = `https://api.github.com/gists/${gistId}`;
        GM_xmlhttpRequest({
            method: 'PATCH',
            url: url,
            headers: {
                'Content-Type': 'application/json',
                ...(token ? { 'Authorization': `token ${token}` } : {})
            },
            data: JSON.stringify({
                files: {
                    'chatgpt_prompts.json': {
                        content: JSON.stringify(config.prompts, null, 2)
                    }
                }
            }),
            onload: function (response) {
                if (response.status === 200) {
                    alert(t.uploadSuccess);
                } else {
                    alert(t.uploadFail(response.status, response.responseText));
                }
            },
            onerror: function () {
                alert(t.uploadFail_onerror);
            }
        });
    }

    function fetchPromptsFromGist(gistId, token = null) {
        const url = `https://api.github.com/gists/${gistId}`;
        GM_xmlhttpRequest({
            method: 'GET',
            url: url,
            headers: {
                ...(token ? { 'Authorization': `token ${token}` } : {})
            },
            onload: function (response) {
                if (response.status !== 200) {
                    alert(t.fetchFail(response.status, response.responseText));
                    return;
                }

                try {
                    const data = JSON.parse(response.responseText);
                    const content = data.files?.['chatgpt_prompts.json']?.content;
                    if (!content) return alert(t.fileNotFound);

                    const imported = JSON.parse(content);
                    if (!Array.isArray(imported)) throw new Error(t.formatInvalid);

                    config.prompts = imported;
                    saveConfig();
                    renderPromptButtons();

                    if (settingsPanel) {
                        const container = document.getElementById('promptEditorContainer');
                        if (container) {
                            container.innerHTML = '';
                            createPromptEditor(container, isDarkTheme());
                        }
                    }

                    alert(t.fetchSuccess);
                } catch (e) {
                    alert(t.parseError(e.message));
                }
            },
            onerror: function () {
                alert(t.fetchFail_onerror);
            }
        });
    }

    function exportPrompts() {
        const dataStr = JSON.stringify(config.prompts, null, 2);
        const blob = new Blob([dataStr], { type: 'application/json' });
        const url = URL.createObjectURL(blob);

        const a = document.createElement('a');
        a.href = url;
        a.download = 'chatgpt-prompts.json';
        a.click();

        URL.revokeObjectURL(url);
    }

    function importPrompts() {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = '.json';

        input.onchange = () => {
            const file = input.files[0];
            if (!file) return;

            const reader = new FileReader();
            reader.onload = (e) => {
                try {
                    const imported = JSON.parse(e.target.result);
                    if (!Array.isArray(imported)) throw new Error(t.formatNotArray);

                    const valid = imported.every(p =>
                                                 typeof p.title === 'string' &&
                                                 typeof p.content === 'string' &&
                                                 p.title.length <= 10 &&
                                                 p.content.length <= 1000
                                                );

                    if (!valid) throw new Error(t.formatInvalidField);

                    if (confirm(t.importOverwriteConfirm(config.prompts.length))) {
                        config.prompts = imported;
                        saveConfig();
                        renderPromptButtons();
                        if (settingsPanel) {
                            const container = document.getElementById('promptEditorContainer');
                            if (container) {
                                container.innerHTML = '';
                                createPromptEditor(container, isDarkTheme());
                            }
                        }
                        alert(t.importSuccess);
                    }
                } catch (err) {
                    alert(t.importFail(err.message));
                }
            };
            reader.readAsText(file);
        };

        input.click();
    }


    function isDarkTheme() {
        const bgColor = window.getComputedStyle(document.body).backgroundColor;
        if (!bgColor) return false;
        const rgb = bgColor.match(/\d+/g).map(Number);
        const brightness = (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000;
        return brightness < 128;
    }

    function injectSettingsButton() {
        if (document.getElementById('cgpt-enhancer-settings-btn')) return;

        const btn = document.createElement('button');
        btn.id = 'cgpt-enhancer-settings-btn';
        btn.innerHTML = '⚙️';
        Object.assign(btn.style, {
            position: 'fixed',
            bottom: '20px',
            right: '20px',
            zIndex: '9999',
            fontSize: '18px',
            padding: '8px 10px',
            background: '#fff',
            border: '1px solid #ccc',
            borderRadius: '50%',
            cursor: 'pointer',
            boxShadow: '0 2px 6px rgba(0,0,0,0.2)',
        });

        btn.title = t.openSettings;
        btn.addEventListener('click', (e) => {
            e.stopPropagation();
            if (settingsPanel) {
                closeSettingsPanel();
            } else {
                createSettingsPanel();
            }
        });

        document.body.appendChild(btn);
    }

    function applyCustomWidth() {
        const percent = config.customChatWidthPercent;
        const maxWidth = `${percent}vw`;

        const update = () => {
            const containers = document.querySelectorAll('main div[class*="max-w-"], main .lg\\:max-w-3xl, main .xl\\:max-w-4xl');
            containers.forEach(el => {
                el.style.maxWidth = maxWidth;
                el.style.width = '100%';
            });
        };

        update();

        const main = document.querySelector('main');
        if (main) {
            const chatObserver = new MutationObserver(update);
            chatObserver.observe(main, { childList: true, subtree: true });
        }

    }

    applyCustomWidth();
    injectSettingsButton();

    function observeThemeChange(callback) {
        const observer = new MutationObserver(() => {
            callback();
        });

        observer.observe(document.body, {
            attributes: true,
            attributeFilter: ['class', 'style']
        });
    }

    function ensurePromptButtonsMounted(interval = 1000) {
        let lastEditor = null;

        setInterval(() => {
            const editor = document.querySelector('.ProseMirror');

            if (editor && editor !== lastEditor) {
                lastEditor = editor;

                const exists = document.getElementById('cgpt-prompt-buttons');
                if (!exists) {
                    renderPromptButtons();
                    forceInputBottom();
                }
            }
        }, interval);
    }

    function renderPromptButtons() {
        const editor = document.querySelector('.ProseMirror');
        if (!editor) return;

        const form = editor.closest('form');
        if (!form) return;

        let wrapper = document.getElementById('cgpt-prompt-buttons');
        if (wrapper) wrapper.remove();

        const dark = isDarkTheme();
        const bg = dark ? '#333' : '#fff';
        const color = dark ? '#fff' : '#000';
        const border = dark ? '#555' : '#aaa';

        // 注入样式(仅添加一次)
        if (!document.getElementById('cgpt-prompt-style')) {
            const style = document.createElement('style');
            style.id = 'cgpt-prompt-style';
            style.textContent = `
            #cgpt-prompt-buttons button:hover {
                border-color: #4caf50;
            }
            #cgpt-prompt-buttons button.drag-over {
                border: 2px dashed #2196f3 !important;
                background-color: rgba(33, 150, 243, 0.1) !important;
            }
        `;
            document.head.appendChild(style);
        }

        wrapper = document.createElement('div');
        wrapper.id = 'cgpt-prompt-buttons';
        Object.assign(wrapper.style, {
            display: 'flex',
            flexWrap: 'wrap',
            gap: '8px',
            padding: '4px',
            marginBottom: '8px',
            borderTop: `1px solid ${border}`,
            background: bg,
            color: color,
            zIndex: '1000',
        });

        // ➕ 添加按钮
        const addBtn = document.createElement('button');
        addBtn.textContent = t.addPrompt;
        Object.assign(addBtn.style, {
            padding: '4px 8px',
            border: `1px dashed ${border}`,
            borderRadius: '4px',
            background: 'transparent',
            color: color,
            cursor: 'pointer',
            fontSize: '12px',
        });

        addBtn.onclick = () => {
            showPromptEditor();
        };

        wrapper.appendChild(addBtn);

        let dragSrcIndex = null;

        config.prompts.forEach((p, i) => {
            const btn = document.createElement('button');
            btn.textContent = p.title;
            btn.setAttribute('draggable', 'true');
            btn.dataset.index = i;

            Object.assign(btn.style, {
                padding: '4px 8px',
                border: `1px solid ${border}`,
                borderRadius: '4px',
                background: bg,
                color: color,
                cursor: 'move',
                fontSize: '12px',
                maxWidth: '80px',
                overflow: 'hidden',
                whiteSpace: 'nowrap',
                textOverflow: 'ellipsis',
                transition: 'all 0.2s ease',
            });

            // 拖动排序
            btn.addEventListener('dragstart', (e) => {
                dragSrcIndex = Number(e.target.dataset.index);
                e.dataTransfer.effectAllowed = 'move';
                e.dataTransfer.setData('text/plain', dragSrcIndex);
                e.target.style.opacity = '0.5';
            });

            btn.addEventListener('dragover', (e) => {
                e.preventDefault();
                e.dataTransfer.dropEffect = 'move';
                btn.classList.add('drag-over');
            });

            btn.addEventListener('dragleave', () => {
                btn.classList.remove('drag-over');
            });

            btn.addEventListener('drop', (e) => {
                e.preventDefault();
                btn.classList.remove('drag-over');

                const targetIndex = Number(e.target.dataset.index);
                if (dragSrcIndex === null || dragSrcIndex === targetIndex) return;

                const moved = config.prompts[dragSrcIndex];
                config.prompts.splice(dragSrcIndex, 1);
                config.prompts.splice(targetIndex, 0, moved);

                saveConfig();
                renderPromptButtons();
            });

            btn.addEventListener('dragend', (e) => {
                e.target.style.opacity = '1';
                dragSrcIndex = null;
            });

            // 插入 prompt 内容(保留换行)
            btn.onclick = (e) => {
                e.preventDefault();
                editor.focus();

                const sel = window.getSelection();
                if (!sel || sel.rangeCount === 0) return;

                const range = sel.getRangeAt(0);
                range.deleteContents();

                const lines = p.content.split('\n');
                const fragment = document.createDocumentFragment();

                lines.forEach((line, idx) => {
                    fragment.appendChild(document.createTextNode(line));
                    if (idx < lines.length - 1) {
                        fragment.appendChild(document.createElement('br'));
                    }
                });

                range.insertNode(fragment);

                sel.removeAllRanges();
                const newRange = document.createRange();
                const lastNode = editor.lastChild;
                newRange.selectNodeContents(lastNode);
                newRange.collapse(false);
                sel.addRange(newRange);

                editor.dispatchEvent(new Event('input', { bubbles: true }));
            };

            // 编辑 / 删除
            btn.oncontextmenu = (e) => {
                e.preventDefault();
                showPromptMenu(e.pageX, e.pageY, i, p);
            };

            btn.onmouseover = () => {
                btn.style.background = dark ? '#444' : '#eee';
            };
            btn.onmouseout = () => {
                btn.style.background = bg;
            };

            wrapper.appendChild(btn);
        });

        // 👇 挂载到输入框上方
        form.insertBefore(wrapper, form.firstChild);
    }






    function forceInputBottom() {
        const editor = document.querySelector('.ProseMirror');
        if (!editor) return;

        const formWrapper = editor.closest('form')?.parentElement;
        if (formWrapper) {
            formWrapper.style.marginTop = 'auto';
        }
    }

    renderPromptButtons();
    forceInputBottom();

    observeThemeChange(() => {
        renderPromptButtons();
        forceInputBottom();
    });

    ensurePromptButtonsMounted();

    const waitInput = setInterval(() => {
        const textarea = document.querySelector('textarea');
        if (textarea) {
            renderPromptButtons();
            clearInterval(waitInput);
        }
    }, 500);

    function createPromptEditor(container, dark) {
        const hint = document.createElement('div');
        hint.textContent = t.promptTips;
        Object.assign(hint.style, {
            fontSize: '13px',
            color: dark ? '#ccc' : '#666',
            padding: '4px',
            fontStyle: 'italic',
        });

        container.appendChild(hint);
    }

    function createSettingsPanel() {
        const dark = isDarkTheme();
        const textColor = dark ? '#fff' : '#000';
        const bgColor = dark ? '#333' : '#fff';
        const borderColor = dark ? '#555' : '#ccc';

        settingsPanel = document.createElement('div');
        settingsPanel.id = 'cgpt-enhancer-settings-panel';

        settingsPanel.innerHTML = `
      <div style="
        position: fixed;
        bottom: 70px;
        right: 20px;
        background: ${bgColor};
        color: ${textColor};
        border: 1px solid ${borderColor};
        box-shadow: 0 2px 12px rgba(0,0,0,0.2);
        z-index: 10000;
        padding: 16px;
        border-radius: 8px;
        width: 320px;
        font-family: sans-serif;
      ">

        <h2 style="margin-top:0; font-size: 16px;">${t.settingsTitle}</h2>

        <div style="margin-top: 12px;">
          <label style="font-weight: bold;">${t.chatWidthLabel}<span id="widthValue">${config.customChatWidthPercent}%</span></label><br>
          <div style="display: flex; align-items: center; gap: 8px;">
            <input type="range" id="widthSlider" min="50" max="80" value="${config.customChatWidthPercent}" style="flex: 1;">
            <button id="resetWidthBtn" style="flex-shrink:0;">${t.reset}</button>
          </div>
        </div>

        <hr style="margin: 12px -8px; border: none; border-top: 1px solid ${borderColor};">

<details style="margin-top: 12px;">
  <summary style="cursor:pointer; font-weight: bold;">${t.promptDataTitle}</summary>

  <div style="margin-top: 8px; display: flex; gap: 8px; justify-content: space-between;">
    <button id="exportPromptsBtn" style="flex:1;">${t.export}</button>
    <button id="importPromptsBtn" style="flex:1;">${t.import}</button>
  </div>

  <div style="margin-top: 16px;">
    <label style="font-weight:bold;">${t.gistId}</label>
    <input id="gistIdInput" style="width:100%;margin-top:4px;padding:4px;" placeholder="${t.gistIdPlaceholder}">

    <label style="font-weight:bold;margin-top:8px;">${t.gistToken}</label>
    <input type="password" id="gistTokenInput" style="width:100%;margin-top:4px;padding:4px;" placeholder="${t.gistTokenPlaceholder}">

    <div style="margin-top:8px;display:flex;gap:8px;">
      <button id="syncUpload" style="flex:1;">${t.upload}</button>
      <button id="syncDownload" style="flex:1;">${t.download}</button>
    </div>
  </div>
</details>

<div id="promptEditorContainer" style="margin-top: 12px;"></div>

      </div>
    `;

        document.body.appendChild(settingsPanel);
        document.addEventListener('click', outsideClickClose);
        settingsPanel.addEventListener('click', e => e.stopPropagation());

        const buttonStyle = {
            flex: '1',
            padding: '4px 8px',
            border: dark ? '1px solid #555' : '1px solid #ccc',
            borderRadius: '4px',
            background: dark ? '#444' : '#f9f9f9',
            color: dark ? '#fff' : '#000',
            cursor: 'pointer'
        };

        ['exportPromptsBtn', 'importPromptsBtn', 'syncUpload', 'syncDownload'].forEach(id => {
            const btn = document.getElementById(id);
            if (btn) Object.assign(btn.style, buttonStyle);
        });

        document.getElementById('exportPromptsBtn').addEventListener('click', exportPrompts);
        document.getElementById('importPromptsBtn').addEventListener('click', importPrompts);

        const slider = document.getElementById('widthSlider');
        const widthLabel = document.getElementById('widthValue');
        slider.addEventListener('input', (e) => {
            config.customChatWidthPercent = parseInt(e.target.value);
            widthLabel.textContent = config.customChatWidthPercent + '%';
            saveConfig();
            applyCustomWidth();
        });

        document.getElementById('resetWidthBtn').addEventListener('click', () => {
            config.customChatWidthPercent = defaultConfig.customChatWidthPercent;
            saveConfig();
            slider.value = config.customChatWidthPercent;
            widthLabel.textContent = config.customChatWidthPercent + '%';
            applyCustomWidth();
        });

        document.getElementById('gistIdInput').value = config.gistId || '';
        document.getElementById('gistTokenInput').value = config.gistToken || '';

        const tokenInput = document.getElementById('gistTokenInput');
        Object.assign(tokenInput.style, {
            background: dark ? '#444' : '#fff',
            color: dark ? '#fff' : '#000',
            border: '1px solid #888',
            borderRadius: '4px',
        });

        document.getElementById('syncUpload').addEventListener('click', () => {
            const gistId = document.getElementById('gistIdInput').value.trim();
            const token = document.getElementById('gistTokenInput').value.trim();

            if (!gistId) return alert(t.gistIdPlaceholder);

            config.gistId = gistId;
            config.gistToken = token;
            saveConfig();

            uploadPromptsToGist(gistId, token);
        });

        document.getElementById('syncDownload').addEventListener('click', () => {
            const gistId = document.getElementById('gistIdInput').value.trim();
            const token = document.getElementById('gistTokenInput').value.trim();

            if (!gistId) return alert(t.gistIdPlaceholder);

            config.gistId = gistId;
            config.gistToken = token;
            saveConfig();

            fetchPromptsFromGist(gistId, token);
        });



        const container = document.getElementById('promptEditorContainer');
        createPromptEditor(container, dark);
    }

    function closeSettingsPanel() {
        if (settingsPanel) {
            settingsPanel.remove();
            settingsPanel = null;
        }
        document.removeEventListener('click', outsideClickClose);
    }

    function outsideClickClose() {
        closeSettingsPanel();
    }

    function showPromptMenu(x, y, index, prompt) {
        const existing = document.getElementById('cgpt-prompt-context-menu');
        if (existing) existing.remove();

        const dark = isDarkTheme();
        const menu = document.createElement('div');
        menu.id = 'cgpt-prompt-context-menu';

        Object.assign(menu.style, {
            position: 'absolute',
            top: `${y}px`,
            left: `${x}px`,
            background: dark ? '#444' : '#fff',
            color: dark ? '#fff' : '#000',
            border: '1px solid #888',
            borderRadius: '4px',
            boxShadow: '0 2px 6px rgba(0,0,0,0.2)',
            zIndex: 10000,
        });

        const entries = [
            { text: t.editPrompt, action: () => showPromptEditor(index, prompt) },
            { text: t.deletePrompt, action: () => {
                if (confirm(t.deleteConfirm(prompt.title))) {
                    config.prompts.splice(index, 1);
                    saveConfig();
                    renderPromptButtons();
                }
            }},
            { text: t.promptBulkDeleteTitle, action: () => showBulkDeleteDialog() },
        ];

        entries.forEach(({ text, action }) => {
            const item = document.createElement('div');
            item.textContent = text;
            Object.assign(item.style, {
                padding: '6px 12px',
                cursor: 'pointer',
            });
            item.onmouseover = () => {
                item.style.background = dark ? '#555' : '#eee';
            };
            item.onmouseout = () => {
                item.style.background = 'inherit';
            };

            item.onclick = () => {
                menu.remove();
                action();
            };
            menu.appendChild(item);
        });

        document.body.appendChild(menu);
        setTimeout(() => {
            document.addEventListener('click', () => menu.remove(), { once: true });
        }, 0);
    }

    function showBulkDeleteDialog() {
        const existing = document.getElementById('cgpt-bulk-delete-dialog');
        if (existing) existing.remove();

        const dark = isDarkTheme();
        const popup = document.createElement('div');
        popup.id = 'cgpt-bulk-delete-dialog';

        Object.assign(popup.style, {
            position: 'fixed',
            top: '50%',
            left: '50%',
            transform: 'translate(-50%, -50%)',
            background: dark ? '#333' : '#fff',
            color: dark ? '#fff' : '#000',
            border: '1px solid #888',
            borderRadius: '8px',
            padding: '16px',
            zIndex: 10000,
            boxShadow: '0 2px 12px rgba(0,0,0,0.3)',
            width: '300px',
            maxHeight: '60vh',
            overflowY: 'auto',
            fontFamily: 'sans-serif'
        });

        const title = document.createElement('div');
        title.textContent = t.promptBulkDeleteTitle;
        Object.assign(title.style, {
            fontWeight: 'bold',
            fontSize: '16px',
            marginBottom: '12px',
            textAlign: 'center'
        });

        popup.appendChild(title);

        const checkboxes = [];

        config.prompts.forEach((p, idx) => {
            const row = document.createElement('div');
            row.style.marginBottom = '6px';

            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.dataset.index = idx;

            const label = document.createElement('label');
            label.textContent = ` ${p.title}`;
            label.style.cursor = 'pointer';

            row.appendChild(checkbox);
            row.appendChild(label);
            popup.appendChild(row);
            checkboxes.push(checkbox);
        });

        const btnRow = document.createElement('div');
        Object.assign(btnRow.style, {
            marginTop: '12px',
            display: 'flex',
            gap: '8px',
        });

        const cancelBtn = document.createElement('button');
        cancelBtn.textContent = t.cancel;
        Object.assign(cancelBtn.style, {
            flex: '1',
            padding: '6px',
            borderRadius: '4px',
            border: 'none',
            background: '#888',
            color: '#fff',
            cursor: 'pointer',
        });
        cancelBtn.onclick = () => popup.remove();

        const deleteBtn = document.createElement('button');
        deleteBtn.textContent = t.promptBulkDeleteButton;
        Object.assign(deleteBtn.style, {
            flex: '1',
            padding: '6px',
            borderRadius: '4px',
            border: 'none',
            background: '#d32f2f',
            color: '#fff',
            cursor: 'pointer',
        });
        deleteBtn.onclick = () => {
            const toDelete = checkboxes
            .map((cb, i) => cb.checked ? i : -1)
            .filter(i => i >= 0);

            if (toDelete.length === 0) {
                alert(t.promptBulkDeleteNone); // ✅ 使用国际化提示
                return;
            }

            if (!confirm(t.promptBulkDeleteConfirm(toDelete.length))) return;

            // 倒序删除
            toDelete.reverse().forEach(i => config.prompts.splice(i, 1));

            saveConfig();
            renderPromptButtons();
            popup.remove();
        };

        btnRow.appendChild(cancelBtn);
        btnRow.appendChild(deleteBtn);
        popup.appendChild(btnRow);

        document.body.appendChild(popup);
    }

    function showPromptEditor(index, prompt = { title: '', content: '' }) {
        const existing = document.getElementById('cgpt-prompt-editor-popup');
        if (existing) existing.remove();

        const dark = isDarkTheme();
        const popup = document.createElement('div');
        popup.id = 'cgpt-prompt-editor-popup';

        Object.assign(popup.style, {
            position: 'fixed',
            top: '50%',
            left: '50%',
            transform: 'translate(-50%, -50%)',
            background: dark ? '#333' : '#fff',
            color: dark ? '#fff' : '#000',
            border: '1px solid #888',
            borderRadius: '8px',
            padding: '0',
            zIndex: 10000,
            boxShadow: '0 2px 12px rgba(0,0,0,0.3)',
            width: '320px',
            minHeight: '200px',
            overflow: 'hidden',
            fontFamily: 'sans-serif'
        });

        // ========== 🟡 拖动条 ==========
        const header = document.createElement('div');
        header.textContent = index !== undefined ? t.editPrompt : t.addPrompt;
        Object.assign(header.style, {
            padding: '10px',
            cursor: 'move',
            fontWeight: 'bold',
            background: dark ? '#444' : '#f0f0f0',
            borderBottom: '1px solid #888',
        });

        popup.appendChild(header);

        // 拖动逻辑
        let isDragging = false, startX, startY;

        header.addEventListener('mousedown', (e) => {
            isDragging = true;
            startX = e.clientX;
            startY = e.clientY;
            const rect = popup.getBoundingClientRect();
            const offsetX = startX - rect.left;
            const offsetY = startY - rect.top;

            const onMouseMove = (e) => {
                if (!isDragging) return;
                const x = e.clientX - offsetX;
                const y = e.clientY - offsetY;
                Object.assign(popup.style, {
                    left: `${x}px`,
                    top: `${y}px`,
                    transform: 'none'
                });
            };

            const onMouseUp = () => {
                isDragging = false;
                document.removeEventListener('mousemove', onMouseMove);
                document.removeEventListener('mouseup', onMouseUp);
            };

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

        // ========== 🔴 错误提示区 ==========
        const errorText = document.createElement('div');
        Object.assign(errorText.style, {
            color: 'red',
            fontSize: '13px',
            textAlign: 'center',
            margin: '8px 0 12px',
            minHeight: '18px',
        });

        const contentWrap = document.createElement('div');
        Object.assign(contentWrap.style, {
            padding: '12px',
        });

        const title = document.createElement('input');
        title.value = prompt.title || '';
        title.maxLength = 10;
        title.placeholder = t.titlePlaceholder;
        Object.assign(title.style, {
            width: '100%',
            marginBottom: '8px',
            padding: '6px',
            border: '1px solid #888',
            borderRadius: '4px',
            background: dark ? '#444' : '#fff',
            color: dark ? '#fff' : '#000',
        });

        const content = document.createElement('textarea');
        content.value = prompt.content || '';
        content.maxLength = 1000;
        content.rows = 4;
        content.placeholder = t.contentPlaceholder;
        Object.assign(content.style, {
            width: '100%',
            marginBottom: '8px',
            padding: '6px',
            border: '1px solid #888',
            borderRadius: '4px',
            background: dark ? '#444' : '#fff',
            color: dark ? '#fff' : '#000',
        });

        const btnRow = document.createElement('div');
        Object.assign(btnRow.style, {
            display: 'flex',
            justifyContent: 'space-between',
            gap: '8px',
        });

        const saveBtn = document.createElement('button');
        saveBtn.textContent = t.save;
        Object.assign(saveBtn.style, {
            flex: '1',
            padding: '6px',
            border: 'none',
            borderRadius: '4px',
            background: '#4caf50',
            color: '#fff',
            cursor: 'pointer'
        });

        const cancelBtn = document.createElement('button');
        cancelBtn.textContent = t.cancel;
        Object.assign(cancelBtn.style, {
            flex: '1',
            padding: '6px',
            border: 'none',
            borderRadius: '4px',
            background: '#888',
            color: '#fff',
            cursor: 'pointer'
        });

        const closePopup = () => {
            popup.remove();
            document.removeEventListener('keydown', keyHandler);
        };

        cancelBtn.onclick = closePopup;

        saveBtn.onclick = () => {
            const newTitle = title.value.trim();
            const newContent = content.value.trim();

            if (!newTitle || !newContent) {
                errorText.textContent = t.titleEmpty;
                return;
            }
            if (newTitle.length > 10 || newContent.length > 1000) {
                errorText.textContent = t.lengthExceeded;
                return;
            }

            const titleExists = config.prompts.some((p, idx) =>
                                                    p.title === newTitle && idx !== index
                                                   );
            if (titleExists) {
                errorText.textContent = t.duplicateTitle;
                return;
            }

            errorText.textContent = ''; // 清除错误提示

            if (typeof index === 'number') {
                config.prompts[index] = { title: newTitle, content: newContent };
            } else {
                config.prompts.push({ title: newTitle, content: newContent });
            }

            saveConfig();
            renderPromptButtons();
            closePopup();
        };

        const keyHandler = (e) => {
            if (e.key === 'Escape') {
                e.preventDefault();
                closePopup();
            } else if (e.key === 'Enter' && !e.shiftKey && document.activeElement === content) {
                e.preventDefault();
                saveBtn.click();
            }
        };

        document.addEventListener('keydown', keyHandler);

        btnRow.appendChild(cancelBtn);
        btnRow.appendChild(saveBtn);

        contentWrap.appendChild(title);
        contentWrap.appendChild(content);
        contentWrap.appendChild(errorText);
        contentWrap.appendChild(btnRow);

        popup.appendChild(contentWrap);
        document.body.appendChild(popup);
    }




})();