This commit is contained in:
NAME
2026-05-15 14:42:31 +00:00
parent 8feb07344e
commit 3700309c06
2 changed files with 118 additions and 120 deletions
+16 -32
View File
@@ -1,6 +1,6 @@
console.log('[XAI Background] ✅ Service worker started'); console.log('[XAI Background] ✅ Service worker started');
// ===== TẠO MENU ===== // ===== MENU =====
chrome.runtime.onInstalled.addListener(() => { chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({ chrome.contextMenus.create({
id: 'writeComment', id: 'writeComment',
@@ -10,69 +10,53 @@ chrome.runtime.onInstalled.addListener(() => {
}); });
}); });
chrome.contextMenus.onClicked.addListener(async (info, tab) => { // FIRE & FORGET — không đợi content trả lời
chrome.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId !== 'writeComment' || !tab?.id) return; if (info.menuItemId !== 'writeComment' || !tab?.id) return;
try { chrome.tabs.sendMessage(tab.id, { action: 'OPEN_FORM' }).catch(() => {});
await chrome.tabs.sendMessage(tab.id, { action: 'OPEN_FORM' });
} catch (err) {
console.error('[XAI Background] ❌ Không gọi được content script:', err.message);
}
}); });
// ===== NHẬN YÊU CẦU TỪ CONTENT ===== // LUÔN GỌI sendResponse(), KHÔNG BAO GIỜ return true
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.action === 'GENERATE_COMMENT') { if (request.action === 'GENERATE_COMMENT') {
// Gọi API async riêng, không await ở đây
handleGenerate(request.data, sender.tab.id); handleGenerate(request.data, sender.tab.id);
// Đóng kênh ngay để tránh lỗi "channel closed"
sendResponse({ received: true }); sendResponse({ received: true });
return false;
} }
sendResponse({ unknown: true });
return false;
}); });
// ===== GỌI API: ĐỌC CONFIG TỪ STORAGE =====
async function handleGenerate({ text, lang, tone, angle }, tabId) { async function handleGenerate({ text, lang, tone, angle }, tabId) {
console.log('[XAI Background] ⏳ Đọc config từ storage...'); const cfg = await chrome.storage.local.get(['apiUrl', 'apiKey']);
if (!cfg.apiUrl || !cfg.apiKey) {
const config = await chrome.storage.local.get(['apiUrl', 'apiKey']);
if (!config.apiUrl || !config.apiKey) {
console.error('[XAI Background] ❌ Thiếu API config');
await chrome.tabs.sendMessage(tabId, { await chrome.tabs.sendMessage(tabId, {
action: 'SHOW_ERROR', action: 'SHOW_ERROR',
error: '⚠️ Chưa cấu hình API.\n\n👉 Bấm tab ⚙️ Config để nhập URL và Key.' error: '⚠️ Chưa cấu hình API.\n\n👉 Bấm tab ⚙️ Config để nhập URL và Key.'
}); }).catch(() => {});
return; return;
} }
try {
const payload = { const payload = {
originalPost: text, originalPost: text,
language: lang, language: lang,
tone: tone?tone.toLowerCase():undefined, tone: tone?tone.toLowerCase():undefined,
angle: angle?angle.toLowerCase():undefined, angle: angle?angle.toLowerCase():undefined,
}; };
console.log('[XAI Background] 📤 Payload:', JSON.stringify(payload, null, 2)); const res = await fetch(cfg.apiUrl, {
console.log('[XAI Background] 🌐 URL:', config.apiUrl);
try {
const res = await fetch(config.apiUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${config.apiKey}` 'Authorization': `Bearer ${cfg.apiKey}`
}, },
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
const data = await res.json(); const data = await res.json();
console.log('[XAI Background] 📥 Response:', res.status, data);
if (!res.ok) throw new Error(`HTTP ${res.status}`); if (!res.ok) throw new Error(`HTTP ${res.status}`);
const comment = data.comment || data.text || JSON.stringify(data); const comment = data.comment || data.text || JSON.stringify(data);
await chrome.tabs.sendMessage(tabId, { action: 'SHOW_RESULT', comment }); await chrome.tabs.sendMessage(tabId, { action: 'SHOW_RESULT', comment }).catch(() => {});
} catch (err) { } catch (err) {
console.error('[XAI Background] ❌ Lỗi fetch:', err.message); await chrome.tabs.sendMessage(tabId, { action: 'SHOW_ERROR', error: err.message }).catch(() => {});
await chrome.tabs.sendMessage(tabId, { action: 'SHOW_ERROR', error: err.message });
} }
} }
+94 -80
View File
@@ -3,14 +3,15 @@
let lastTweetText = ''; let lastTweetText = '';
let sidebarHost = null; let sidebarHost = null;
let isTyping = false; // chặn bấm paste nhiều lần
// ===== DATA (giữ nguyên) ===== // ===== DATA =====
const LANGS = [ const LANGS = [
{ value: 'vi', label: 'Việt' }, { value: 'vi', label: 'Việt' },
{ value: 'ja', label: 'Nhật' },
{ value: 'en', label: 'English' }, { value: 'en', label: 'English' },
{ value: 'ko', label: 'Hàn' }, { value: 'ja', label: 'Nhật' },
{ value: 'cn', label: 'Trung' } { value: 'ko', label: 'Han' },
{ value: 'cn', label: 'TQ' }
]; ];
const TONE_BASE = [ const TONE_BASE = [
@@ -62,7 +63,7 @@
return tone === 'EMPATHETIC' ? [...ANGLE_EMPATHY] : [...ANGLE_DEFAULT]; return tone === 'EMPATHETIC' ? [...ANGLE_EMPATHY] : [...ANGLE_DEFAULT];
} }
// ===== A. BẮT CHUỘT PHẢI ===== // ===== A. CAPTURE TWEET =====
document.addEventListener('contextmenu', (e) => { document.addEventListener('contextmenu', (e) => {
const article = e.target.closest('article'); const article = e.target.closest('article');
if (!article) { lastTweetText = ''; return; } if (!article) { lastTweetText = ''; return; }
@@ -70,87 +71,95 @@
article.querySelector('[data-testid="tweetText"]') || article.querySelector('[data-testid="tweetText"]') ||
article.querySelector('div[lang]') || article.querySelector('div[lang]') ||
article.querySelector('div[dir="auto"]'); article.querySelector('div[dir="auto"]');
lastTweetText = textEl lastTweetText = textEl ? textEl.innerText.trim() : article.innerText.trim().slice(0, 600);
? textEl.innerText.trim()
: article.innerText.trim().slice(0, 600);
}); });
// ===== B. LISTENER — FIX LỖI CHANNEL =====
chrome.runtime.onMessage.addListener((req, sender, sendResponse) => { chrome.runtime.onMessage.addListener((req, sender, sendResponse) => {
if (req.action === 'OPEN_FORM') { if (req.action === 'OPEN_FORM') {
if (!lastTweetText) { if (!lastTweetText) {
alert('Không tìm thấy tweet. Hãy chuột phải vào phần chữ của tweet.'); sendResponse({ ok: false, reason: 'no_text' });
return true; return false;
} }
openSidebar(lastTweetText); openSidebar(lastTweetText);
return true; sendResponse({ ok: true });
return false;
} }
if (req.action === 'SHOW_RESULT') { showResult(req.comment); return true; } if (req.action === 'SHOW_RESULT') {
if (req.action === 'SHOW_ERROR') { showError(req.error); return true; } showResult(req.comment);
sendResponse({ ok: true });
return false;
}
if (req.action === 'SHOW_ERROR') {
showError(req.error);
sendResponse({ ok: true });
return false;
}
sendResponse({ ok: false, unknown: true });
return false;
}); });
// ===== HUMAN TYPING SIMULATION ===== // ===== C. TYPING ENGINE (VIẾT LẠI) =====
async function simulateHumanTyping(text) { async function simulateHumanTyping(fullText) {
// X dùng contenteditable div. Tìm ô reply đang mở. // Tìm ô reply của X
let el = let editor =
document.querySelector('[data-testid="tweetTextarea_0"]') || document.querySelector('[data-testid="tweetTextarea_0"]') ||
document.querySelector('div[contenteditable="true"][role="textbox"]') || document.querySelector('div[contenteditable="true"][role="textbox"]');
document.querySelector('div[contenteditable="true"][data-text="true"]');
if (!el) { 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!'); 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; return false;
} }
el.focus(); editor.focus();
el.click(); editor.click();
// Reset sạch: xóa hết, tạo 1 TextNode duy nhất để dễ manipulate
editor.textContent = '';
const textNode = document.createTextNode('');
editor.appendChild(textNode);
// Di chuyển cursor về cuối nếu cần (optional)
const sel = window.getSelection(); const sel = window.getSelection();
sel.selectAllChildren(el); const range = document.createRange();
sel.collapseToEnd(); range.setStart(textNode, 0);
const baseDelay = 35; // ms
for (let i = 0; i < text.length; i++) {
const char = text[i];
// Cách đáng tin cậy nhất trên X: execCommand insertText
const inserted = document.execCommand('insertText', false, char);
// Fallback nếu execCommand bị khước từ
if (!inserted) {
const range = sel.getRangeAt(0);
range.deleteContents();
const node = document.createTextNode(char);
range.insertNode(node);
range.setStartAfter(node);
range.collapse(true); range.collapse(true);
sel.removeAllRanges(); sel.removeAllRanges();
sel.addRange(range); sel.addRange(range);
}
// Kích hoạt React re-render for (let i = 0; i < fullText.length; i++) {
el.dispatchEvent(new InputEvent('input', { const char = fullText[i];
// 1. Thêm ký tự vào TextNode
textNode.nodeValue += char;
// 2. Đẩy cursor về cuối
range.setEnd(textNode, textNode.nodeValue.length);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
// 3. Báo cho React/Draft.js biết DOM đã đổi
editor.dispatchEvent(new InputEvent('input', {
bubbles: true, bubbles: true,
cancelable: true, cancelable: true,
inputType: 'insertText', inputType: 'insertText',
data: char data: char
})); }));
// Delay ngẫu nhiên // 4. Delay như người gõ thật
let delay = baseDelay + Math.random() * 70; let delay = 30 + Math.random() * 50;
if (char === ' ') delay += 30 + Math.random() * 50; if (char === ' ') delay += 40 + Math.random() * 40;
if ('.!?,'.includes(char)) delay += 120 + Math.random() * 250; if ('.!?,'.includes(char)) delay += 120 + Math.random() * 200;
await new Promise(r => setTimeout(r, delay)); await new Promise(r => setTimeout(r, delay));
} }
// Final change event cho chắc // Final change event
el.dispatchEvent(new Event('change', { bubbles: true })); editor.dispatchEvent(new Event('change', { bubbles: true }));
return true; return true;
} }
// ===== SIDEBAR ===== // ===== D. SIDEBAR UI =====
function openSidebar(tweetText) { function openSidebar(tweetText) {
removeSidebar(); removeSidebar();
@@ -171,7 +180,7 @@
cursor: pointer; box-shadow: 0 4px 16px rgba(0,0,0,0.25); border: none; pointer-events: auto; z-index: 1; 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; } transition: transform .15s ease; user-select: none; }
.fab:hover { transform: scale(1.08); } .fab:hover { transform: scale(1.08); }
.drawer { position: fixed; right: 0; top: 0; width: 442px; max-width: 100vw; height: 100vh; background: #fff; color: #0f1419; .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; 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; display: flex; flex-direction: column; pointer-events: auto; z-index: 2;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; } font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
@@ -195,17 +204,18 @@
font-weight: 700; font-size: 15px; cursor: pointer; margin-top: 6px; } font-weight: 700; font-size: 15px; cursor: pointer; margin-top: 6px; }
button.primary:hover { background: #1a8cd8; } button.primary:hover { background: #1a8cd8; }
button.primary:disabled { background: #8ecdf7; cursor: default; } button.primary:disabled { background: #8ecdf7; cursor: default; }
.status { display: none; padding: 12px; border-radius: 10px; font-size: 14px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; } button.green { background: #17bf63 !important; }
button.green:hover { background: #15a857 !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.ok { background: #e8f6fe; border: 1px solid #b4dffc; }
.status.err { background: #ffeaea; border: 1px solid #ffc5c5; color: #b00; } .status.err { background: #ffeaea; border: 1px solid #ffc5c5; color: #b00; }
.copy-hint { font-size: 12px; color: #536471; text-align: center; display: none; } .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; } .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; } .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; 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 { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #0f1419; cursor: pointer; }
.check-row input { cursor: pointer; } .check-row input { cursor: pointer; }
.btn-green { background: #17bf63 !important; }
.btn-green:hover { background: #15a857 !important; }
`; `;
const fab = document.createElement('button'); const fab = document.createElement('button');
@@ -220,7 +230,6 @@
<button class="tab-btn" data-tab="config">⚙️ Config <span class="badge" id="cfg-missing" style="display:none">!</span></button> <button class="tab-btn" data-tab="config">⚙️ Config <span class="badge" id="cfg-missing" style="display:none">!</span></button>
</div> </div>
<!-- TAB COMMENT -->
<div class="tab-content active" id="tab-comment"> <div class="tab-content active" id="tab-comment">
<div class="tweet-box">${escapeHtml(tweetText)}</div> <div class="tweet-box">${escapeHtml(tweetText)}</div>
@@ -240,36 +249,30 @@
<div class="status" id="ai-status"></div> <div class="status" id="ai-status"></div>
<div class="copy-hint" id="ai-hint">📋 Copy kết quả và dán vào ô reply!</div> <div class="copy-hint" id="ai-hint">📋 Copy kết quả và dán vào ô reply!</div>
<!-- === TYPING FEATURE === -->
<div class="typing-opts" id="ai-typing-opts"> <div class="typing-opts" id="ai-typing-opts">
<label class="check-row"> <label class="check-row">
<input type="checkbox" id="ai-typing" checked> <input type="checkbox" id="ai-typing" checked>
<span>Giả lập gõ như người thật (từ từ)</span> <span>Giả lập gõ như người thật (từ từ)</span>
</label> </label>
<button class="primary btn-green" id="ai-paste">📥 Dán vào ô reply</button> <button class="primary green" id="ai-paste">📥 Dán vào ô reply</button>
<div style="font-size:12px;color:#536471;text-align:center;"> <div style="font-size:12px;color:#536471;text-align:center;">
💡 Nếu chưa mở ô reply, hãy bấm Reply trước! 💡 Nếu chưa mở ô reply, hãy bấm Reply trước!
</div> </div>
</div> </div>
</div> </div>
<!-- TAB CONFIG -->
<div class="tab-content" id="tab-config"> <div class="tab-content" id="tab-config">
<div style="font-size:13px; color:#536471; margin-bottom:6px;"> <div style="font-size:13px; color:#536471; margin-bottom:6px;">
Nhập API của bạn. Dữ liệu được lưu trên trình duyệt này. Nhập API của bạn. Dữ liệu được lưu trên trình duyệt này.
</div> </div>
<label>API URL</label> <label>API URL</label>
<input type="text" id="cfg-url" placeholder="https://api.yoursite.com/v1/generate"> <input type="text" id="cfg-url" placeholder="https://api.yoursite.com/v1/generate">
<label>API Key</label> <label>API Key</label>
<input type="password" id="cfg-key" placeholder="sk-..."> <input type="password" id="cfg-key" placeholder="sk-...">
<div style="display:flex; gap:8px;"> <div style="display:flex; gap:8px;">
<button class="primary" id="cfg-save" style="flex:1">💾 Lưu</button> <button class="primary" id="cfg-save" style="flex:1">💾 Lưu</button>
<button class="primary" id="cfg-test" style="flex:1;background:#17bf63;">🧪 Test đọc</button> <button class="primary green" id="cfg-test" style="flex:1">🧪 Test đọc</button>
</div> </div>
<div class="status" id="cfg-status"></div> <div class="status" id="cfg-status"></div>
</div> </div>
`; `;
@@ -278,7 +281,7 @@
shadow.appendChild(fab); shadow.appendChild(fab);
shadow.appendChild(drawer); shadow.appendChild(drawer);
// Toggle drawer // Toggle
const toggleDrawer = () => drawer.classList.toggle('open'); const toggleDrawer = () => drawer.classList.toggle('open');
fab.addEventListener('click', toggleDrawer); fab.addEventListener('click', toggleDrawer);
drawer.querySelector('.btn-x').addEventListener('click', () => drawer.classList.remove('open')); drawer.querySelector('.btn-x').addEventListener('click', () => drawer.classList.remove('open'));
@@ -296,7 +299,7 @@
}); });
}); });
// ===== COMMENT TAB LOGIC ===== // ===== COMMENT TAB =====
const langSel = drawer.querySelector('#ai-lang'); const langSel = drawer.querySelector('#ai-lang');
const toneSel = drawer.querySelector('#ai-tone'); const toneSel = drawer.querySelector('#ai-tone');
const toneHint = drawer.querySelector('#ai-tone-hint'); const toneHint = drawer.querySelector('#ai-tone-hint');
@@ -346,8 +349,10 @@
const tone = toneSel.value; const tone = toneSel.value;
const angle = angleSel.value; const angle = angleSel.value;
runBtn.disabled = true; runBtn.disabled = true;
statusEl.style.display = 'block'; statusEl.className = 'status ok'; statusEl.textContent = '⏳ Đang gọi API...'; copyHint.style.display = 'none'; statusEl.style.display = 'block'; statusEl.className = 'status ok';
statusEl.textContent = '⏳ Đang gọi API...'; copyHint.style.display = 'none';
typingOpts.classList.remove('visible'); typingOpts.classList.remove('visible');
chrome.runtime.sendMessage( chrome.runtime.sendMessage(
{ action: 'GENERATE_COMMENT', data: { text: tweetText, lang, tone, angle } }, { action: 'GENERATE_COMMENT', data: { text: tweetText, lang, tone, angle } },
() => { () => {
@@ -360,27 +365,37 @@
); );
}); });
// Paste button // PASTE BUTTON
pasteBtn.addEventListener('click', async () => { pasteBtn.addEventListener('click', async () => {
const text = statusEl.textContent; const text = statusEl.textContent;
if (!text || text.startsWith('⏳')) { if (!text || text.startsWith('⏳') || text.startsWith('Lỗi') || text.startsWith('⚠️')) {
alert('Chưa có nội dung để dán. Hãy tạo comment trước!'); alert('Chưa có nội dung hợp lệ để dán. Hãy tạo comment trước!');
return; return;
} }
if (typingChk.checked) { if (isTyping) return;
isTyping = true;
pasteBtn.disabled = true; pasteBtn.disabled = true;
try {
if (typingChk.checked) {
pasteBtn.textContent = '⌨️ Đang gõ...'; pasteBtn.textContent = '⌨️ Đang gõ...';
const ok = await simulateHumanTyping(text); const ok = await simulateHumanTyping(text);
pasteBtn.disabled = false; if (ok) drawer.classList.remove('open'); // thu sidebar để người dùng thấy reply
pasteBtn.textContent = '📥 Dán vào ô reply';
if (ok) drawer.classList.remove('open'); // thu sidebar để người dùng thấy ô reply
} else { } else {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
alert('✅ Đã copy vào clipboard! Bạn tự dán nhé.'); 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';
} }
}); });
// ===== CONFIG TAB (giữ nguyên) ===== // ===== CONFIG TAB =====
const urlIn = drawer.querySelector('#cfg-url'); const urlIn = drawer.querySelector('#cfg-url');
const keyIn = drawer.querySelector('#cfg-key'); const keyIn = drawer.querySelector('#cfg-key');
const saveBtn = drawer.querySelector('#cfg-save'); const saveBtn = drawer.querySelector('#cfg-save');
@@ -414,7 +429,7 @@
}); });
} }
// ===== RESULT / ERROR ===== // ===== SHOW RESULT / ERROR =====
function showResult(comment) { function showResult(comment) {
if (!sidebarHost) return; if (!sidebarHost) return;
const s = sidebarHost.shadowRoot; const s = sidebarHost.shadowRoot;
@@ -428,7 +443,6 @@
if (copyHint) copyHint.style.display = 'block'; if (copyHint) copyHint.style.display = 'block';
if (runBtn) runBtn.disabled = false; if (runBtn) runBtn.disabled = false;
if (drawer) drawer.classList.add('open'); if (drawer) drawer.classList.add('open');
// Hiện nút paste + typing options
if (typingOpts) typingOpts.classList.add('visible'); if (typingOpts) typingOpts.classList.add('visible');
} }
function showError(msg) { function showError(msg) {