Update
This commit is contained in:
+117
-20
@@ -4,7 +4,7 @@
|
||||
let lastTweetText = '';
|
||||
let sidebarHost = null;
|
||||
|
||||
// ===== DATA =====
|
||||
// ===== DATA (giữ nguyên) =====
|
||||
const LANGS = [
|
||||
{ value: 'ja', label: 'Nhật' },
|
||||
{ value: 'vi', label: 'Việt' },
|
||||
@@ -62,7 +62,7 @@
|
||||
return tone === 'EMPATHETIC' ? [...ANGLE_EMPATHY] : [...ANGLE_DEFAULT];
|
||||
}
|
||||
|
||||
// ===== DOM =====
|
||||
// ===== A. BẮT CHUỘT PHẢI =====
|
||||
document.addEventListener('contextmenu', (e) => {
|
||||
const article = e.target.closest('article');
|
||||
if (!article) { lastTweetText = ''; return; }
|
||||
@@ -88,6 +88,68 @@
|
||||
if (req.action === 'SHOW_ERROR') { showError(req.error); return true; }
|
||||
});
|
||||
|
||||
// ===== HUMAN TYPING SIMULATION =====
|
||||
async function simulateHumanTyping(text) {
|
||||
// X dùng contenteditable div. Tìm ô reply đang mở.
|
||||
let el =
|
||||
document.querySelector('[data-testid="tweetTextarea_0"]') ||
|
||||
document.querySelector('div[contenteditable="true"][role="textbox"]') ||
|
||||
document.querySelector('div[contenteditable="true"][data-text="true"]');
|
||||
|
||||
if (!el) {
|
||||
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();
|
||||
|
||||
// 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);
|
||||
range.collapse(true);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
}
|
||||
|
||||
// Kích hoạt React re-render
|
||||
el.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;
|
||||
|
||||
await new Promise(r => setTimeout(r, delay));
|
||||
}
|
||||
|
||||
// Final change event cho chắc
|
||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
return true;
|
||||
}
|
||||
|
||||
// ===== SIDEBAR =====
|
||||
function openSidebar(tweetText) {
|
||||
removeSidebar();
|
||||
@@ -109,7 +171,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: 400px; max-width: 100vw; height: 100vh; background: #fff; color: #0f1419;
|
||||
.drawer { position: fixed; right: 0; top: 0; width: 442px; 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; }
|
||||
@@ -138,6 +200,12 @@
|
||||
.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; }
|
||||
.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');
|
||||
@@ -171,6 +239,18 @@
|
||||
|
||||
<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>
|
||||
<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 -->
|
||||
@@ -204,7 +284,7 @@
|
||||
drawer.querySelector('.btn-x').addEventListener('click', () => drawer.classList.remove('open'));
|
||||
requestAnimationFrame(() => drawer.classList.add('open'));
|
||||
|
||||
// Tab switching
|
||||
// Tabs
|
||||
const tabBtns = drawer.querySelectorAll('.tab-btn');
|
||||
const tabContents = drawer.querySelectorAll('.tab-content');
|
||||
tabBtns.forEach(btn => {
|
||||
@@ -225,6 +305,9 @@
|
||||
const runBtn = drawer.querySelector('#ai-run');
|
||||
const statusEl = drawer.querySelector('#ai-status');
|
||||
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');
|
||||
|
||||
function populateSelect(sel, items, selectedValue) {
|
||||
sel.innerHTML = items.map(it => `<option value="${it.value}">${it.text}</option>`).join('');
|
||||
@@ -264,6 +347,7 @@
|
||||
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';
|
||||
typingOpts.classList.remove('visible');
|
||||
chrome.runtime.sendMessage(
|
||||
{ action: 'GENERATE_COMMENT', data: { text: tweetText, lang, tone, angle } },
|
||||
() => {
|
||||
@@ -276,7 +360,27 @@
|
||||
);
|
||||
});
|
||||
|
||||
// ===== CONFIG TAB LOGIC =====
|
||||
// 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!');
|
||||
return;
|
||||
}
|
||||
if (typingChk.checked) {
|
||||
pasteBtn.disabled = true;
|
||||
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
|
||||
} else {
|
||||
await navigator.clipboard.writeText(text);
|
||||
alert('✅ Đã copy vào clipboard! Bạn tự dán nhé.');
|
||||
}
|
||||
});
|
||||
|
||||
// ===== CONFIG TAB (giữ nguyên) =====
|
||||
const urlIn = drawer.querySelector('#cfg-url');
|
||||
const keyIn = drawer.querySelector('#cfg-key');
|
||||
const saveBtn = drawer.querySelector('#cfg-save');
|
||||
@@ -289,31 +393,20 @@
|
||||
cfgStatus.className = 'status ' + (isErr ? 'err' : 'ok');
|
||||
cfgStatus.textContent = msg;
|
||||
}
|
||||
|
||||
// Đọc config hiện tại
|
||||
chrome.storage.local.get(['apiUrl', 'apiKey'], (cfg) => {
|
||||
if (cfg.apiUrl) urlIn.value = cfg.apiUrl;
|
||||
if (cfg.apiKey) keyIn.value = cfg.apiKey;
|
||||
cfgBadge.style.display = (!cfg.apiUrl || !cfg.apiKey) ? 'inline-block' : 'none';
|
||||
});
|
||||
|
||||
saveBtn.addEventListener('click', () => {
|
||||
const url = urlIn.value.trim();
|
||||
const key = keyIn.value.trim();
|
||||
if (!url || !key) {
|
||||
showCfgStatus('❌ URL và Key không được để trống!', true);
|
||||
return;
|
||||
}
|
||||
try { new URL(url); } catch {
|
||||
showCfgStatus('❌ URL không hợp lệ (cần có https://)', true);
|
||||
return;
|
||||
}
|
||||
const url = urlIn.value.trim(), key = keyIn.value.trim();
|
||||
if (!url || !key) { showCfgStatus('❌ URL và Key không được để trống!', true); return; }
|
||||
try { new URL(url); } catch { showCfgStatus('❌ URL không hợp lệ', true); return; }
|
||||
chrome.storage.local.set({ apiUrl: url, apiKey: key }, () => {
|
||||
showCfgStatus('✅ Đã lưu! Bạn có thể đóng tab này và bấm Tạo Comment.', false);
|
||||
showCfgStatus('✅ Đã lưu!', false);
|
||||
cfgBadge.style.display = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
testBtn.addEventListener('click', () => {
|
||||
chrome.storage.local.get(['apiUrl', 'apiKey'], (cfg) => {
|
||||
showCfgStatus(`URL: ${cfg.apiUrl || '(trống)'}\nKey: ${cfg.apiKey ? cfg.apiKey.slice(0,8)+'...' : '(trống)'}`, !cfg.apiUrl || !cfg.apiKey);
|
||||
@@ -329,10 +422,14 @@
|
||||
const copyHint = s.querySelector('#ai-hint');
|
||||
const runBtn = s.querySelector('#ai-run');
|
||||
const drawer = s.querySelector('.drawer');
|
||||
const typingOpts = s.querySelector('#ai-typing-opts');
|
||||
|
||||
if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'status ok'; statusEl.textContent = comment; }
|
||||
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) {
|
||||
if (!sidebarHost) return;
|
||||
|
||||
Reference in New Issue
Block a user