From 977d02ee2edc8d9c4a7912f2137cbc82d3aacf21 Mon Sep 17 00:00:00 2001 From: NAME Date: Fri, 15 May 2026 14:22:04 +0000 Subject: [PATCH] Update --- chrom-ext/content.bk | 353 +++++++++++++++++++++++++++++++++++++++++++ chrom-ext/content.js | 137 ++++++++++++++--- 2 files changed, 470 insertions(+), 20 deletions(-) create mode 100644 chrom-ext/content.bk diff --git a/chrom-ext/content.bk b/chrom-ext/content.bk new file mode 100644 index 0000000..5af031c --- /dev/null +++ b/chrom-ext/content.bk @@ -0,0 +1,353 @@ +(() => { + 'use strict'; + + let lastTweetText = ''; + let sidebarHost = null; + + // ===== DATA ===== + const LANGS = [ + { value: 'ja', label: 'Nhật' }, + { value: 'vi', label: 'Việt' }, + { value: 'en', label: 'English' }, + { value: 'ko', label: 'Hàn' }, + { value: 'cn', label: 'Trung' } + ]; + + const TONE_BASE = [ + { value: 'PROFESSIONAL', text: 'chuyên nghiệp, rõ ràng, đáng tin cậy' }, + { value: 'CASUAL', text: 'Giản dị, thân thiện' }, + { value: 'HYPE', text: 'Hype — Hào hứng, tràn đầy năng lượng' }, + { value: 'URGENT', text: 'urgent' }, + { value: 'HUMOROUS', text: 'Dí dỏm, hài hước' }, + { value: 'INFORMATIVE', text: 'Thông tin, chính xác' }, + { value: 'EMPATHETIC', text: 'empathetic — Đồng cảm, thấu hiểu cảm xúc' }, + { value: 'PROVOCATIVE', text: 'provocative — Gợi mở suy nghĩ, thách thức giả định' }, + { value: 'AUTHORITATIVE', text: 'authoritative — Tự tin, uy quyền' }, + { value: 'SPICY', text: 'spicy — Tự tin, hơi đối đầu, chỉ thẳng' } + ]; + + const TONE_JA_ONLY = [ + { value: 'AGGRESSIVE', text: 'aggressive — Cục súc, attack ideas mạnh' }, + { value: 'PROFANE', text: 'profane — Nói tục thoải mái, raw' }, + { value: 'INFLAMMATORY', text: 'inflammatory — Kích động cao' }, + { value: 'SAVAGE', text: 'savage — Chửi tục OK, sass tối đa' } + ]; + + const ANGLE_DEFAULT = [ + { value: 'AGREE', text: 'Đồng ý' }, + { value: 'CHALLENGE', text: 'Không đồng ý' }, + { value: 'ADD_INFO', text: 'Thêm thông tin liên quan hữu ích' }, + { value: 'FUNNY', text: 'Hóm hỉnh, hài hước nhẹ nhàng' }, + { value: 'QUESTION', text: 'Đặt câu hỏi tiếp theo thông minh' }, + { value: 'RELATE', text: 'Chia sẻ trải nghiệm cá nhân tương tự' }, + { value: 'DEVIL_ADVOCATE', text: 'Phản biện công bằng, không thù địch' }, + { value: 'EXPAND', text: 'Phân tích sâu 1 điểm' }, + { value: 'VALIDATE', text: 'Khẳng định bằng chứng mạnh mẽ' }, + { value: 'CTA', text: 'Kêu gọi hành động nhẹ nhàng' } + ]; + + const ANGLE_EMPATHY = [ + { value: 'WISH_RECOVERY', text: 'Chúc hồi phục' }, + { value: 'TRIBUTE', text: 'Tưởng nhớ / RIP' }, + { value: 'SOLIDARITY', text: 'Đồng lòng / Đứng cùng' }, + { value: 'PERSONAL_SUPPORT', text: 'Hỗ trợ cá nhân' }, + { value: 'SHARED_GRIEF', text: 'Cùng nỗi buồn' } + ]; + + function getTones(lang) { + if (lang === 'ja') return [...TONE_BASE, ...TONE_JA_ONLY]; + return [...TONE_BASE]; + } + function getAngles(tone) { + return tone === 'EMPATHETIC' ? [...ANGLE_EMPATHY] : [...ANGLE_DEFAULT]; + } + + // ===== DOM ===== + document.addEventListener('contextmenu', (e) => { + const article = e.target.closest('article'); + if (!article) { lastTweetText = ''; return; } + const textEl = + 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); + }); + + 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; + } + openSidebar(lastTweetText); + return true; + } + if (req.action === 'SHOW_RESULT') { showResult(req.comment); return true; } + if (req.action === 'SHOW_ERROR') { showError(req.error); return true; } + }); + + // ===== SIDEBAR ===== + function openSidebar(tweetText) { + removeSidebar(); + + const host = document.createElement('div'); + host.id = 'x-ai-sidebar-host'; + Object.assign(host.style, { + position: 'fixed', top: '0', left: '0', width: '0', height: '0', + zIndex: '2147483647', overflow: 'visible', pointerEvents: 'none' + }); + document.body.appendChild(host); + sidebarHost = host; + + const shadow = host.attachShadow({ mode: 'open' }); + const style = document.createElement('style'); + style.textContent = ` + .fab { position: fixed; right: 24px; bottom: 24px; width: 56px; height: 56px; border-radius: 50%; + background: #1d9bf0; color: #fff; font-size: 24px; display: flex; align-items: center; justify-content: center; + 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; + 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; } + .drawer.open { transform: translateX(0); } + .header { padding: 14px 18px; border-bottom: 1px solid #eff3f4; display: flex; justify-content: space-between; align-items: center; + font-weight: 700; font-size: 16px; } + .btn-x { background: none; border: none; font-size: 20px; cursor: pointer; color: #536471; padding: 4px; line-height: 1; } + .tabs { display: flex; border-bottom: 1px solid #eff3f4; } + .tab-btn { flex: 1; padding: 10px; border: none; background: none; cursor: pointer; font-size: 14px; color: #536471; + border-bottom: 2px solid transparent; font-weight: 600; } + .tab-btn.active { color: #1d9bf0; border-bottom-color: #1d9bf0; } + .tab-content { padding: 16px 18px; flex: 1; overflow-y: auto; display: none; flex-direction: column; gap: 10px; } + .tab-content.active { display: flex; } + .tweet-box { background: #f7f9f9; border: 1px solid #cfd9de; border-radius: 10px; padding: 10px; font-size: 13px; + line-height: 1.4; max-height: 150px; overflow-y: auto; color: #333; white-space: pre-wrap; word-break: break-word; } + label { font-size: 12px; font-weight: 700; color: #536471; text-transform: uppercase; letter-spacing: .3px; margin-top: 4px; } + select, input[type="text"], input[type="password"] { width: 100%; padding: 9px 10px; border-radius: 8px; border: 1px solid #cfd9de; + font-size: 14px; background: #fff; box-sizing: border-box; } + .hint-text { font-size: 12px; color: #888; line-height: 1.4; font-style: italic; } + button.primary { width: 100%; padding: 10px; border-radius: 9999px; border: none; background: #1d9bf0; color: #fff; + 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; } + .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; } + `; + + const fab = document.createElement('button'); + fab.className = 'fab'; fab.textContent = '🤖'; fab.title = 'AI Comment'; + + const drawer = document.createElement('div'); + drawer.className = 'drawer'; + drawer.innerHTML = ` +
✍️ AI Comment
+
+ + +
+ + +
+
${escapeHtml(tweetText)}
+ + + + + + +
+ + + +
+ + + +
+
📋 Copy kết quả và dán vào ô reply!
+
+ + +
+
+ Nhập API của bạn. Dữ liệu được lưu trên trình duyệt này. +
+ + + + + + + +
+ + +
+ +
+
+ `; + + shadow.appendChild(style); + shadow.appendChild(fab); + shadow.appendChild(drawer); + + // Toggle drawer + const toggleDrawer = () => drawer.classList.toggle('open'); + fab.addEventListener('click', toggleDrawer); + drawer.querySelector('.btn-x').addEventListener('click', () => drawer.classList.remove('open')); + requestAnimationFrame(() => drawer.classList.add('open')); + + // Tab switching + const tabBtns = drawer.querySelectorAll('.tab-btn'); + const tabContents = drawer.querySelectorAll('.tab-content'); + tabBtns.forEach(btn => { + btn.addEventListener('click', () => { + tabBtns.forEach(b => b.classList.remove('active')); + tabContents.forEach(c => c.classList.remove('active')); + btn.classList.add('active'); + drawer.querySelector(`#tab-${btn.dataset.tab}`).classList.add('active'); + }); + }); + + // ===== COMMENT TAB LOGIC ===== + const langSel = drawer.querySelector('#ai-lang'); + const toneSel = drawer.querySelector('#ai-tone'); + const toneHint = drawer.querySelector('#ai-tone-hint'); + const angleSel = drawer.querySelector('#ai-angle'); + const angleHint= drawer.querySelector('#ai-angle-hint'); + const runBtn = drawer.querySelector('#ai-run'); + const statusEl = drawer.querySelector('#ai-status'); + const copyHint = drawer.querySelector('#ai-hint'); + + function populateSelect(sel, items, selectedValue) { + sel.innerHTML = items.map(it => ``).join(''); + if (selectedValue && items.find(i => i.value === selectedValue)) sel.value = selectedValue; + } + function updateTone(lang, keepValueIfValid) { + const items = getTones(lang); + const old = toneSel.value; + populateSelect(toneSel, items, keepValueIfValid && items.find(i => i.value === old) ? old : null); + onToneChange(); + } + function onToneChange() { + const lang = langSel.value; + const tone = toneSel.value; + const tItem = getTones(lang).find(i => i.value === tone); + toneHint.textContent = tItem ? tItem.text : ''; + const oldAngle = angleSel.value; + const aItems = getAngles(tone); + populateSelect(angleSel, aItems, aItems.find(i => i.value === oldAngle) ? oldAngle : null); + onAngleChange(); + } + function onAngleChange() { + const tone = toneSel.value; + const angle = angleSel.value; + const aItem = getAngles(tone).find(i => i.value === angle); + angleHint.textContent = aItem ? aItem.text : ''; + } + + langSel.addEventListener('change', () => updateTone(langSel.value, true)); + toneSel.addEventListener('change', onToneChange); + angleSel.addEventListener('change', onAngleChange); + updateTone(langSel.value, false); + + runBtn.addEventListener('click', () => { + const lang = langSel.value; + 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'; + chrome.runtime.sendMessage( + { action: 'GENERATE_COMMENT', data: { text: tweetText, lang, tone, angle } }, + () => { + if (chrome.runtime.lastError) { + statusEl.className = 'status err'; + statusEl.textContent = 'Lỗi kết nối: ' + chrome.runtime.lastError.message; + runBtn.disabled = false; + } + } + ); + }); + + // ===== CONFIG TAB LOGIC ===== + const urlIn = drawer.querySelector('#cfg-url'); + const keyIn = drawer.querySelector('#cfg-key'); + const saveBtn = drawer.querySelector('#cfg-save'); + const testBtn = drawer.querySelector('#cfg-test'); + const cfgStatus= drawer.querySelector('#cfg-status'); + const cfgBadge = drawer.querySelector('#cfg-missing'); + + function showCfgStatus(msg, isErr) { + cfgStatus.style.display = 'block'; + 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; + } + 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); + 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); + }); + }); + } + + // ===== RESULT / ERROR ===== + function showResult(comment) { + if (!sidebarHost) return; + const s = sidebarHost.shadowRoot; + const statusEl = s.querySelector('#ai-status'); + const copyHint = s.querySelector('#ai-hint'); + const runBtn = s.querySelector('#ai-run'); + const drawer = s.querySelector('.drawer'); + 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'); + } + function showError(msg) { + if (!sidebarHost) return; + const s = sidebarHost.shadowRoot; + const statusEl = s.querySelector('#ai-status'); + const runBtn = s.querySelector('#ai-run'); + const drawer = s.querySelector('.drawer'); + if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'status err'; statusEl.textContent = msg; } + if (runBtn) runBtn.disabled = false; + if (drawer) drawer.classList.add('open'); + } + function removeSidebar() { + if (sidebarHost) { sidebarHost.remove(); sidebarHost = null; } + } + function escapeHtml(text) { + const d = document.createElement('div'); d.textContent = text; return d.innerHTML; + } +})(); \ No newline at end of file diff --git a/chrom-ext/content.js b/chrom-ext/content.js index 5af031c..905ab5e 100644 --- a/chrom-ext/content.js +++ b/chrom-ext/content.js @@ -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 @@
📋 Copy kết quả và dán vào ô reply!
+ + +
+ + +
+ 💡 Nếu chưa mở ô reply, hãy bấm Reply trước! +
+
@@ -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 => ``).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;