ChatGPT Prompt Presets

Enhance ChatGPT experience by adding customizable prompt presets.

// ==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);
    }




})();

QingJ © 2025

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