This commit is contained in:
Your Name
2026-05-16 18:44:40 +07:00
parent 7811e1f144
commit f1db444853
2 changed files with 33 additions and 12 deletions
+32 -11
View File
@@ -60,7 +60,7 @@
return [...TONE_BASE]; return [...TONE_BASE];
} }
function getAngles(tone) { function getAngles(tone) {
return tone === 'EMPATHETIC' ? [...ANGLE_DEFAULT,...ANGLE_EMPATHY] : [...ANGLE_DEFAULT]; return tone === 'EMPATHETIC' ? [...ANGLE_DEFAULT, ...ANGLE_EMPATHY] : [...ANGLE_DEFAULT];
} }
// ===== A. CAPTURE TWEET ===== // ===== A. CAPTURE TWEET =====
@@ -99,9 +99,8 @@
return false; return false;
}); });
// ===== C. TYPING ENGINE (VIẾT LẠI) ===== // ===== C. TYPING ENGINE =====
async function simulateHumanTyping(fullText) { async function simulateHumanTyping(fullText) {
// Tìm ô reply của X
let editor = 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"]');
@@ -114,7 +113,6 @@
editor.focus(); editor.focus();
editor.click(); editor.click();
// Reset sạch: xóa hết, tạo 1 TextNode duy nhất để dễ manipulate
editor.textContent = ''; editor.textContent = '';
const textNode = document.createTextNode(''); const textNode = document.createTextNode('');
editor.appendChild(textNode); editor.appendChild(textNode);
@@ -128,17 +126,13 @@
for (let i = 0; i < fullText.length; i++) { for (let i = 0; i < fullText.length; i++) {
const char = fullText[i]; const char = fullText[i];
// 1. Thêm ký tự vào TextNode
textNode.nodeValue += char; textNode.nodeValue += char;
// 2. Đẩy cursor về cuối
range.setEnd(textNode, textNode.nodeValue.length); range.setEnd(textNode, textNode.nodeValue.length);
range.collapse(false); range.collapse(false);
sel.removeAllRanges(); sel.removeAllRanges();
sel.addRange(range); sel.addRange(range);
// 3. Báo cho React/Draft.js biết DOM đã đổi
editor.dispatchEvent(new InputEvent('input', { editor.dispatchEvent(new InputEvent('input', {
bubbles: true, bubbles: true,
cancelable: true, cancelable: true,
@@ -146,7 +140,6 @@
data: char data: char
})); }));
// 4. Delay như người gõ thật
let delay = 30 + Math.random() * 50; let delay = 30 + Math.random() * 50;
if (char === ' ') delay += 40 + Math.random() * 40; if (char === ' ') delay += 40 + Math.random() * 40;
if ('.!?,'.includes(char)) delay += 120 + Math.random() * 200; if ('.!?,'.includes(char)) delay += 120 + Math.random() * 200;
@@ -154,7 +147,6 @@
await new Promise(r => setTimeout(r, delay)); await new Promise(r => setTimeout(r, delay));
} }
// Final change event
editor.dispatchEvent(new Event('change', { bubbles: true })); editor.dispatchEvent(new Event('change', { bubbles: true }));
return true; return true;
} }
@@ -206,6 +198,8 @@
button.primary:disabled { background: #8ecdf7; cursor: default; } button.primary:disabled { background: #8ecdf7; cursor: default; }
button.green { background: #17bf63 !important; } button.green { background: #17bf63 !important; }
button.green:hover { background: #15a857 !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; .status { display: none; padding: 12px; border-radius: 10px; font-size: 14px; line-height: 1.5;
white-space: pre-wrap; word-break: break-word; } white-space: pre-wrap; word-break: break-word; }
.status.ok { background: #e8f6fe; border: 1px solid #b4dffc; } .status.ok { background: #e8f6fe; border: 1px solid #b4dffc; }
@@ -216,6 +210,8 @@
.typing-opts.visible { display: flex; } .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-row { display: flex; gap: 8px; }
.btn-row .primary { flex: 1; margin-top: 0; }
`; `;
const fab = document.createElement('button'); const fab = document.createElement('button');
@@ -254,7 +250,10 @@
<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>
<div class="btn-row">
<button class="primary gray" id="ai-copy">📋 Copy</button>
<button class="primary green" id="ai-paste">📥 Dán vào ô reply</button> <button class="primary green" id="ai-paste">📥 Dán vào ô reply</button>
</div>
<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>
@@ -311,6 +310,7 @@
const typingOpts = drawer.querySelector('#ai-typing-opts'); const typingOpts = drawer.querySelector('#ai-typing-opts');
const typingChk = drawer.querySelector('#ai-typing'); const typingChk = drawer.querySelector('#ai-typing');
const pasteBtn = drawer.querySelector('#ai-paste'); const pasteBtn = drawer.querySelector('#ai-paste');
const copyBtn = drawer.querySelector('#ai-copy');
function populateSelect(sel, items, selectedValue) { function populateSelect(sel, items, selectedValue) {
sel.innerHTML = items.map(it => `<option value="${it.value}">${it.text}</option>`).join(''); sel.innerHTML = items.map(it => `<option value="${it.value}">${it.text}</option>`).join('');
@@ -365,6 +365,27 @@
); );
}); });
// COPY BUTTON
copyBtn.addEventListener('click', async () => {
const text = statusEl.textContent;
if (!text || text.startsWith('⏳') || text.startsWith('Lỗi') || text.startsWith('⚠️')) {
const old = copyBtn.textContent;
copyBtn.textContent = '⚠️ Chưa có nội dung!';
setTimeout(() => copyBtn.textContent = old, 1200);
return;
}
try {
await navigator.clipboard.writeText(text);
const old = copyBtn.textContent;
copyBtn.textContent = '✅ Đã copy!';
setTimeout(() => copyBtn.textContent = old, 1200);
} catch (e) {
const old = copyBtn.textContent;
copyBtn.textContent = '❌ Lỗi copy';
setTimeout(() => copyBtn.textContent = old, 1200);
}
});
// PASTE BUTTON // PASTE BUTTON
pasteBtn.addEventListener('click', async () => { pasteBtn.addEventListener('click', async () => {
const text = statusEl.textContent; const text = statusEl.textContent;
@@ -380,7 +401,7 @@
if (typingChk.checked) { if (typingChk.checked) {
pasteBtn.textContent = '⌨️ Đang gõ...'; pasteBtn.textContent = '⌨️ Đang gõ...';
const ok = await simulateHumanTyping(text); const ok = await simulateHumanTyping(text);
if (ok) drawer.classList.remove('open'); // thu sidebar để người dùng thấy reply if (ok) drawer.classList.remove('open');
} else { } else {
await navigator.clipboard.writeText(text); await navigator.clipboard.writeText(text);
alert('✅ Đã copy vào clipboard!\n\nBạn tự dán vào ô reply nhé.'); alert('✅ Đã copy vào clipboard!\n\nBạn tự dán vào ô reply nhé.');
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB