ChatGPT Ctrl+Enter to Send

Press Ctrl+Enter or Cmd+Enter to send a message on the ChatGPT webpage. Pressing Enter alone will only insert a line break.

// ==UserScript==
// @name         ChatGPT Ctrl+Enter 以發送
// @name:en      ChatGPT Ctrl+Enter to Send
// @name:zh-CN   ChatGPT Ctrl+Enter 以发送
// @name:ja      ChatGPT Ctrl+Enter で送信
// @namespace    http://tampermonkey.net/
// @version      9.8.1
// @description  在 ChatGPT 網頁按下 Ctrl+Enter 或 Cmd+Enter 發送訊息,單獨按 Enter 只換行
// @description:en  Press Ctrl+Enter or Cmd+Enter to send a message on the ChatGPT webpage. Pressing Enter alone will only insert a line break.
// @description:zh-cn 在 ChatGPT 网页按下 Ctrl+Enter 或 Cmd+Enter 发送消息,单独按 Enter 只换行
// @description:ja ChatGPTのウェブページでCtrl+EnterまたはCmd+Enterを押してメッセージを送信します。Enterキーだけを押すと改行されます
// @author       SoizoKtantas & ChatGPT
// @match        https://chatgpt.com/*
// @icon         https://www.google.com/s2/favicons?domain=openai.com
// @license      Apache License 2.0
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @require      https://update.greasyfork.org/scripts/411512/864854/GM_createMenu.js
// ==/UserScript==

