Update
This commit is contained in:
+97
-83
@@ -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 range = document.createRange();
|
||||
range.setStart(textNode, 0);
|
||||
range.collapse(true);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
|
||||
const baseDelay = 35; // ms
|
||||
for (let i = 0; i < fullText.length; i++) {
|
||||
const char = fullText[i];
|
||||
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const char = text[i];
|
||||
// 1. Thêm ký tự vào TextNode
|
||||
textNode.nodeValue += char;
|
||||
|
||||
// Cách đáng tin cậy nhất trên X: execCommand insertText
|
||||
const inserted = document.execCommand('insertText', false, char);
|
||||
// 2. Đẩy cursor về cuối
|
||||
range.setEnd(textNode, textNode.nodeValue.length);
|
||||
range.collapse(false);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
|
||||
// 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);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}
|
||||
|
||||
// Kích hoạt React re-render
|
||||
el.dispatchEvent(new InputEvent('input', {
|
||||
// 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) {
|
||||
pasteBtn.disabled = true;
|
||||
pasteBtn.textContent = '⌨️ Đang gõ...';
|
||||
const ok = await simulateHumanTyping(text);
|
||||
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'); // thu sidebar để người dùng thấy reply
|
||||
} 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';
|
||||
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é.');
|
||||
}
|
||||
});
|
||||
|
||||
// ===== 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) {
|
||||
|
||||
Reference in New Issue
Block a user