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');
// ===== TẠO MENU =====
// ===== MENU =====
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
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;
try {
await chrome.tabs.sendMessage(tab.id, { action: 'OPEN_FORM' });
} catch (err) {
console.error('[XAI Background] ❌ Không gọi được content script:', err.message);
}
chrome.tabs.sendMessage(tab.id, { action: 'OPEN_FORM' }).catch(() => {});
});
// ===== 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) => {
if (request.action === 'GENERATE_COMMENT') {
// Gọi API async riêng, không await ở đây
handleGenerate(request.data, sender.tab.id);
// Đóng kênh ngay để tránh lỗi "channel closed"
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) {
console.log('[XAI Background] ⏳ Đọc config từ storage...');
const config = await chrome.storage.local.get(['apiUrl', 'apiKey']);
if (!config.apiUrl || !config.apiKey) {
console.error('[XAI Background] ❌ Thiếu API config');
const cfg = await chrome.storage.local.get(['apiUrl', 'apiKey']);
if (!cfg.apiUrl || !cfg.apiKey) {
await chrome.tabs.sendMessage(tabId, {
action: 'SHOW_ERROR',
error: '⚠️ Chưa cấu hình API.\n\n👉 Bấm tab ⚙️ Config để nhập URL và Key.'
});
}).catch(() => {});
return;
}
try {
const payload = {
originalPost: text,
language: lang,
tone: tone?tone.toLowerCase():undefined,
angle: angle?angle.toLowerCase():undefined,
};
console.log('[XAI Background] 📤 Payload:', JSON.stringify(payload, null, 2));
console.log('[XAI Background] 🌐 URL:', config.apiUrl);
try {
const res = await fetch(config.apiUrl, {
const res = await fetch(cfg.apiUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${config.apiKey}`
'Authorization': `Bearer ${cfg.apiKey}`
},
body: JSON.stringify(payload)
});
const data = await res.json();
console.log('[XAI Background] 📥 Response:', res.status, data);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
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) {
console.error('[XAI Background] ❌ Lỗi fetch:', err.message);
await chrome.tabs.sendMessage(tabId, { action: 'SHOW_ERROR', error: err.message });
await chrome.tabs.sendMessage(tabId, { action: 'SHOW_ERROR', error: err.message }).catch(() => {});
}
}
+94 -80
View File
@@ -3,14 +3,15 @@
let lastTweetText = '';
let sidebarHost = null;
let isTyping = false; // chặn bấm paste nhiều lần
// ===== DATA (giữ nguyên) =====
// ===== DATA =====
const LANGS = [
{ value: 'vi', label: 'Việt' },
{ value: 'ja', label: 'Nhật' },
{ value: 'en', label: 'English' },
{ value: 'ko', label: 'Hàn' },
{ value: 'cn', label: 'Trung' }
{ value: 'ja', label: 'Nhật' },
{ value: 'ko', label: 'Han' },
{ value: 'cn', label: 'TQ' }
];
const TONE_BASE = [
@@ -62,7 +63,7 @@
return tone === 'EMPATHETIC' ? [...ANGLE_EMPATHY] : [...ANGLE_DEFAULT];
}
// ===== A. BẮT CHUỘT PHẢI =====
// ===== A. CAPTURE TWEET =====
document.addEventListener('contextmenu', (e) => {
const article = e.target.closest('article');
if (!article) { lastTweetText = ''; return; }
@@ -70,87 +71,95 @@
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);
lastTweetText = textEl ? textEl.innerText.trim() : article.innerText.trim().slice(0, 600);
});
// ===== B. LISTENER — FIX LỖI CHANNEL =====
chrome.runtime.onMessage.addListener((req, sender, sendResponse) => {
if (req.action === 'OPEN_FORM') {
if (!lastTweetText) {
alert('Không tìm thấy tweet. Hãy chuột phải vào phần chữ của tweet.');
return true;
sendResponse({ ok: false, reason: 'no_text' });
return false;
}
openSidebar(lastTweetText);
return true;
sendResponse({ ok: true });
return false;
}
if (req.action === 'SHOW_RESULT') { showResult(req.comment); return true; }
if (req.action === 'SHOW_ERROR') { showError(req.error); return true; }
if (req.action === 'SHOW_RESULT') {
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 =====
async function simulateHumanTyping(text) {
// X dùng contenteditable div. Tìm ô reply đang mở.
let el =
// ===== C. TYPING ENGINE (VIẾT LẠI) =====
async function simulateHumanTyping(fullText) {
// Tìm ô reply của X
let editor =
document.querySelector('[data-testid="tweetTextarea_0"]') ||
document.querySelector('div[contenteditable="true"][role="textbox"]') ||
document.querySelector('div[contenteditable="true"][data-text="true"]');
document.querySelector('div[contenteditable="true"][role="textbox"]');
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!');
return false;
}
el.focus();
el.click();
editor.focus();
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();
sel.selectAllChildren(el);
sel.collapseToEnd();
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);
const range = document.createRange();
range.setStart(textNode, 0);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
}
// Kích hoạt React re-render
el.dispatchEvent(new InputEvent('input', {
for (let i = 0; i < fullText.length; i++) {
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,
cancelable: true,
inputType: 'insertText',
data: char
}));
// Delay ngẫu nhiên
let delay = baseDelay + Math.random() * 70;
if (char === ' ') delay += 30 + Math.random() * 50;
if ('.!?,'.includes(char)) delay += 120 + Math.random() * 250;
// 4. Delay như người gõ thật
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));
}
// Final change event cho chắc
el.dispatchEvent(new Event('change', { bubbles: true }));
// Final change event
editor.dispatchEvent(new Event('change', { bubbles: true }));
return true;
}
// ===== SIDEBAR =====
// ===== D. SIDEBAR UI =====
function openSidebar(tweetText) {
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;
transition: transform .15s ease; user-select: none; }
.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;
display: flex; flex-direction: column; pointer-events: auto; z-index: 2;
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; }
button.primary:hover { background: #1a8cd8; }
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.err { background: #ffeaea; border: 1px solid #ffc5c5; color: #b00; }
.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; }
.typing-opts.visible { display: flex; flex-direction: column; gap: 8px; }
.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-green { background: #17bf63 !important; }
.btn-green:hover { background: #15a857 !important; }
`;
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>
</div>
<!-- TAB COMMENT -->
<div class="tab-content active" id="tab-comment">
<div class="tweet-box">${escapeHtml(tweetText)}</div>
@@ -240,36 +249,30 @@
<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>
<!-- === TYPING FEATURE === -->
<div class="typing-opts" id="ai-typing-opts">
<label class="check-row">
<input type="checkbox" id="ai-typing" checked>
<span>Giả lập gõ như người thật (từ từ)</span>
</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;">
💡 Nếu chưa mở ô reply, hãy bấm Reply trước!
</div>
</div>
</div>
<!-- TAB CONFIG -->
<div class="tab-content" id="tab-config">
<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.
</div>
<label>API URL</label>
<input type="text" id="cfg-url" placeholder="https://api.yoursite.com/v1/generate">
<label>API Key</label>
<input type="password" id="cfg-key" placeholder="sk-...">
<div style="display:flex; gap:8px;">
<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 class="status" id="cfg-status"></div>
</div>
`;
@@ -278,7 +281,7 @@
shadow.appendChild(fab);
shadow.appendChild(drawer);
// Toggle drawer
// Toggle
const toggleDrawer = () => drawer.classList.toggle('open');
fab.addEventListener('click', toggleDrawer);
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 toneSel = drawer.querySelector('#ai-tone');
const toneHint = drawer.querySelector('#ai-tone-hint');
@@ -346,8 +349,10 @@
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...'; 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');
chrome.runtime.sendMessage(
{ action: 'GENERATE_COMMENT', data: { text: tweetText, lang, tone, angle } },
() => {
@@ -360,27 +365,37 @@
);
});
// Paste button
// PASTE BUTTON
pasteBtn.addEventListener('click', async () => {
const text = statusEl.textContent;
if (!text || text.startsWith('⏳')) {
alert('Chưa có nội dung để dán. Hãy tạo comment trước!');
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 (typingChk.checked) {
if (isTyping) return;
isTyping = true;
pasteBtn.disabled = true;
try {
if (typingChk.checked) {
pasteBtn.textContent = '⌨️ Đang gõ...';
const ok = await simulateHumanTyping(text);
pasteBtn.disabled = false;
pasteBtn.textContent = '📥 Dán vào ô reply';
if (ok) drawer.classList.remove('open'); // thu sidebar để người dùng thấy ô reply
if (ok) drawer.classList.remove('open'); // thu sidebar để người dùng thấy reply
} else {
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 keyIn = drawer.querySelector('#cfg-key');
const saveBtn = drawer.querySelector('#cfg-save');
@@ -414,7 +429,7 @@
});
}
// ===== RESULT / ERROR =====
// ===== SHOW RESULT / ERROR =====
function showResult(comment) {
if (!sidebarHost) return;
const s = sidebarHost.shadowRoot;
@@ -428,7 +443,6 @@
if (copyHint) copyHint.style.display = 'block';
if (runBtn) runBtn.disabled = false;
if (drawer) drawer.classList.add('open');
// Hiện nút paste + typing options
if (typingOpts) typingOpts.classList.add('visible');
}
function showError(msg) {