(() => { 'use strict'; let lastTweetText = ''; let lastSelectedText = ''; let sidebarHost = null; let isTyping = false; // ===== DATA ===== const LANGS = [ { value: 'en', label: 'English' ,selected: true }, { value: 'vi', label: 'Việt' }, { value: 'ja', label: 'Nhật' }, { value: 'ko', label: 'Han' }, { value: 'cn', label: 'TQ' } ]; const ModelAI = [ { value: 'openai', label: 'Chat GPT' }, { value: 'google', label: 'Gemini' }, { value: 'deepseek', label: 'Deep Seek' }, ] const TONE_BASE = [ { value: 'CASUAL', text: 'Giản dị, thân thiện' , selected: true }, { value: 'PROFESSIONAL', text: 'chuyên nghiệp, rõ ràng, đáng tin cậy' }, { value: 'HYPE', text: 'Hype — Hào hứng, tràn đầy năng lượng' }, { value: 'URGENT', text: 'urgent' }, { value: 'HUMOROUS', text: 'Dí dỏm, hài hước' }, { value: 'INFORMATIVE', text: 'Thông tin, chính xác' }, { value: 'EMPATHETIC', text: 'empathetic — Đồng cảm, thấu hiểu cảm xúc' }, { value: 'PROVOCATIVE', text: 'provocative — Gợi mở suy nghĩ, thách thức giả định' }, { value: 'AUTHORITATIVE', text: 'authoritative — Tự tin, uy quyền' }, { value: 'SPICY', text: 'spicy — Tự tin, hơi đối đầu, chỉ thẳng' } ]; const TONE_JA_ONLY = [ { value: 'AGGRESSIVE', text: 'aggressive — Cục súc, attack ideas mạnh' }, { value: 'PROFANE', text: 'profane — Nói tục thoải mái, raw' }, { value: 'INFLAMMATORY', text: 'inflammatory — Kích động cao' }, { value: 'SAVAGE', text: 'savage — Chửi tục OK, sass tối đa' } ]; const ANGLE_DEFAULT = [ { value: 'NATURAL', text: 'Phản ứng tự nhiên' }, { value: 'AGREE', text: 'Đồng ý' }, { value: 'CHALLENGE', text: 'Không đồng ý' }, { value: 'ADD_INFO', text: 'Thêm thông tin liên quan hữu ích' }, { value: 'FUNNY', text: 'Hóm hỉnh, hài hước nhẹ nhàng' }, { value: 'QUESTION', text: 'Đặt câu hỏi tiếp theo thông minh' }, { value: 'RELATE', text: 'Chia sẻ trải nghiệm cá nhân tương tự' }, { value: 'DEVIL_ADVOCATE', text: 'Phản biện công bằng, không thù địch' }, { value: 'EXPAND', text: 'Phân tích sâu 1 điểm' }, { value: 'VALIDATE', text: 'Khẳng định bằng chứng mạnh mẽ' }, { value: 'CTA', text: 'Kêu gọi hành động nhẹ nhàng' } ]; const ANGLE_EMPATHY = [ { value: 'WISH_RECOVERY', text: 'Chúc hồi phục' }, { value: 'TRIBUTE', text: 'Tưởng nhớ / RIP' }, { value: 'SOLIDARITY', text: 'Đồng lòng / Đứng cùng' }, { value: 'PERSONAL_SUPPORT', text: 'Hỗ trợ cá nhân' }, { value: 'SHARED_GRIEF', text: 'Cùng nỗi buồn' } ]; function getTones(lang) { if (lang === 'ja') return [...TONE_BASE, ...TONE_JA_ONLY]; return [...TONE_BASE]; } function getAngles(tone) { return tone === 'EMPATHETIC' ? [...ANGLE_DEFAULT, ...ANGLE_EMPATHY] : [...ANGLE_DEFAULT]; } // ===== A. CAPTURE TWEET & SELECTED TEXT ===== document.addEventListener('contextmenu', (e) => { const sel = window.getSelection().toString().trim(); if (sel) lastSelectedText = sel; const article = e.target.closest('article'); if (article) { const textEl = article.querySelector('[data-testid="tweetText"]') || article.querySelector('div[lang]') || article.querySelector('div[dir="auto"]'); lastTweetText = textEl ? textEl.innerText.trim() : article.innerText.trim().slice(0, 600); } }); // ===== B. LISTENER ===== chrome.runtime.onMessage.addListener((req, sender, sendResponse) => { if (req.action === 'OPEN_FORM') { if (!lastTweetText) { sendResponse({ ok: false, reason: 'no_text' }); return false; } openSidebar(lastTweetText); sendResponse({ ok: true }); return false; } if (req.action === 'OPEN_TRANSLATE_FORM') { openSidebar(''); switchTab('translate'); const s = sidebarHost?.shadowRoot; if (s) { const input = s.querySelector('#trans-input'); if (input) input.value = req.text || ''; } sendResponse({ ok: true }); return false; } if (req.action === 'SHOW_RESULT') { showResult(req.comment, req.model, req.commentTransVi, req.commentTransModel); sendResponse({ ok: true }); return false; } if (req.action === 'SHOW_ERROR') { showError(req.error); sendResponse({ ok: true }); return false; } if (req.action === 'SHOW_TRANSLATE_RESULT') { showTranslateResult(req.content, req.model); sendResponse({ ok: true }); return false; } if (req.action === 'SHOW_TRANSLATE_ERROR') { showTranslateError(req.error); sendResponse({ ok: true }); return false; } sendResponse({ ok: false, unknown: true }); return false; }); // ===== C. TYPING ENGINE ===== async function simulateHumanTyping(fullText) { let editor = document.querySelector('[data-testid="tweetTextarea_0"]') || document.querySelector('div[contenteditable="true"][role="textbox"]'); if (!editor) { alert('🔍 Không tìm thấy ô reply.\n\n👉 Hãy bấm nút "Reply" của tweet trước để ô nhập hiện ra, rồi thử lại!'); return false; } editor.focus(); editor.click(); editor.textContent = ''; const textNode = document.createTextNode(''); editor.appendChild(textNode); const sel = window.getSelection(); const range = document.createRange(); range.setStart(textNode, 0); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); for (let i = 0; i < fullText.length; i++) { const char = fullText[i]; textNode.nodeValue += char; range.setEnd(textNode, textNode.nodeValue.length); range.collapse(false); sel.removeAllRanges(); sel.addRange(range); editor.dispatchEvent(new InputEvent('input', { bubbles: true, cancelable: true, inputType: 'insertText', data: char })); let delay = 30 + Math.random() * 50; if (char === ' ') delay += 40 + Math.random() * 40; if ('.!?,'.includes(char)) delay += 120 + Math.random() * 200; await new Promise(r => setTimeout(r, delay)); } editor.dispatchEvent(new Event('change', { bubbles: true })); return true; } // ===== D. SIDEBAR UI ===== function switchTab(name) { if (!sidebarHost) return; const s = sidebarHost.shadowRoot; s.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === name)); s.querySelectorAll('.tab-content').forEach(c => c.classList.toggle('active', c.id === `tab-${name}`)); } function openSidebar(tweetText) { // ⭐ FIX: Nếu mở comment mới (có tweetText), xóa sidebar cũ để render lại từ đầu if (tweetText && sidebarHost) { removeSidebar(); } // Nếu sidebar vẫn còn (trường hợp translate hoặc đang mở), chỉ cần mở drawer if (sidebarHost) { const drawer = sidebarHost.shadowRoot.querySelector('.drawer'); if (drawer) drawer.classList.add('open'); return; } const host = document.createElement('div'); host.id = 'x-ai-sidebar-host'; Object.assign(host.style, { position: 'fixed', top: '0', left: '0', width: '0', height: '0', zIndex: '2147483647', overflow: 'visible', pointerEvents: 'none' }); document.body.appendChild(host); sidebarHost = host; const shadow = host.attachShadow({ mode: 'open' }); const style = document.createElement('style'); style.textContent = ` .fab { position: fixed; right: 24px; bottom: 24px; width: 56px; height: 56px; border-radius: 50%; background: #1d9bf0; color: #fff; font-size: 24px; display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 4px 16px rgba(0,0,0,0.25); border: none; pointer-events: auto; z-index: 1; transition: transform .15s ease; user-select: none; } .fab:hover { transform: scale(1.08); } .drawer { position: fixed; right: 0; top: 0; width: 420px; max-width: 100vw; height: 100vh; background: #fff; color: #0f1419; box-shadow: -5px 0 25px rgba(0,0,0,0.15); transform: translateX(100%); transition: transform .25s ease; display: flex; flex-direction: column; pointer-events: auto; z-index: 2; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; } .drawer.open { transform: translateX(0); } .header { padding: 14px 18px; border-bottom: 1px solid #eff3f4; display: flex; justify-content: space-between; align-items: center; font-weight: 700; font-size: 16px; } .btn-x { background: none; border: none; font-size: 20px; cursor: pointer; color: #536471; padding: 4px; line-height: 1; } .tabs { display: flex; border-bottom: 1px solid #eff3f4; } .tab-btn { flex: 1; padding: 10px; border: none; background: none; cursor: pointer; font-size: 14px; color: #536471; border-bottom: 2px solid transparent; font-weight: 600; } .tab-btn.active { color: #1d9bf0; border-bottom-color: #1d9bf0; } .tab-content { padding: 16px 18px; flex: 1; overflow-y: auto; display: none; flex-direction: column; gap: 10px; } .tab-content.active { display: flex; } .tweet-box { background: #f7f9f9; border: 1px solid #cfd9de; border-radius: 10px; padding: 10px; font-size: 16px; line-height: 1.4; max-height: 150px; overflow-y: auto; color: #333; white-space: pre-wrap; word-break: break-word; } label { font-size: 12px; font-weight: 700; color: #536471; text-transform: uppercase; letter-spacing: .3px; margin-top: 4px; } select, input[type="text"], input[type="password"], textarea { width: 100%; padding: 9px 10px; border-radius: 8px; border: 1px solid #cfd9de; font-size: 14px; background: #fff; box-sizing: border-box; font-family: inherit; } textarea { resize: vertical; min-height: 80px; } .hint-text { font-size: 12px; color: #888; line-height: 1.4; font-style: italic; } button.primary { width: 100%; padding: 10px; border-radius: 9999px; border: none; background: #1d9bf0; color: #fff; font-weight: 700; font-size: 15px; cursor: pointer; margin-top: 6px; } button.primary:hover { background: #1a8cd8; } button.primary:disabled { background: #8ecdf7; cursor: default; } button.green { background: #17bf63 !important; } button.green:hover { background: #15a857 !important; } button.gray { background: #536471 !important; } button.gray:hover { background: #3e4e56 !important; } .status { display: none; padding: 12px; border-radius: 10px; font-size: 14px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; } .status.ok { background: #e8f6fe; border: 1px solid #b4dffc; } .status.err { background: #ffeaea; border: 1px solid #ffc5c5; color: #b00; } .status.trans { background: #f0fdf4; border: 1px solid #86efac; color: #14532d; } .copy-hint { font-size: 12px; color: #536471; text-align: center; display: none; } .badge { display: inline-block; padding: 2px 8px; border-radius: 999px; background: #ffad1f; color: #fff; font-size: 11px; font-weight: 700; } .typing-opts { margin-top: 8px; padding-top: 10px; border-top: 1px solid #eff3f4; display: none; flex-direction: column; gap: 8px; } .typing-opts.visible { display: flex; } .check-row { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #0f1419; cursor: pointer; } .check-row input { cursor: pointer; } .btn-row { display: flex; gap: 8px; } .btn-row .primary { flex: 1; margin-top: 0; } `; const fab = document.createElement('button'); fab.className = 'fab'; fab.textContent = '🤖'; fab.title = 'AI Comment'; const drawer = document.createElement('div'); drawer.className = 'drawer'; drawer.innerHTML = `
✍️ AI Comment
${escapeHtml(tweetText)}
💡 Nếu chưa mở ô reply, hãy bấm Reply trước!
Nhập văn bản và chọn ngôn ngữ cần dịch. API translate lấy từ tab Config.
Cấu hình API. Nếu Translate URL để trống, sẽ dùng chung Comment URL.
`; shadow.appendChild(style); shadow.appendChild(fab); shadow.appendChild(drawer); // Toggle const toggleDrawer = () => drawer.classList.toggle('open'); fab.addEventListener('click', toggleDrawer); drawer.querySelector('.btn-x').addEventListener('click', () => drawer.classList.remove('open')); requestAnimationFrame(() => drawer.classList.add('open')); // Tabs const tabBtns = drawer.querySelectorAll('.tab-btn'); const tabContents = drawer.querySelectorAll('.tab-content'); tabBtns.forEach(btn => { btn.addEventListener('click', () => { tabBtns.forEach(b => b.classList.remove('active')); tabContents.forEach(c => c.classList.remove('active')); btn.classList.add('active'); drawer.querySelector(`#tab-${btn.dataset.tab}`).classList.add('active'); }); }); // ===== COMMENT TAB ===== const langSel = drawer.querySelector('#ai-lang'); const toneSel = drawer.querySelector('#ai-tone'); const toneHint = drawer.querySelector('#ai-tone-hint'); const angleSel = drawer.querySelector('#ai-angle'); const angleHint= drawer.querySelector('#ai-angle-hint'); const runBtn = drawer.querySelector('#ai-run'); const statusEl = drawer.querySelector('#ai-status'); const transViEl= drawer.querySelector('#ai-trans-vi'); const copyHint = drawer.querySelector('#ai-hint'); const typingOpts = drawer.querySelector('#ai-typing-opts'); const typingChk = drawer.querySelector('#ai-typing'); const pasteBtn = drawer.querySelector('#ai-paste'); const copyBtn = drawer.querySelector('#ai-copy'); function populateSelect(sel, items, selectedValue) { sel.innerHTML = items.map(it => ``).join(''); if (selectedValue && items.find(i => i.value === selectedValue)) sel.value = selectedValue; } function updateTone(lang, keepValueIfValid) { const items = getTones(lang); const old = toneSel.value; populateSelect(toneSel, items, keepValueIfValid && items.find(i => i.value === old) ? old : null); onToneChange(); } function onToneChange() { const lang = langSel.value; const tone = toneSel.value; const tItem = getTones(lang).find(i => i.value === tone); toneHint.textContent = tItem ? tItem.text : ''; const oldAngle = angleSel.value; const aItems = getAngles(tone); populateSelect(angleSel, aItems, aItems.find(i => i.value === oldAngle) ? oldAngle : null); onAngleChange(); } function onAngleChange() { const tone = toneSel.value; const angle = angleSel.value; const aItem = getAngles(tone).find(i => i.value === angle); angleHint.textContent = aItem ? aItem.text : ''; } langSel.addEventListener('change', () => updateTone(langSel.value, true)); toneSel.addEventListener('change', onToneChange); angleSel.addEventListener('change', onAngleChange); updateTone(langSel.value, false); runBtn.addEventListener('click', () => { const lang = langSel.value; const tone = toneSel.value; const angle = angleSel.value; runBtn.disabled = true; statusEl.style.display = 'block'; statusEl.className = 'status ok'; statusEl.textContent = '⏳ Đang gọi API...'; transViEl.style.display = 'none'; transViEl.textContent = ''; copyHint.style.display = 'none'; typingOpts.classList.remove('visible'); chrome.runtime.sendMessage( { action: 'GENERATE_COMMENT', data: { text: tweetText, lang, tone, angle } }, () => { if (chrome.runtime.lastError) { statusEl.className = 'status err'; statusEl.textContent = 'Lỗi kết nối: ' + chrome.runtime.lastError.message; runBtn.disabled = false; } } ); }); copyBtn.addEventListener('click', async () => { const text = statusEl.textContent; if (!text || text.startsWith('⏳') || text.startsWith('Lỗi') || text.startsWith('⚠️')) { flashBtn(copyBtn, '⚠️ Chưa có nội dung!'); return; } try { await navigator.clipboard.writeText(text); flashBtn(copyBtn, '✅ Đã copy!'); } catch (e) { flashBtn(copyBtn, '❌ Lỗi copy'); } }); pasteBtn.addEventListener('click', async () => { const text = statusEl.textContent; if (!text || text.startsWith('⏳') || text.startsWith('Lỗi') || text.startsWith('⚠️')) { alert('Chưa có nội dung hợp lệ để dán. Hãy tạo comment trước!'); return; } if (isTyping) return; isTyping = true; pasteBtn.disabled = true; try { if (typingChk.checked) { pasteBtn.textContent = '⌨️ Đang gõ...'; const ok = await simulateHumanTyping(text); if (ok) drawer.classList.remove('open'); } else { await navigator.clipboard.writeText(text); alert('✅ Đã copy vào clipboard!\n\nBạn tự dán vào ô reply nhé.'); } } catch (err) { console.error('Typing error:', err); alert('Lỗi khi nhập: ' + err.message); } finally { isTyping = false; pasteBtn.disabled = false; pasteBtn.textContent = '📥 Dán vào ô reply'; } }); // ===== TRANSLATE TAB ===== const transInput = drawer.querySelector('#trans-input'); const transTarget= drawer.querySelector('#trans-target'); const transModel= drawer.querySelector('#trans-model'); const transRun = drawer.querySelector('#trans-run'); const transStatus= drawer.querySelector('#trans-status'); const transHint = drawer.querySelector('#trans-hint'); const transCopy = drawer.querySelector('#trans-copy'); const transPaste = drawer.querySelector('#trans-paste'); transRun.addEventListener('click', () => { const text = transInput.value.trim(); const target = transTarget.value; const targetModel = transModel.value; if (!text) { flashBtn(transRun, '⚠️ Nhập văn bản!'); return; } transRun.disabled = true; transStatus.style.display = 'block'; transStatus.className = 'status ok'; transStatus.textContent = '⏳ Đang dịch...'; transHint.style.display = 'none'; chrome.runtime.sendMessage( { action: 'TRANSLATE_TEXT', data: { text, target_lang: target, target_model: targetModel } }, () => { if (chrome.runtime.lastError) { transStatus.className = 'status err'; transStatus.textContent = 'Lỗi kết nối: ' + chrome.runtime.lastError.message; transRun.disabled = false; } } ); }); transCopy.addEventListener('click', async () => { const text = transStatus.textContent; if (!text || text.startsWith('⏳')) { flashBtn(transCopy, '⚠️ Chưa có!'); return; } try { await navigator.clipboard.writeText(text); flashBtn(transCopy, '✅ Đã copy!'); } catch (e) { flashBtn(transCopy, '❌ Lỗi'); } }); transPaste.addEventListener('click', async () => { const text = transStatus.textContent; if (!text || text.startsWith('⏳')) { alert('Chưa có kết quả dịch!'); return; } if (isTyping) return; isTyping = true; transPaste.disabled = true; try { transPaste.textContent = '⌨️ Đang gõ...'; const ok = await simulateHumanTyping(text); if (ok) drawer.classList.remove('open'); } catch (err) { alert('Lỗi khi nhập: ' + err.message); } finally { isTyping = false; transPaste.disabled = false; transPaste.textContent = '📥 Dán vào ô reply'; } }); // ===== CONFIG TAB ===== const urlIn = drawer.querySelector('#cfg-url'); const keyIn = drawer.querySelector('#cfg-key'); const transUrlIn= drawer.querySelector('#cfg-trans-url'); const transKeyIn= drawer.querySelector('#cfg-trans-key'); const saveBtn = drawer.querySelector('#cfg-save'); const testBtn = drawer.querySelector('#cfg-test'); const cfgStatus = drawer.querySelector('#cfg-status'); const cfgBadge = drawer.querySelector('#cfg-missing'); function showCfgStatus(msg, isErr) { cfgStatus.style.display = 'block'; cfgStatus.className = 'status ' + (isErr ? 'err' : 'ok'); cfgStatus.textContent = msg; } chrome.storage.local.get(['apiUrl','apiKey','translateUrl','translateKey'], (cfg) => { if (cfg.apiUrl) urlIn.value = cfg.apiUrl; if (cfg.apiKey) keyIn.value = cfg.apiKey; if (cfg.translateUrl) transUrlIn.value = cfg.translateUrl; if (cfg.translateKey) transKeyIn.value = cfg.translateKey; cfgBadge.style.display = (!cfg.apiUrl || !cfg.apiKey) ? 'inline-block' : 'none'; }); saveBtn.addEventListener('click', () => { const url = urlIn.value.trim(), key = keyIn.value.trim(); if (!url || !key) { showCfgStatus('❌ Comment URL và Key không được để trống!', true); return; } try { new URL(url); } catch { showCfgStatus('❌ URL không hợp lệ', true); return; } const tUrl = transUrlIn.value.trim(); const tKey = transKeyIn.value.trim(); chrome.storage.local.set({ apiUrl: url, apiKey: key, translateUrl: tUrl, translateKey: tKey }, () => { showCfgStatus('✅ Đã lưu!', false); cfgBadge.style.display = 'none'; }); }); testBtn.addEventListener('click', () => { chrome.storage.local.get(['apiUrl','apiKey','translateUrl','translateKey'], (cfg) => { const tUrl = cfg.translateUrl || cfg.apiUrl || '(trống)'; showCfgStatus( `Comment: ${cfg.apiUrl || '(trống)'}\n` + `Translate: ${tUrl}\n` + `Key: ${cfg.apiKey ? cfg.apiKey.slice(0,8)+'...' : '(trống)'}`, !cfg.apiUrl || !cfg.apiKey ); }); }); } // ===== SHOW RESULT / ERROR ===== function showResult(comment, model='', transVi='', transByModal = '') { if (!sidebarHost) return; const s = sidebarHost.shadowRoot; const statusEl = s.querySelector('#ai-status'); const transViEl= s.querySelector('#ai-trans-vi'); const copyHint = s.querySelector('#ai-hint'); const runBtn = s.querySelector('#ai-run'); const drawer = s.querySelector('.drawer'); const typingOpts = s.querySelector('#ai-typing-opts'); console.log({model}); if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'status ok'; statusEl.textContent = comment; } if (transViEl) { if (transVi) { transViEl.style.display = 'block'; transViEl.textContent = '🇻🇳 ' + transVi; } else { transViEl.style.display = 'none'; transViEl.textContent = ''; } } if (copyHint) { copyHint.style.display = 'block'; copyHint.textContent = `W:${model} - T:${transByModal}` || '📋 Copy kết quả và dán vào ô reply!'; } if (runBtn) runBtn.disabled = false; if (drawer) drawer.classList.add('open'); if (typingOpts) typingOpts.classList.add('visible'); } function showError(msg) { if (!sidebarHost) return; const s = sidebarHost.shadowRoot; const statusEl = s.querySelector('#ai-status'); const transViEl= s.querySelector('#ai-trans-vi'); const runBtn = s.querySelector('#ai-run'); const drawer = s.querySelector('.drawer'); if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'status err'; statusEl.textContent = msg; } if (transViEl) { transViEl.style.display = 'none'; transViEl.textContent = ''; } if (runBtn) runBtn.disabled = false; if (drawer) drawer.classList.add('open'); } function showTranslateResult(content, model = '') { if (!sidebarHost) return; const s = sidebarHost.shadowRoot; const status = s.querySelector('#trans-status'); const hint = s.querySelector('#trans-hint'); const runBtn = s.querySelector('#trans-run'); const drawer = s.querySelector('.drawer'); if (status) { status.style.display = 'block'; status.className = 'status ok'; status.textContent = content; } if (hint) { hint.style.display = 'block';hint.textContent = model } if (runBtn) runBtn.disabled = false; if (drawer) drawer.classList.add('open'); } function showTranslateError(msg) { if (!sidebarHost) return; const s = sidebarHost.shadowRoot; const status = s.querySelector('#trans-status'); const runBtn = s.querySelector('#trans-run'); if (status) { status.style.display = 'block'; status.className = 'status err'; status.textContent = msg; } if (runBtn) runBtn.disabled = false; } function removeSidebar() { if (sidebarHost) { sidebarHost.remove(); sidebarHost = null; } } function escapeHtml(text) { const d = document.createElement('div'); d.textContent = text; return d.innerHTML; } function flashBtn(btn, msg) { const old = btn.textContent; btn.textContent = msg; setTimeout(() => btn.textContent = old, 1200); } })();