// ==UserScript==
// @name Telegram 输入框翻译并发送 (v2.5 - v2.2基础 + 自动发送开关)
// @namespace http://tampermonkey.net/
// @version 2.5
// @description 基于v2.2自动发送逻辑,增加自动发送开关。翻译输入框内容(中/缅->指定风格英文)并替换。按回车/按钮触发。
// @author Your Name / AI Assistant
// @match https://web.telegram.org/k/*
// @match https://web.telegram.org/a/*
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @connect api.ohmygpt.com
// @icon https://upload.wikimedia.org/wikipedia/commons/thumb/8/82/Telegram_logo.svg/48px-Telegram_logo.svg.png
// ==/UserScript==
(function() {
'use strict';
// --- Configuration ---
const OHMYGPT_API_KEY = "sk-RK1MU6Cg6a48fBecBBADT3BlbKFJ4C209a954d3b4428b54b"; // 你的 OhMyGPT API Key
const OHMYGPT_API_ENDPOINT = "https://api.ohmygpt.com/v1/chat/completions";
const INPUT_TRANSLATE_MODEL = "gpt-4o-mini"; // 输入框翻译模型
// --- YOUR Specific v2.2 Translation Prompt ---
const TRANSLATION_PROMPT = `
Role: You are a professional translator executing a specific task.
Task: Translate the user's Chinese or Burmese text into US-style English, adhering strictly to the output requirements below.
Strict Output Requirements:
1. **Style:** Use US-style English with common letter-based abbreviations (e.g., u, ur, r, thx, &, bfr, frst, tmrw, nxt).
2. **Sophistication:** Maintain a high English level with polished, articulate, and sophisticated word choices despite abbreviations.
3. **Meaning:** Preserve the full original meaning. Include question marks if the original is a question.
4. **Punctuation:** Do NOT end the translation with a period (.).
5. **Abbreviations:** ONLY use letter-based abbreviations. ABSOLUTELY NO number-based abbreviations (NO "2" for "to", NO "4" for "for"). Use "to", "for". Double-check your output for numbers in abbreviations.
6. **Format:** Output ONLY the translated text. NO explanations, NO notes, NO apologies, NO preliminary remarks. If translation is unnecessary (e.g., proper nouns, codes), return the original text unmodified.
Input Text to Translate:
{text_to_translate}
`; // Note: Keep the placeholder {text_to_translate}
// Selectors
const INPUT_SELECTOR = 'div.input-message-input[contenteditable="true"]';
const SEND_BUTTON_SELECTOR = 'button.btn-send';
const INPUT_AREA_CONTAINER_SELECTOR = '.chat-input-main'; // Container needed for controls
// UI Element IDs
const INPUT_OVERLAY_ID = 'custom-input-translate-overlay';
const AUTO_SEND_TOGGLE_ID = 'custom-auto-send-toggle'; // Added for toggle button
// Language Detection Regex
const CHINESE_REGEX = /[\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]/;
const BURMESE_REGEX = /[\u1000-\u109F]/;
// State Variables
let inputTranslationOverlayElement = null;
let autoSendToggleElement = null; // Added for toggle button
let currentInputApiXhr = null;
let isTranslatingAndSending = false;
let sendButtonClickListenerAttached = false;
let autoSendEnabled = false; // Added for toggle button state
// --- CSS Styles (Overlay AND Toggle Button) ---
GM_addStyle(`
#${INPUT_OVERLAY_ID} {
position: absolute; bottom: 100%; left: 10px; right: 120px; /* Make space for toggle */
background-color: rgba(30, 30, 30, 0.9); backdrop-filter: blur(3px);
border: 1px solid rgba(255, 255, 255, 0.2); border-bottom: none;
padding: 4px 8px; font-size: 13px; color: #e0e0e0;
border-radius: 6px 6px 0 0; box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.2);
z-index: 150; display: none; max-height: 60px; overflow-y: auto;
line-height: 1.3; text-align: left; transition: opacity 0.2s ease-in-out;
}
#${INPUT_OVERLAY_ID}.visible { display: block; opacity: 1; }
#${INPUT_OVERLAY_ID} .status { font-style: italic; color: #aaa; }
#${INPUT_OVERLAY_ID} .error { font-weight: bold; color: #ff8a8a; }
#${INPUT_OVERLAY_ID} .success { font-weight: bold; color: #90ee90; } /* Added for clarity */
#${AUTO_SEND_TOGGLE_ID} {
position: absolute; bottom: 100%; right: 10px; z-index: 151;
padding: 4px 10px; font-size: 12px; font-weight: bold;
border: 1px solid rgba(255, 255, 255, 0.2); border-bottom: none;
border-radius: 6px 6px 0 0; cursor: pointer; user-select: none;
transition: background-color 0.2s ease, color 0.2s ease;
}
#${AUTO_SEND_TOGGLE_ID}.autosend-off { background-color: rgba(80, 80, 80, 0.9); color: #ccc; }
#${AUTO_SEND_TOGGLE_ID}.autosend-on { background-color: rgba(70, 130, 180, 0.95); color: #fff; }
#${AUTO_SEND_TOGGLE_ID}:hover { filter: brightness(1.1); }
`);
// --- Helper Functions ---
function detectLanguage(text) { if (!text) return null; if (CHINESE_REGEX.test(text)) return 'Chinese'; if (BURMESE_REGEX.test(text)) return 'Burmese'; return 'Other'; }
function setCursorToEnd(element) { const range = document.createRange(); const sel = window.getSelection(); range.selectNodeContents(element); range.collapse(false); sel.removeAllRanges(); sel.addRange(range); element.focus(); }
// Merged function to ensure both overlay and toggle exist
function ensureControlsExist(inputMainContainer) {
if (!inputMainContainer) return;
if (window.getComputedStyle(inputMainContainer).position === 'static') {
inputMainContainer.style.position = 'relative';
console.log("[InputTranslate] Set input container to relative positioning.");
}
// Ensure Overlay
if (!inputTranslationOverlayElement || !inputMainContainer.contains(inputTranslationOverlayElement)) {
inputTranslationOverlayElement = document.createElement('div');
inputTranslationOverlayElement.id = INPUT_OVERLAY_ID;
inputMainContainer.appendChild(inputTranslationOverlayElement);
console.log("[InputTranslate] Overlay element created.");
}
// Ensure Toggle Button
if (!autoSendToggleElement || !inputMainContainer.contains(autoSendToggleElement)) {
autoSendToggleElement = document.createElement('button');
autoSendToggleElement.id = AUTO_SEND_TOGGLE_ID;
updateAutoSendButtonVisual(); // Set initial state
autoSendToggleElement.addEventListener('click', toggleAutoSend);
inputMainContainer.appendChild(autoSendToggleElement);
console.log("[InputTranslate] Auto-send toggle button created.");
}
}
function updateInputOverlay(content, type = 'status', duration = 0) {
// Ensure controls container exists first
const inputContainer = document.querySelector(INPUT_AREA_CONTAINER_SELECTOR);
ensureControlsExist(inputContainer); // This now ensures both overlay & toggle button are ready
if (!inputTranslationOverlayElement) return; // Guard if container still not found
inputTranslationOverlayElement.innerHTML = `<span class="${type}">${content}</span>`;
inputTranslationOverlayElement.classList.add('visible');
inputTranslationOverlayElement.scrollTop = inputTranslationOverlayElement.scrollHeight;
if (duration > 0) { setTimeout(hideInputOverlay, duration); }
}
function hideInputOverlay() {
if (inputTranslationOverlayElement) {
inputTranslationOverlayElement.classList.remove('visible');
setTimeout(() => {
if (inputTranslationOverlayElement && !inputTranslationOverlayElement.classList.contains('visible')) {
inputTranslationOverlayElement.textContent = '';
}
}, 250);
}
}
// --- Auto Send Toggle Logic (Copied from v2.4) ---
function updateAutoSendButtonVisual() {
if (!autoSendToggleElement) return;
if (autoSendEnabled) {
autoSendToggleElement.textContent = "自动发送: 开";
autoSendToggleElement.className = 'autosend-on';
autoSendToggleElement.id = AUTO_SEND_TOGGLE_ID;
} else {
autoSendToggleElement.textContent = "自动发送: 关";
autoSendToggleElement.className = 'autosend-off';
autoSendToggleElement.id = AUTO_SEND_TOGGLE_ID;
}
}
function toggleAutoSend() {
autoSendEnabled = !autoSendEnabled;
console.log(`[InputTranslate] Auto Send Toggled: ${autoSendEnabled ? 'ON' : 'OFF'}`);
updateAutoSendButtonVisual();
updateInputOverlay(`自动发送已${autoSendEnabled ? '开启' : '关闭'}`, 'status', 2000);
}
// --- Shared Translate -> Replace -> Send Logic (v2.2 core + Toggle Check) ---
function translateAndSend(originalText, inputElement, sendButton) {
if (isTranslatingAndSending) {
console.warn("[InputTranslate] Already processing, ignoring translateAndSend call.");
return;
}
if (!inputElement || !sendButton) {
console.error("[InputTranslate] Input element or send button missing in translateAndSend.");
// Maybe add overlay feedback here? updateInputOverlay(...)
return;
}
isTranslatingAndSending = true;
hideInputOverlay();
updateInputOverlay("翻译中...", 'status');
const finalPrompt = TRANSLATION_PROMPT.replace('{text_to_translate}', originalText);
const requestBody = { model: INPUT_TRANSLATE_MODEL, messages: [{"role": "user", "content": finalPrompt }], temperature: 0.6 }; // Using temp from your v2.2
console.log(`[InputTranslate] Calling API (${INPUT_TRANSLATE_MODEL}) for translateAndSend (v2.2 base)`);
if (currentInputApiXhr && typeof currentInputApiXhr.abort === 'function') { currentInputApiXhr.abort(); }
currentInputApiXhr = GM_xmlhttpRequest({
method: "POST", url: OHMYGPT_API_ENDPOINT,
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${OHMYGPT_API_KEY}` },
data: JSON.stringify(requestBody),
onload: function(response) {
currentInputApiXhr = null;
try {
// Using v2.2 response handling
if (response.status >= 200 && response.status < 300) {
const data = JSON.parse(response.responseText);
const translation = data.choices?.[0]?.message?.content?.trim();
if (translation) {
console.log("[InputTranslate] Success:", translation);
inputElement.textContent = translation; // Replace content
setCursorToEnd(inputElement); // Move cursor & focus
inputElement.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); // Trigger input event
// <<< Check Auto-Send Toggle >>>
console.log(`[InputTranslate] Checking autoSendEnabled state: ${autoSendEnabled}`);
if (autoSendEnabled) {
// --- Use the exact sending logic from your v2.2 ---
const sendDelay = 150; // v2.2 delay
console.log(`[InputTranslate] Auto-sending ON. Using v2.2 logic. Setting timeout (${sendDelay}ms) for click.`);
setTimeout(() => {
// Check flag inside timeout
if (!isTranslatingAndSending) {
console.log("[InputTranslate][Timeout] Sending aborted before programmatic click (v2.2 logic check).");
return;
}
console.log("[InputTranslate][Timeout] Programmatically clicking send button (v2.2 logic).");
// Check connection before clicking (good practice from v2.2)
if (sendButton && sendButton.isConnected) {
sendButton.click();
} else {
console.warn("[InputTranslate][Timeout] Send button disappeared before programmatic click (v2.2 logic).");
}
hideInputOverlay(); // Clear "翻译中..."
isTranslatingAndSending = false; // Reset flag *after* initiating send
}, sendDelay); // Use 150ms delay
} else {
// Auto-send is OFF (Logic from v2.3/v2.4)
console.log("[InputTranslate] Auto-sending OFF. Translation replaced, awaiting manual send.");
updateInputOverlay("翻译完成 ✓ (请手动发送)", 'success', 3500);
isTranslatingAndSending = false; // Reset flag now
}
} else {
// Handle empty content error (from v2.2 improved)
let errorMsg = "API Error: No translation content found in response.";
if (data.error) { errorMsg = `API Error: ${data.error.message || JSON.stringify(data.error)}`; }
console.error("[InputTranslate]", errorMsg, "Full response:", response.responseText);
throw new Error(errorMsg);
}
} else {
// Handle non-2xx status codes (from v2.2 improved)
console.error("[InputTranslate] API Error (Status):", response.status, response.statusText, response.responseText);
let errorDetail = `HTTP ${response.status}: ${response.statusText}`;
try { const errData = JSON.parse(response.responseText); errorDetail = errData.error?.message || errorDetail; }
catch (e) { /* ignore parse error */ }
throw new Error(errorDetail);
}
} catch (e) {
console.error("[InputTranslate] API/Parse Error:", e);
updateInputOverlay(`翻译失败: ${e.message.substring(0, 80)}`, 'error', 4000);
isTranslatingAndSending = false; // Reset flag on error
}
},
onerror: function(response) { currentInputApiXhr = null; console.error("[InputTranslate] Request Error:", response); updateInputOverlay(`翻译失败: 网络错误 (${response.status || 'N/A'})`, 'error', 4000); isTranslatingAndSending = false; },
ontimeout: function() { currentInputApiXhr = null; console.error("[InputTranslate] Timeout"); updateInputOverlay("翻译失败: 请求超时", 'error', 4000); isTranslatingAndSending = false; },
onabort: function() { currentInputApiXhr = null; console.log("[InputTranslate] API request aborted."); hideInputOverlay(); isTranslatingAndSending = false; }, // Reset flag on abort
timeout: 30000 // 30 seconds
});
}
// --- Event Listeners (Using v2.2 logic, adapted slightly for clarity) ---
function handleInputKeyDown(event) {
const inputElement = event.target;
if (!inputElement || !inputElement.matches(INPUT_SELECTOR)) return;
if (event.key === 'Enter' && !event.shiftKey && !event.altKey && !event.ctrlKey) {
if (isTranslatingAndSending) {
console.log("[InputTranslate][Enter] Ignored, already processing.");
event.preventDefault(); event.stopPropagation(); return;
}
const text = inputElement.textContent?.trim() || "";
const detectedLang = detectLanguage(text);
if (text && (detectedLang === 'Chinese' || detectedLang === 'Burmese')) {
console.log(`[InputTranslate][Enter] Detected ${detectedLang}. Translating...`);
event.preventDefault(); event.stopPropagation();
const sendButton = document.querySelector(SEND_BUTTON_SELECTOR);
if (!sendButton) { updateInputOverlay("错误: 未找到发送按钮!", 'error', 5000); console.error("[InputTranslate][Enter] Send button not found!"); return; }
// Check disabled *before* calling (good practice)
if (sendButton.disabled) { updateInputOverlay("错误: 发送按钮不可用!", 'error', 5000); return; }
translateAndSend(text, inputElement, sendButton); // Call shared function
} else { console.log(`[InputTranslate][Enter] Allowing normal send for ${detectedLang || 'empty'}.`); hideInputOverlay(); }
}
// Abort logic (from v2.2)
else if (isTranslatingAndSending && !['Shift', 'Control', 'Alt', 'Meta', 'Enter'].includes(event.key)) {
hideInputOverlay();
if (currentInputApiXhr && typeof currentInputApiXhr.abort === 'function') {
console.log("[InputTranslate] User typed, aborting translation.");
currentInputApiXhr.abort(); // onabort resets flag
} else {
isTranslatingAndSending = false; // Reset flag if no XHR
}
} else if (!isTranslatingAndSending) {
// Hide overlay (error/success) if user starts typing normally
if (inputTranslationOverlayElement && inputTranslationOverlayElement.classList.contains('visible')) {
const overlayContent = inputTranslationOverlayElement.querySelector('span');
if (overlayContent && !overlayContent.classList.contains('status')) { // Keep "Translating..." status visible
hideInputOverlay();
}
}
}
}
function handleSendButtonClick(event) {
const sendButton = event.target.closest(SEND_BUTTON_SELECTOR);
if (!sendButton) return;
// Prevent default click *only if* translation is needed and *not* already processing
const inputElement = document.querySelector(INPUT_SELECTOR);
if (!inputElement) { console.error("[InputTranslate][Click] Input element not found."); return; }
const text = inputElement.textContent?.trim() || "";
const detectedLang = detectLanguage(text);
if (text && (detectedLang === 'Chinese' || detectedLang === 'Burmese')) {
if (isTranslatingAndSending) {
console.log("[InputTranslate][Click] Ignored, translation already in progress.");
event.preventDefault(); event.stopPropagation(); return;
}
console.log(`[InputTranslate][Click] Detected ${detectedLang}. Translating...`);
event.preventDefault(); event.stopPropagation(); // Prevent original send
// Check disabled *before* calling (good practice)
if (sendButton.disabled) { updateInputOverlay("错误: 发送按钮不可用!", 'error', 5000); return; }
translateAndSend(text, inputElement, sendButton); // Call shared function
} else {
// Allow normal send if language not targeted or already processing (the programmatic click)
if (!isTranslatingAndSending) {
console.log(`[InputTranslate][Click] Allowing normal send for ${detectedLang || 'empty'}.`);
hideInputOverlay();
}
}
}
// --- Initialization & Attaching Listeners (Using robust observer from v2.3/v2.4) ---
function initialize() {
console.log("[Telegram Input Translator v2.5] Initializing...");
const observer = new MutationObserver(mutations => {
let inputFound = false; let controlsContainerFound = false;
mutations.forEach(mutation => {
if (mutation.addedNodes) {
mutation.addedNodes.forEach(node => {
if (node.nodeType !== 1) return;
// Find Input
const inputElementNode = node.matches(INPUT_SELECTOR) ? node : node.querySelector(INPUT_SELECTOR);
if (inputElementNode && !inputElementNode.dataset.customInputTranslateListener) {
attachInputListeners(inputElementNode); inputFound = true;
}
// Find Container for Controls
const containerElementNode = node.matches(INPUT_AREA_CONTAINER_SELECTOR) ? node : node.querySelector(INPUT_AREA_CONTAINER_SELECTOR);
if(containerElementNode) { ensureControlsExist(containerElementNode); controlsContainerFound = true; }
});
}
});
// Fallback check for container
if (inputFound && !controlsContainerFound) { const inputContainer = document.querySelector(INPUT_AREA_CONTAINER_SELECTOR); if (inputContainer) ensureControlsExist(inputContainer); }
// Check Send Button periodically
if (!sendButtonClickListenerAttached) { const sendButton = document.querySelector(SEND_BUTTON_SELECTOR); if (sendButton && !sendButton.dataset.customSendClickListener) { attachSendButtonListener(sendButton); } }
});
observer.observe(document.body, { childList: true, subtree: true });
// Initial check
const initialInputElement = document.querySelector(INPUT_SELECTOR);
const initialContainer = document.querySelector(INPUT_AREA_CONTAINER_SELECTOR);
if (initialInputElement && !initialInputElement.dataset.customInputTranslateListener) { attachInputListeners(initialInputElement); }
if (initialContainer) { ensureControlsExist(initialContainer); } // Ensure controls exist even if input listener added later
const initialSendButton = document.querySelector(SEND_BUTTON_SELECTOR);
if(initialSendButton && !initialSendButton.dataset.customSendClickListener) { attachSendButtonListener(initialSendButton); }
console.log("[Telegram Input Translator v2.5] Observer active.");
}
function attachInputListeners(inputElement) {
if (inputElement.dataset.customInputTranslateListener) return;
console.log("[InputTranslate] Attaching Keydown listener to input:", inputElement);
inputElement.addEventListener('keydown', handleInputKeyDown, true);
inputElement.dataset.customInputTranslateListener = 'true';
const inputContainer = inputElement.closest(INPUT_AREA_CONTAINER_SELECTOR);
if (inputContainer) ensureControlsExist(inputContainer); // Ensure controls when input found
}
function attachSendButtonListener(sendButton) {
if (sendButton.dataset.customSendClickListener) return;
console.log("[InputTranslate] Attaching Click listener to Send button:", sendButton);
sendButton.addEventListener('click', handleSendButtonClick, true);
sendButton.dataset.customSendClickListener = 'true';
sendButtonClickListenerAttached = true;
// Monitor button removal
const buttonObserver = new MutationObserver(() => {
if (!sendButton.isConnected) {
console.log("[InputTranslate] Send button removed. Resetting listener flag.");
buttonObserver.disconnect();
if (sendButton.dataset.customSendClickListener) { delete sendButton.dataset.customSendClickListener; }
sendButtonClickListenerAttached = false;
}
});
if (sendButton.parentNode) { buttonObserver.observe(sendButton.parentNode, { childList: true, subtree: false }); }
else { console.warn("[InputTranslate] Send button parent node not found for observer."); }
}
// --- Start Initialization ---
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => setTimeout(initialize, 1500)); }
else { setTimeout(initialize, 1500); }
})();