DZMM Copy and Send

在输入框右侧添加蓝色"复制发送"按钮,在编辑弹窗中也添加记忆发送功能

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @license MIT
// @name         DZMM Copy and Send
// @namespace    https://www.dzmm.ai/
// @version      1.3
// @description  在输入框右侧添加蓝色"复制发送"按钮,在编辑弹窗中也添加记忆发送功能
// @author       Allex0716
// @match        https://www.dzmm.ai/chat*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    // 保存到内存(JS 变量,页面刷新后自动清空)
    let savedContent = '';
    let savedEditContent = '';

    // ========== 工具函数 ==========

    /**
     * 查找页面中真正的发送按钮
     *
     * 发送按钮(lucide-send)仅在输入框有内容时才出现,
     * 需要排除自己注入的蓝色按钮,否则会死循环。
     *
     * @returns {HTMLButtonElement|null}
     */
    function findSendButton() {
        const allSendIcons = document.querySelectorAll('.lucide-send');
        for (const icon of allSendIcons) {
            const btn = icon.closest('button');
            if (btn && !btn.classList.contains('dzmm-copy-send-btn')) {
                return btn;
            }
        }
        return null;
    }

    /**
     * 找到输入框旁边的按钮容器
     *
     * 该容器(div.flex.items-center)在页面初始状态就存在,
     * 包含 plus 按钮或 send 按钮。
     *
     * @returns {HTMLElement|null}
     */
    function findContainer() {
        const input = document.querySelector('#chat-input');
        if (!input) return null;
        const parent = input.parentElement;
        if (!parent) return null;
        return parent.querySelector(':scope > .flex.items-center');
    }

    /**
     * 获取容器内第一个按钮及其包裹层,用于克隆 DOM 结构
     *
     * @param {HTMLElement} container - 按钮容器
     * @returns {{ wrapper: HTMLElement|null, button: HTMLElement|null }}
     */
    function getFirstButtonInfo(container) {
        const firstWrapper = container.querySelector(':scope > div');
        const firstButton = firstWrapper ? firstWrapper.querySelector('button') : null;
        return { wrapper: firstWrapper, button: firstButton };
    }

    /**
     * 用原生 setter 设置 textarea 的值并触发 React 状态更新
     *
     * @param {HTMLTextAreaElement} el
     * @param {string} value
     */
    function setNativeValue(el, value) {
        const nativeSetter = Object.getOwnPropertyDescriptor(
            window.HTMLTextAreaElement.prototype,
            'value'
        ).set;
        nativeSetter.call(el, value);
        el.dispatchEvent(new Event('input', { bubbles: true }));
    }

    /**
     * 动态查找发送按钮并点击
     *
     * 发送按钮可能在 React 渲染后才出现,通过 requestAnimationFrame 轮询等待。
     *
     * @param {number} retries - 剩余重试次数
     */
    function triggerSend(retries = 10) {
        const sendBtn = findSendButton();
        if (sendBtn) {
            sendBtn.click();
            return;
        }
        if (retries > 0) {
            requestAnimationFrame(() => triggerSend(retries - 1));
        }
    }

    // ========== 聊天输入框蓝色按钮 ==========

    /**
     * 在聊天输入框右侧注入蓝色"记忆发送"按钮
     *
     * @returns {boolean} 是否注入成功(或已存在)
     */
    function injectChatButton() {
        const input = document.querySelector('#chat-input');
        const container = findContainer();
        if (!input || !container) return false;

        // 避免重复注入
        if (container.querySelector('.dzmm-copy-send-btn')) return true;

        const { wrapper, button } = getFirstButtonInfo(container);
        if (!wrapper || !button) return false;

        // 创建新按钮包裹层(克隆结构)
        const newWrapper = document.createElement('div');
        newWrapper.style.cssText = wrapper.style.cssText;
        newWrapper.style.marginRight = '6px';

        // 创建蓝色按钮,克隆 send 按钮的样式
        const newButton = document.createElement('button');
        newButton.className = 'inline-flex items-center justify-center gap-2 whitespace-nowrap text-sm font-medium ring-offset-background transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 active:opacity-70 py-2 rounded-full h-9 px-4 relative dzmm-copy-send-btn bg-blue-500 text-white hover:bg-blue-600';
        newButton.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-send size-4 shrink-0 -translate-x-px"><path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"></path><path d="m21.854 2.147-10.94 10.939"></path></svg>';

        newButton.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();

            const currentContent = input.value.trim();

            if (currentContent) {
                // 输入框有内容 → 保存并发送
                savedContent = currentContent;
                triggerSend();
            } else if (savedContent) {
                // 输入框无内容,但有保存的内容 → 填充并发送
                setNativeValue(input, savedContent);
                triggerSend();
            }
            // 无内容也无保存 → 不执行任何操作
        });

        newWrapper.appendChild(newButton);
        // 插入到容器第一个按钮之前
        container.insertBefore(newWrapper, wrapper);
        return true;
    }

    // ========== 编辑弹窗蓝色按钮 ==========

    /**
     * 在编辑弹窗中找到关键 DOM 元素
     *
     * @returns {{ saveButton: HTMLButtonElement, buttonRow: HTMLElement, textarea: HTMLTextAreaElement, dialog: HTMLElement }|null}
     */
    function findEditDialogElements() {
        const allButtons = document.querySelectorAll('button');
        for (const btn of allButtons) {
            // 跳过已经注入过的按钮
            if (btn.dataset.dzmmEditInjected) continue;
            if (btn.textContent.includes('保存并重新生成回复')) {
                // 按钮所在的容器(按钮行)
                const buttonRow = btn.parentElement;
                if (!buttonRow) continue;

                // 向上查找弹窗容器,再向内查找 textarea
                const dialog = btn.closest('[role="dialog"]')
                    || btn.closest('[data-slot="dialog-content"]')
                    || btn.closest('.fixed');
                if (!dialog) continue;

                const textarea = dialog.querySelector('textarea');
                if (!textarea) continue;

                return { saveButton: btn, buttonRow, textarea, dialog };
            }
        }
        return null;
    }

    /**
     * 在编辑弹窗的「保存并重新生成回复」按钮下方注入蓝色"记忆发送"按钮
     *
     * @returns {boolean} 是否注入成功(或弹窗未打开)
     */
    function injectEditDialogButton() {
        const elements = findEditDialogElements();
        if (!elements) return false;

        const { saveButton, buttonRow, textarea } = elements;

        // 标记已注入,避免重复处理
        saveButton.dataset.dzmmEditInjected = '1';

        // 创建纵向 flex 容器包裹保存按钮和下方按钮行
        const columnWrap = document.createElement('div');
        columnWrap.style.display = 'flex';
        columnWrap.style.flexDirection = 'column';
        columnWrap.style.flex = '1';
        columnWrap.style.minWidth = '0';

        // 创建下方按钮行(清空 + 蓝色按钮,各占 50%)
        const bottomRow = document.createElement('div');
        bottomRow.style.display = 'flex';
        bottomRow.style.gap = '6px';

        // 清空按钮
        const clearBtn = document.createElement('button');
        clearBtn.className = saveButton.className;
        clearBtn.classList.add('dzmm-copy-send-edit-btn');
        clearBtn.style.flex = '1';
        clearBtn.textContent = '清空';

        clearBtn.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            setNativeValue(textarea, '');
        });

        // 蓝色按钮,宽度一半
        const blueBtn = document.createElement('button');
        blueBtn.className = saveButton.className;
        blueBtn.classList.add('dzmm-copy-send-edit-btn');
        blueBtn.classList.remove('bg-primary', 'text-primary-foreground', 'hover:bg-primary/90');
        blueBtn.classList.add('bg-blue-500', 'text-white', 'hover:bg-blue-600');
        blueBtn.style.flex = '1';
        blueBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-send size-4 shrink-0 -translate-x-px"><path d="M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z"></path><path d="m21.854 2.147-10.94 10.939"></path></svg>';

        blueBtn.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();

            const currentContent = textarea.value.trim();

            if (currentContent) {
                // 弹窗有内容 → 保存并点击「保存并重新生成回复」
                savedEditContent = currentContent;
                saveButton.click();
            } else if (savedEditContent) {
                // 弹窗无内容,但有保存的内容 → 填充并点击
                setNativeValue(textarea, savedEditContent);
                requestAnimationFrame(() => {
                    saveButton.click();
                });
            }
            // 无内容也无保存 → 不执行任何操作
        });

        // 组装:保存按钮在上,清空+蓝色按钮在下
        bottomRow.appendChild(clearBtn);
        bottomRow.appendChild(blueBtn);

        buttonRow.insertBefore(columnWrap, saveButton);
        columnWrap.appendChild(saveButton);
        columnWrap.appendChild(bottomRow);

        return true;
    }

    // ========== 初始化 ==========

    function init() {
        injectChatButton();
        injectEditDialogButton();

        // 聊天按钮:页面元素未加载则重试
        if (!document.querySelector('.dzmm-copy-send-btn')) {
            setTimeout(init, 500);
        }
    }

    // 监听 DOM 变化:SPA 页面切换后按钮消失 + 弹窗出现
    const observer = new MutationObserver(() => {
        if (!document.querySelector('.dzmm-copy-send-btn')) {
            injectChatButton();
        }
        injectEditDialogButton();
    });

    // 页面加载后启动
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

    // 等页面稳定后开始观察 DOM 变化
    setTimeout(() => {
        observer.observe(document.body, { childList: true, subtree: true });
    }, 2000);
})();