// ==UserScript==
// @name Telegram 输入框翻译并发送 (v2.2 - 新翻译风格)
// @namespace http://tampermonkey.net/
// @version 2.2
// @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"; // 输入框翻译模型
// --- NEW TRANSLATION PROMPT ---
const TRANSLATION_PROMPT = `Act as a professional translator. Your task is to translate the user's text according to these rules:
1. **Target Language & Style:** Translate into authentic, standard American English.
2. **Tone:** Use a gentle, kind, and polite tone. Aim for natural, conversational warmth often associated with polite female speech, but maintain standard grammar and avoid slang or overly casual abbreviations.
3. **Fluency:** Ensure the translation sounds natural and fluent, avoiding any stiffness or "machine translation" feel.
4. **Punctuation:**
* Do NOT end sentences with a period (.).
* RETAIN the question mark (?) if the original is a question.
5. **Output:** Provide ONLY the final translated text. No explanations, introductions, or labels.
Text to translate:
{text_to_translate}`;
// Selectors
const INPUT_SELECTOR = 'div.input-message-input[contenteditable="true"]';
const SEND_BUTTON_SELECTOR = 'button.btn-send'; // Please double-check this selector in Telegram Web UI if needed
// 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} { 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'; /* Ensure container allows absolute positioning */ inputMainContainer.appendChild(inputTranslationOverlayElement); console.log("[InputTranslate] Overlay element created."); } }
function updateInputOverlay(content, type = 'status', duration = 0) { const inputContainer = document.querySelector(INPUT_SELECTOR)?.closest('.chat-input-main'); if (!inputTranslationOverlayElement && inputContainer) { ensureInputOverlayExists(inputContainer); } if(!inputTranslationOverlayElement) { console.warn("[InputTranslate] Could not find/create overlay."); 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.");
updateInputOverlay("错误: 输入框或发送按钮丢失", 'error', 4000);
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.7 }; // Slightly higher temp for potentially more natural tone variation
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 {
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
inputElement.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); // Trigger input event
// Use a small delay before clicking send
setTimeout(() => {
if (!isTranslatingAndSending) { // Check if aborted by user typing
console.log("[InputTranslate] Sending aborted before programmatic click.");
return;
}
console.log("[InputTranslate] Programmatically clicking send button.");
sendButton.click();
hideInputOverlay(); // Clear "翻译中..."
isTranslatingAndSending = false; // Reset flag *after* initiating send
}, 150); // Delay before click
} else {
let errorMsg = data.error?.message || "API返回空内容";
console.error("[InputTranslate] API Error (Empty Content):", response.responseText);
throw new Error(errorMsg);
}
} else {
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, 60)}`, 'error', 5000);
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; }, // Also reset flag on abort
timeout: 30000 // 30 seconds
});
}
// --- Event Listeners ---
function handleInputKeyDown(event) {
const inputElement = event.target;
// Handle Enter Key
if (event.key === 'Enter' && !event.shiftKey && !event.altKey && !event.ctrlKey) {
if (isTranslatingAndSending) {
console.log("[InputTranslate][Enter] Ignored, already processing.");
event.preventDefault(); // Prevent potential duplicate processing
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 & sending...`);
event.preventDefault(); // <<< PREVENT default Enter action (sending original)
event.stopPropagation();
const sendButton = document.querySelector(SEND_BUTTON_SELECTOR);
if (!sendButton || sendButton.disabled) { // Check if button exists and is enabled
updateInputOverlay("错误: 发送按钮不可用!", 'error', 5000);
// Optionally abort if button is missing/disabled
// isTranslatingAndSending = false; // Already false here
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)
}
}
// Handle other key presses (abort translation if user types more)
else if (!['Shift', 'Control', 'Alt', 'Meta', 'Enter', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
// If a translation is in progress and user types something else, abort it
if (isTranslatingAndSending && currentInputApiXhr && typeof currentInputApiXhr.abort === 'function') {
console.log("[InputTranslate] User typed, aborting translation.");
currentInputApiXhr.abort(); // Abort the API call
// updateInputOverlay("翻译已取消", 'status', 1500); // Optional feedback
// isTranslatingAndSending = false; // Flag reset is handled by onabort
} else {
// If not translating, hide any previous overlay messages on new input
hideInputOverlay();
}
}
}
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
}
// Check if this click is the *programmatic* one we trigger after translation
// If isTranslatingAndSending is true *when the handler starts*, it means this might be the FIRST click
// that INITIATED the translation. The SECOND, programmatic click happens *after* isTranslatingAndSending is set back to false (or should).
// However, due to timing, let's refine the logic:
// We only want to intercept the *first* manual click if translation is needed.
// If translateAndSend was called, it sets isTranslatingAndSending=true.
// If the programmatic click happens while that flag is still true (unlikely with delay, but possible), we should let it pass.
// Let's re-evaluate: The core issue is preventing the ORIGINAL text from sending via the FIRST click.
// The `isTranslatingAndSending` flag helps prevent *starting* a new translation while one is running.
const inputElement = document.querySelector(INPUT_SELECTOR);
if (!inputElement) {
console.error("[InputTranslate][Click] Input element not found.");
return; // Allow default click action
}
const text = inputElement.textContent?.trim() || "";
const detectedLang = detectLanguage(text);
// Check if translation is needed AND we are not *already* in the middle of processing this specific text
if (text && (detectedLang === 'Chinese' || detectedLang === 'Burmese')) {
// If we are already translating (e.g., Enter was pressed just before click), prevent default but don't start another translation
if (isTranslatingAndSending) {
console.log("[InputTranslate][Click] Intercepted, translation already in progress.");
event.preventDefault();
event.stopPropagation();
return;
}
// If not already translating, start the process and prevent the default send
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 {
// Allow normal send if language is not targeted or text is empty
if (!isTranslatingAndSending) { // Only log if not currently processing
console.log(`[InputTranslate][Click] Allowing normal send for ${detectedLang || 'empty'}.`);
hideInputOverlay();
}
// Allow default click action
}
}
// --- Initialization & Attaching Listeners ---
function initialize() {
console.log("[Telegram Input Translator v2.2] Initializing...");
let inputElement = document.querySelector(INPUT_SELECTOR);
function attachInputListeners() {
inputElement = document.querySelector(INPUT_SELECTOR);
if (inputElement && !inputElement.dataset.customInputTranslateListener) {
console.log("[Telegram Input Translator] Attaching Keydown listener to input field.");
inputElement.addEventListener('keydown', handleInputKeyDown, true); // Use capture phase
inputElement.dataset.customInputTranslateListener = 'true';
const inputContainer = inputElement.closest('.chat-input-main');
if (inputContainer) {
ensureInputOverlayExists(inputContainer);
} else {
console.warn("[InputTranslate] Could not find '.chat-input-main' container for overlay.");
// Attempt to attach overlay later if needed
}
return true; // Listener attached
}
return false; // Not attached
}
function attachSendButtonListener() {
const sendButton = document.querySelector(SEND_BUTTON_SELECTOR);
if (sendButton && !sendButton.dataset.customSendClickListener) {
console.log("[Telegram Input Translator] Attaching Click listener to Send button.");
sendButton.addEventListener('click', handleSendButtonClick, true); // Use capture phase
sendButton.dataset.customSendClickListener = 'true';
sendButtonClickListenerAttached = true; // Mark globally
return true; // Listener attached
}
if (!sendButton && sendButtonClickListenerAttached) {
console.log("[Telegram Input Translator] Send button lost, listener flag reset.");
sendButtonClickListenerAttached = false; // Reset if button disappears
}
return sendButtonClickListenerAttached; // Return current status
}
// Initial attempt
let inputReady = attachInputListeners();
let buttonReady = attachSendButtonListener();
// If not immediately found, use MutationObserver for robustness
if (!inputReady || !buttonReady) {
console.log("[Telegram Input Translator] Input or Send button not found immediately, setting up observer...");
const observer = new MutationObserver((mutationsList, observer) => {
if (!inputReady) {
inputReady = attachInputListeners();
}
if (!buttonReady) {
buttonReady = attachSendButtonListener();
}
// If both are found, no need to observe anymore (though Telegram might rebuild UI)
// Keeping observer active is safer for dynamic UIs like Telegram Web A/K
// if (inputReady && buttonReady) {
// console.log("[Telegram Input Translator] Both listeners attached via observer.");
// observer.disconnect();
// }
// Ensure overlay exists if input is ready
if(inputReady && !inputTranslationOverlayElement) {
const inputContainer = document.querySelector(INPUT_SELECTOR)?.closest('.chat-input-main');
if (inputContainer) ensureInputOverlayExists(inputContainer);
}
});
observer.observe(document.body, { childList: true, subtree: true });
console.log("[Telegram Input Translator] Observer active.");
} else {
console.log("[Telegram Input Translator] Initial listeners attached successfully.");
}
console.log("[Telegram Input Translator v2.2] Initialization sequence complete. Monitoring for elements.");
}
// Wait for the UI - use a slight delay after DOMContentLoaded or run directly if already loaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => setTimeout(initialize, 1500)); // Delay after DOM ready
} else {
setTimeout(initialize, 1500); // Delay even if already loaded
}
})();