(function () {
    "use strict";

    // 定義語言
    const languages = {
        "zh-TW": {
            enterEnabled: "Enter 換行啓用中",
            enterDisabled: "Enter 換行停用中",
            ctrlEnterEnabled: "Ctrl+Enter 啓用中",
            ctrlEnterDisabled: "Ctrl+Enter 停用中",
            enterEnabledAlert: "Enter 換行已啓用",
            enterDisabledAlert: "Enter 換行已停用",
            ctrlEnterEnabledAlert: "Ctrl+Enter 發送已啓用",
            ctrlEnterDisabledAlert: "Ctrl+Enter 發送已停用",
        },
        "zh-CN": {
            enterEnabled: "Enter 换行启用中",
            enterDisabled: "Enter 换行停用中",
            ctrlEnterEnabled: "Ctrl+Enter 启用中",
            ctrlEnterDisabled: "Ctrl+Enter 停用中",
            enterEnabledAlert: "Enter 换行已启用",
            enterDisabledAlert: "Enter 换行已停用",
            ctrlEnterEnabledAlert: "Ctrl+Enter 发送已启用",
            ctrlEnterDisabledAlert: "Ctrl+Enter 发送已停用",
        },
        en: {
            enterEnabled: "Enter for newline enabled",
            enterDisabled: "Enter for newline disabled",
            ctrlEnterEnabled: "Ctrl+Enter to send enabled",
            ctrlEnterDisabled: "Ctrl+Enter to send disabled",
            enterEnabledAlert: "Enter for newline enabled",
            enterDisabledAlert: "Enter for newline disabled",
            ctrlEnterEnabledAlert: "Ctrl+Enter to send enabled",
            ctrlEnterDisabledAlert: "Ctrl+Enter to send disabled",
        },
        ja: {
            enterEnabled: "Enter 改行が有効",
            enterDisabled: "Enter 改行が無効",
            ctrlEnterEnabled: "Ctrl+Enter 送信が有効",
            ctrlEnterDisabled: "Ctrl+Enter 送信が無効",
            enterEnabledAlert: "Enter 改行が有効になりました",
            enterDisabledAlert: "Enter 改行が無効になりました",
            ctrlEnterEnabledAlert: "Ctrl+Enter 送信が有効になりました",
            ctrlEnterDisabledAlert: "Ctrl+Enter 送信が無効になりました",
        },
    };

    // 獲取語言
    const userLang = navigator.language || navigator.userLanguage;
    const lang = languages[userLang] || languages["zh-TW"];

    // 初始化開關狀態
    let isEnterEnabled = GM_getValue("isEnterEnabled", true);
    let isCtrlEnterEnabled = GM_getValue("isCtrlEnterEnabled", true);

    // 定義開關菜單
    GM_createMenu.add([
        {
            on: {
                name: lang.enterEnabled,
                callback: function () {
                    isEnterEnabled = true;
                    GM_setValue("isEnterEnabled", true);
                    alert(lang.enterEnabledAlert);
                },
            },
            off: {
                name: lang.enterDisabled,
                callback: function () {
                    isEnterEnabled = false;
                    GM_setValue("isEnterEnabled", false);
                    alert(lang.enterDisabledAlert);
                },
            },
            load: function (menuStatus) {
                if (menuStatus === "on") {
                    isEnterEnabled = true;
                } else {
                    isEnterEnabled = false;
                }
            },
            default: isEnterEnabled,
        },
        {
            on: {
                name: lang.ctrlEnterEnabled,
                callback: function () {
                    isCtrlEnterEnabled = true;
                    GM_setValue("isCtrlEnterEnabled", true);
                },
            },
            off: {
                name: lang.ctrlEnterDisabled,
                callback: function () {
                    isCtrlEnterEnabled = false;
                    GM_setValue("isCtrlEnterEnabled", false);
                },
            },
            load: function (menuStatus) {
                if (menuStatus === "on") {
                    isCtrlEnterEnabled = true;
                } else {
                    isCtrlEnterEnabled = false;
                }
            },
            default: isCtrlEnterEnabled,
        },
    ]);
    GM_createMenu.create({ storage: true });

    // 添加事件監聽器到文檔,使用 capture 階段
    document.addEventListener(
        "keydown",
        function (e) {
            // 獲取焦點元素
            const activeElement = document.activeElement;

            // 檢查焦點是否在 #prompt-textarea 上
            if (activeElement && activeElement.id === "prompt-textarea") {
                if (e.key === "Enter") {
                    if (!isCtrlEnterEnabled && (e.ctrlKey || e.metaKey)) {
                        e.preventDefault(); // 防止默認行為
                        e.stopImmediatePropagation(); // 阻止其他事件處理器
                        // 查找發送按鈕
                        const sendButton = activeElement
                            // .closest("#prompt-textarea")
                            .parentElement.parentElement.querySelector(
                                "button[data-testid='send-button']"
                            );

                        if (sendButton) {
                            // 模擬點擊發送按鈕
                            sendButton.click();
                        }
                    }
                    if (!isEnterEnabled) {
                        e.preventDefault(); // 防止默認行為
                        e.stopImmediatePropagation(); // 阻止其他事件處理器
                        // 插入換行
                        const textarea = activeElement;
                        const start = textarea.selectionStart;
                        const end = textarea.selectionEnd;
                        textarea.value =
                            textarea.value.substring(0, start) +
                            "\n" +
                            textarea.value.substring(end);
                        // 將光標位置調整到換行後
                        textarea.selectionStart = textarea.selectionEnd =
                            start + 1;

                        if (textarea.selectionStart === textarea.value.length) {
                            // 滑动到最底部
                            textarea.scrollTop = textarea.scrollHeight;
                        }
                        // 手動觸發 input 事件
                        const event = new Event("input", { bubbles: true });
                        textarea.dispatchEvent(event);
                    }
                }
            } else {
                // 檢查其他輸入框
                if (e.key === "Enter") {
                    if (!isCtrlEnterEnabled && (e.ctrlKey || e.metaKey)) {
                        const parent = activeElement.closest(
                            "div.group\\/conversation-turn"
                        );
                        if (parent) {
                            // console.log(parent)
                            e.preventDefault(); // 防止默認行為
                            e.stopImmediatePropagation(); // 阻止其他事件處理器

                            const sendButton = parent.querySelector(
                                "div > div > div > div:last-of-type > button:last-of-type"
                            );
                            if (sendButton) {
                                // console.log(sendButton);
                                sendButton.click();
                            }
                        }
                    }
                }
            }
        },
        true
    ); // 使用 capture 階段
})();