在输入框右侧添加蓝色"复制发送"按钮,在编辑弹窗中也添加记忆发送功能
// ==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);
})();