// ==UserScript==
// @name Telegram 输入框翻译并发送 (v2.1 - 支持回车和按钮)
// @namespace http://tampermonkey.net/
// @version 2.1
// @description 按回车或点击发送按钮时,翻译输入框内容(中/缅->指定风格英文)并自动替换和发送。拦截中文/缅甸语原文发送。
// @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"; // 输入框翻译模型
const TRANSLATION_PROMPT = `Ur a pro translator, & u must strictly follow these rules, understood? I’ll send Chinese, & u will translate to US-style English with abbreviations, no period at the end. Keep the full meaning & question marks for questions. Use fluent and sophisticated English to ensure the expression is polished, articulate, and of a high linguistic standard.Only use letter-based abbreviations like "u" for "you," "ur" for "your," "r" for "are," "thx" for "thanks," & but ensure the language still reflects a high English level with polished & sophisticated word choices, got it? Use & for And. U r absolutely forbidden from using numbers in abbreviations—no "2" for "to," no "4" for "for," no "b4" for "before," or any number ever! Only use letters: "to," "for," "bfr" for "before," "frst" for "first," "tmrw" for "tomorrow," "nxt" for "next." U must double-check ur output to ensure no numbers like "2" or "4" appear in abbreviations, replacing them with "to" or "for," u can't use number abbreviations in English words, clear?
Chinese text to translate:
{text_to_translate}`;
// Selectors
const INPUT_SELECTOR = 'div.input-message-input[contenteditable="true"]';
const SEND_BUTTON_SELECTOR = 'button.btn-send'; // 请根据需要检查并调整此选择器
// Input Translation Overlay (For status/error feedback)
const INPUT_OVERLAY_ID = 'custom-input-translate-overlay';
// Language Detection Regex
const CHINESE_REGEX = /[\u3400-\u4DBF\u4E00-\u9FFF\uF900-\uFAFF]/;
const BURMESE_REGEX = /[\u1000-\u109F]/;
// State Variables
let inputTranslationOverlayElement = null;
let currentInputApiXhr = null;
let isTranslatingAndSending = false; // Flag to prevent conflicts/loops
let sendButtonClickListenerAttached = false; // Track if click listener is attached
// --- CSS Styles (Only for Overlay) ---
GM_addStyle(`
#${INPUT_OVERLAY_ID} { /* ... Overlay styles unchanged ... */ position: absolute; bottom: 100%; left: 10px; right: 10px; 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; }
#${INPUT_OVERLAY_ID}.visible { display: block; }
#${INPUT_OVERLAY_ID} .status { font-style: italic; color: #aaa; }
#${INPUT_OVERLAY_ID} .error { font-weight: bold; color: #ff8a8a; }
`);
// --- 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(); }
function ensureInputOverlayExists(inputMainContainer) { if (!inputMainContainer) return; if (!inputTranslationOverlayElement || !document.body.contains(inputTranslationOverlayElement)) { inputTranslationOverlayElement = document.createElement('div'); inputTranslationOverlayElement.id = INPUT_OVERLAY_ID; inputMainContainer.style.position = 'relative'; inputMainContainer.appendChild(inputTranslationOverlayElement); console.log("[InputTranslate] Overlay element created."); } }
function updateInputOverlay(content, type = 'status', duration = 0) { if (!inputTranslationOverlayElement) { const inputContainer = document.querySelector(INPUT_SELECTOR)?.closest('.chat-input-main'); ensureInputOverlayExists(inputContainer); if(!inputTranslationOverlayElement) return; } 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'); inputTranslationOverlayElement.textContent = ''; } }
// --- Shared Translate -> Replace -> Send Logic ---
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.");
return;
}
isTranslatingAndSending = true;
hideInputOverlay(); // Clear previous status
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 };
console.log(`[InputTranslate] Calling API (${INPUT_TRANSLATE_MODEL}) for translateAndSend`);
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 {
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
inputElement.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); // Trigger input event
// Use a small delay before clicking send
setTimeout(() => {
console.log("[InputTranslate] Programmatically clicking send button.");
sendButton.click();
hideInputOverlay(); // Clear "翻译中..."
isTranslatingAndSending = false; // Reset flag *after* initiating send
}, 150); // Increased delay slightly
} else {
let errorMsg = data.error?.message || "API返回空内容";
throw new Error(errorMsg); // Treat as error
}
} catch (e) {
console.error("[InputTranslate] API/Parse Error:", e);
updateInputOverlay(`翻译失败: ${e.message.substring(0, 50)}`, 'error', 4000);
isTranslatingAndSending = false; // Reset flag on error
}
},
onerror: function(response) { /* ... error handling ... */ currentInputApiXhr = null; console.error("[InputTranslate] Request Error:", response); updateInputOverlay(`翻译失败: 网络错误 (${response.status})`, 'error', 4000); isTranslatingAndSending = false; },
ontimeout: function() { /* ... error handling ... */ currentInputApiXhr = null; console.error("[InputTranslate] Timeout"); updateInputOverlay("翻译失败: 请求超时", 'error', 4000); isTranslatingAndSending = false; },
onabort: function() { /* ... error handling ... */ currentInputApiXhr = null; console.log("[InputTranslate] API request aborted."); /* Don't reset flag here */ },
timeout: 30000
});
}
// --- Event Listeners ---
function handleInputKeyDown(event) {
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 inputElement = event.target;
const text = inputElement.textContent?.trim() || "";
const detectedLang = detectLanguage(text);
if (text && (detectedLang === 'Chinese' || detectedLang === 'Burmese')) {
console.log(`[InputTranslate][Enter] Detected ${detectedLang}. Translating & sending...`);
event.preventDefault(); // <<< PREVENT default Enter action (sending original)
event.stopPropagation();
const sendButton = document.querySelector(SEND_BUTTON_SELECTOR);
if (!sendButton) {
updateInputOverlay("错误: 未找到发送按钮!", 'error', 5000);
return;
}
translateAndSend(text, inputElement, sendButton); // Use the shared function
} else {
console.log(`[InputTranslate][Enter] Allowing normal send for ${detectedLang || 'empty'}.`);
hideInputOverlay();
// Allow default action (send original text or do nothing if empty)
}
} else {
if (!['Shift', 'Control', 'Alt', 'Meta', 'Enter'].includes(event.key)) {
hideInputOverlay();
if (currentInputApiXhr && typeof currentInputApiXhr.abort === 'function') {
currentInputApiXhr.abort();
isTranslatingAndSending = false; // Reset if user types other keys during processing
}
}
}
}
function handleSendButtonClick(event) {
// Check if the click target *is* the send button we are tracking
const sendButton = event.target.closest(SEND_BUTTON_SELECTOR);
if (!sendButton) {
return; // Click was not on the send button or its child
}
if (isTranslatingAndSending) {
console.log("[InputTranslate][Click] Ignored, already processing.");
// Important: Don't prevent default here, allow the *second*, programmatic click to go through
return;
}
const inputElement = document.querySelector(INPUT_SELECTOR);
if (!inputElement) {
console.error("[InputTranslate][Click] Input element not found.");
return; // Allow default click action? Or prevent? Better allow.
}
const text = inputElement.textContent?.trim() || "";
const detectedLang = detectLanguage(text);
if (text && (detectedLang === 'Chinese' || detectedLang === 'Burmese')) {
console.log(`[InputTranslate][Click] Detected ${detectedLang}. Translating & sending...`);
event.preventDefault(); // <<< PREVENT the *first* default click action (sending original)
event.stopPropagation();
translateAndSend(text, inputElement, sendButton); // Use the shared function
} else {
console.log(`[InputTranslate][Click] Allowing normal send for ${detectedLang || 'empty'}.`);
hideInputOverlay();
// Allow default click action (send original text or do nothing if empty)
}
}
function handleInputBlur(event) {
// Optional: More advanced blur handling if needed
// setTimeout(() => { /* ... Check activeElement ... */ }, 200);
}
// --- Initialization & Attaching Listeners ---
function initialize() {
console.log("[Telegram Input Translator v2.1] Initializing...");
const inputElement = document.querySelector(INPUT_SELECTOR);
// Attach listener to input field for Enter key
if (inputElement && !inputElement.dataset.customInputTranslateListener) {
console.log("[Telegram Input Translator] Attaching Keydown listener to input field.");
inputElement.addEventListener('keydown', handleInputKeyDown, true); // Use capture
// inputElement.addEventListener('blur', handleInputBlur); // Blur listener might be less critical now
inputElement.dataset.customInputTranslateListener = 'true';
const inputContainer = inputElement.closest('.chat-input-main');
ensureInputOverlayExists(inputContainer);
} else if (!inputElement) {
console.log("[Telegram Input Translator] Input field not found yet, retrying init...");
setTimeout(initialize, 1500); // Retry initialization
return;
}
// Attach listener to Send Button (using periodic check for simplicity)
// A MutationObserver would be more efficient but adds complexity
if (!sendButtonClickListenerAttached) {
setInterval(() => {
const sendButton = document.querySelector(SEND_BUTTON_SELECTOR);
// Check if button exists and listener is not already attached (using a data attribute)
if (sendButton && !sendButton.dataset.customSendClickListener) {
console.log("[Telegram Input Translator] Attaching Click listener to Send button.");
// Use capture phase for the click listener too, to intercept early
sendButton.addEventListener('click', handleSendButtonClick, true);
sendButton.dataset.customSendClickListener = 'true';
sendButtonClickListenerAttached = true; // Mark as attached globally (though interval continues)
} else if (!sendButton && sendButtonClickListenerAttached) {
// If button disappears, reset flag so we re-attach if it reappears
console.log("[Telegram Input Translator] Send button lost, listener flag reset.");
sendButtonClickListenerAttached = false;
}
}, 1000); // Check every second
}
console.log("[Telegram Input Translator v2.1] Initialization complete. Ready.");
}
// Wait for the UI
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => setTimeout(initialize, 2000));
} else {
setTimeout(initialize, 2000);
}
})();