Files
x-poster-client/chrom-ext/content.js
T
2026-05-16 14:02:06 +00:00

664 lines
31 KiB
JavaScript

(() => {
'use strict';
let lastTweetText = '';
let lastSelectedText = '';
let sidebarHost = null;
let isTyping = false;
// ===== DATA =====
const LANGS = [
{ value: 'vi', label: 'Việt' },
{ value: 'en', label: 'English' },
{ value: 'ja', label: 'Nhật' },
{ value: 'ko', label: 'Han' },
{ value: 'cn', label: 'TQ' }
];
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_DEFAULT, ...ANGLE_EMPATHY] : [...ANGLE_DEFAULT];
}
// ===== A. CAPTURE TWEET & SELECTED TEXT =====
document.addEventListener('contextmenu', (e) => {
// Lưu text đang bôi đen (cho translate)
const sel = window.getSelection().toString().trim();
if (sel) lastSelectedText = sel;
// Lưu tweet text
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);
});
// ===== B. LISTENER =====
chrome.runtime.onMessage.addListener((req, sender, sendResponse) => {
if (req.action === 'OPEN_FORM') {
if (!lastTweetText) {
sendResponse({ ok: false, reason: 'no_text' });
return false;
}
openSidebar(lastTweetText);
sendResponse({ ok: true });
return false;
}
if (req.action === 'OPEN_TRANSLATE_FORM') {
openSidebar('');
switchTab('translate');
const s = sidebarHost?.shadowRoot;
if (s) {
const input = s.querySelector('#trans-input');
if (input) input.value = req.text || '';
}
sendResponse({ ok: true });
return false;
}
if (req.action === 'SHOW_RESULT') {
showResult(req.comment, req.model, req.commentTransVi);
sendResponse({ ok: true });
return false;
}
if (req.action === 'SHOW_ERROR') {
showError(req.error);
sendResponse({ ok: true });
return false;
}
if (req.action === 'SHOW_TRANSLATE_RESULT') {
showTranslateResult(req.content);
sendResponse({ ok: true });
return false;
}
if (req.action === 'SHOW_TRANSLATE_ERROR') {
showTranslateError(req.error);
sendResponse({ ok: true });
return false;
}
sendResponse({ ok: false, unknown: true });
return false;
});
// ===== C. TYPING ENGINE =====
async function simulateHumanTyping(fullText) {
let editor =
document.querySelector('[data-testid="tweetTextarea_0"]') ||
document.querySelector('div[contenteditable="true"][role="textbox"]');
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;
}
editor.focus();
editor.click();
editor.textContent = '';
const textNode = document.createTextNode('');
editor.appendChild(textNode);
const sel = window.getSelection();
const range = document.createRange();
range.setStart(textNode, 0);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
for (let i = 0; i < fullText.length; i++) {
const char = fullText[i];
textNode.nodeValue += char;
range.setEnd(textNode, textNode.nodeValue.length);
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
editor.dispatchEvent(new InputEvent('input', {
bubbles: true,
cancelable: true,
inputType: 'insertText',
data: char
}));
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));
}
editor.dispatchEvent(new Event('change', { bubbles: true }));
return true;
}
// ===== D. SIDEBAR UI =====
function switchTab(name) {
if (!sidebarHost) return;
const s = sidebarHost.shadowRoot;
s.querySelectorAll('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === name));
s.querySelectorAll('.tab-content').forEach(c => c.classList.toggle('active', c.id === `tab-${name}`));
}
function openSidebar(tweetText) {
if (sidebarHost) {
// Nếu sidebar đã mở, chỉ cần mở drawer
const drawer = sidebarHost.shadowRoot.querySelector('.drawer');
if (drawer) drawer.classList.add('open');
return;
}
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: 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; }
.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"], textarea { width: 100%; padding: 9px 10px; border-radius: 8px; border: 1px solid #cfd9de;
font-size: 14px; background: #fff; box-sizing: border-box; font-family: inherit; }
textarea { resize: vertical; min-height: 80px; }
.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; }
button.green { background: #17bf63 !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;
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; }
.status.trans { background: #f0fdf4; border: 1px solid #86efac; color: #14532d; }
.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; 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-row { display: flex; gap: 8px; }
.btn-row .primary { flex: 1; margin-top: 0; }
`;
const fab = document.createElement('button');
fab.className = 'fab'; fab.textContent = '🤖'; fab.title = 'AI Comment';
const drawer = document.createElement('div');
drawer.className = 'drawer';
drawer.innerHTML = `
<div class="header"><span>✍️ AI Comment</span><button class="btn-x">✕</button></div>
<div class="tabs">
<button class="tab-btn active" data-tab="comment">Comment</button>
<button class="tab-btn" data-tab="translate">🌐 Dịch</button>
<button class="tab-btn" data-tab="config">⚙️ Config <span class="badge" id="cfg-missing" style="display:none">!</span></button>
</div>
<div class="tab-content active" id="tab-comment">
<div class="tweet-box">${escapeHtml(tweetText)}</div>
<label>Ngôn ngữ đầu ra</label>
<select id="ai-lang">${LANGS.map(l => `<option value="${l.value}">${l.label}</option>`).join('')}</select>
<label>Tone</label>
<select id="ai-tone"></select>
<div class="hint-text" id="ai-tone-hint"></div>
<label>Angle</label>
<select id="ai-angle"></select>
<div class="hint-text" id="ai-angle-hint"></div>
<button class="primary" id="ai-run">🚀 Tạo Comment</button>
<div class="status" id="ai-status"></div>
<div class="status trans" id="ai-trans-vi" style="display:none;"></div>
<div class="copy-hint" id="ai-hint"></div>
<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>
<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>
</div>
<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>
<div class="tab-content" id="tab-translate">
<div style="font-size:13px;color:#536471;margin-bottom:6px;">
Nhập văn bản và chọn ngôn ngữ cần dịch. API translate lấy từ tab Config.
</div>
<label>Văn bản cần dịch</label>
<textarea id="trans-input" placeholder="Nhập hoặc bôi đen rồi chuột phải..."></textarea>
<label>Ngôn ngữ đích</label>
<select id="trans-target">${LANGS.map(l => `<option value="${l.value}">${l.label}</option>`).join('')}</select>
<button class="primary" id="trans-run">🌐 Dịch</button>
<div class="status" id="trans-status"></div>
<div class="copy-hint" id="trans-hint"></div>
<div class="btn-row">
<button class="primary gray" id="trans-copy">📋 Copy</button>
<button class="primary green" id="trans-paste">📥 Dán vào ô reply</button>
</div>
</div>
<div class="tab-content" id="tab-config">
<div style="font-size:13px; color:#536471; margin-bottom:6px;">
Cấu hình API. Nếu Translate URL để trống, sẽ dùng chung Comment URL.
</div>
<label>Comment API URL</label>
<input type="text" id="cfg-url" placeholder="https://api.yoursite.com/v1/generate">
<label>Comment API Key</label>
<input type="password" id="cfg-key" placeholder="sk-...">
<label>Translate API URL (tùy chọn)</label>
<input type="text" id="cfg-trans-url" placeholder="https://api.yoursite.com/v1/translate">
<label>Translate API Key (tùy chọn)</label>
<input type="password" id="cfg-trans-key" placeholder="sk-...">
<div style="display:flex; gap:8px;">
<button class="primary" id="cfg-save" style="flex:1">💾 Lưu</button>
<button class="primary green" id="cfg-test" style="flex:1">🧪 Test đọc</button>
</div>
<div class="status" id="cfg-status"></div>
</div>
`;
shadow.appendChild(style);
shadow.appendChild(fab);
shadow.appendChild(drawer);
// Toggle
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'));
// Tabs
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 =====
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 transViEl= drawer.querySelector('#ai-trans-vi');
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');
const copyBtn = drawer.querySelector('#ai-copy');
function populateSelect(sel, items, selectedValue) {
sel.innerHTML = items.map(it => `<option value="${it.value}">${it.text}</option>`).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...';
transViEl.style.display = 'none'; transViEl.textContent = '';
copyHint.style.display = 'none';
typingOpts.classList.remove('visible');
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;
}
}
);
});
// Comment buttons
copyBtn.addEventListener('click', async () => {
const text = statusEl.textContent;
if (!text || text.startsWith('⏳') || text.startsWith('Lỗi') || text.startsWith('⚠️')) {
flashBtn(copyBtn, '⚠️ Chưa có nội dung!');
return;
}
try {
await navigator.clipboard.writeText(text);
flashBtn(copyBtn, '✅ Đã copy!');
} catch (e) { flashBtn(copyBtn, '❌ Lỗi copy'); }
});
pasteBtn.addEventListener('click', async () => {
const text = statusEl.textContent;
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 (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');
} 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';
}
});
// ===== TRANSLATE TAB =====
const transInput = drawer.querySelector('#trans-input');
const transTarget= drawer.querySelector('#trans-target');
const transRun = drawer.querySelector('#trans-run');
const transStatus= drawer.querySelector('#trans-status');
const transHint = drawer.querySelector('#trans-hint');
const transCopy = drawer.querySelector('#trans-copy');
const transPaste = drawer.querySelector('#trans-paste');
transRun.addEventListener('click', () => {
const text = transInput.value.trim();
const target = transTarget.value;
if (!text) { flashBtn(transRun, '⚠️ Nhập văn bản!'); return; }
transRun.disabled = true;
transStatus.style.display = 'block'; transStatus.className = 'status ok';
transStatus.textContent = '⏳ Đang dịch...';
transHint.style.display = 'none';
chrome.runtime.sendMessage(
{ action: 'TRANSLATE_TEXT', data: { text, target_lang: target } },
() => {
if (chrome.runtime.lastError) {
transStatus.className = 'status err';
transStatus.textContent = 'Lỗi kết nối: ' + chrome.runtime.lastError.message;
transRun.disabled = false;
}
}
);
});
transCopy.addEventListener('click', async () => {
const text = transStatus.textContent;
if (!text || text.startsWith('⏳')) { flashBtn(transCopy, '⚠️ Chưa có!'); return; }
try { await navigator.clipboard.writeText(text); flashBtn(transCopy, '✅ Đã copy!'); }
catch (e) { flashBtn(transCopy, '❌ Lỗi'); }
});
transPaste.addEventListener('click', async () => {
const text = transStatus.textContent;
if (!text || text.startsWith('⏳')) { alert('Chưa có kết quả dịch!'); return; }
if (isTyping) return;
isTyping = true; transPaste.disabled = true;
try {
transPaste.textContent = '⌨️ Đang gõ...';
const ok = await simulateHumanTyping(text);
if (ok) drawer.classList.remove('open');
} catch (err) {
alert('Lỗi khi nhập: ' + err.message);
} finally {
isTyping = false; transPaste.disabled = false;
transPaste.textContent = '📥 Dán vào ô reply';
}
});
// ===== CONFIG TAB =====
const urlIn = drawer.querySelector('#cfg-url');
const keyIn = drawer.querySelector('#cfg-key');
const transUrlIn= drawer.querySelector('#cfg-trans-url');
const transKeyIn= drawer.querySelector('#cfg-trans-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;
}
chrome.storage.local.get(['apiUrl','apiKey','translateUrl','translateKey'], (cfg) => {
if (cfg.apiUrl) urlIn.value = cfg.apiUrl;
if (cfg.apiKey) keyIn.value = cfg.apiKey;
if (cfg.translateUrl) transUrlIn.value = cfg.translateUrl;
if (cfg.translateKey) transKeyIn.value = cfg.translateKey;
cfgBadge.style.display = (!cfg.apiUrl || !cfg.apiKey) ? 'inline-block' : 'none';
});
saveBtn.addEventListener('click', () => {
const url = urlIn.value.trim(), key = keyIn.value.trim();
if (!url || !key) { showCfgStatus('❌ Comment URL và Key không được để trống!', true); return; }
try { new URL(url); } catch { showCfgStatus('❌ URL không hợp lệ', true); return; }
const tUrl = transUrlIn.value.trim() || url;
const tKey = transKeyIn.value.trim() || key;
chrome.storage.local.set({
apiUrl: url, apiKey: key,
translateUrl: tUrl === url ? '' : tUrl,
translateKey: transKeyIn.value.trim()
}, () => {
showCfgStatus('✅ Đã lưu!', false);
cfgBadge.style.display = 'none';
});
});
testBtn.addEventListener('click', () => {
chrome.storage.local.get(['apiUrl','apiKey','translateUrl','translateKey'], (cfg) => {
const tUrl = cfg.translateUrl || cfg.apiUrl || '(trống)';
showCfgStatus(
`Comment: ${cfg.apiUrl || '(trống)'}\n` +
`Translate: ${tUrl}\n` +
`Key: ${cfg.apiKey ? cfg.apiKey.slice(0,8)+'...' : '(trống)'}`,
!cfg.apiUrl || !cfg.apiKey
);
});
});
}
// ===== SHOW RESULT / ERROR =====
function showResult(comment, model='', transVi='') {
if (!sidebarHost) return;
const s = sidebarHost.shadowRoot;
const statusEl = s.querySelector('#ai-status');
const transViEl= s.querySelector('#ai-trans-vi');
const copyHint = s.querySelector('#ai-hint');
const runBtn = s.querySelector('#ai-run');
const drawer = s.querySelector('.drawer');
const typingOpts = s.querySelector('#ai-typing-opts');
console.log({model});
if (statusEl) {
statusEl.style.display = 'block';
statusEl.className = 'status ok';
statusEl.textContent = comment;
}
if (transViEl) {
if (transVi) {
transViEl.style.display = 'block';
transViEl.textContent = '🇻🇳 ' + transVi;
} else {
transViEl.style.display = 'none';
transViEl.textContent = '';
}
}
if (copyHint) { copyHint.style.display = 'block'; copyHint.textContent = model || '📋 Copy kết quả và dán vào ô reply!'; }
if (runBtn) runBtn.disabled = false;
if (drawer) drawer.classList.add('open');
if (typingOpts) typingOpts.classList.add('visible');
}
function showError(msg) {
if (!sidebarHost) return;
const s = sidebarHost.shadowRoot;
const statusEl = s.querySelector('#ai-status');
const transViEl= s.querySelector('#ai-trans-vi');
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 (transViEl) { transViEl.style.display = 'none'; transViEl.textContent = ''; }
if (runBtn) runBtn.disabled = false;
if (drawer) drawer.classList.add('open');
}
function showTranslateResult(content) {
if (!sidebarHost) return;
const s = sidebarHost.shadowRoot;
const status = s.querySelector('#trans-status');
const hint = s.querySelector('#trans-hint');
const runBtn = s.querySelector('#trans-run');
const drawer = s.querySelector('.drawer');
if (status) { status.style.display = 'block'; status.className = 'status ok'; status.textContent = content; }
if (hint) hint.style.display = 'block';
if (runBtn) runBtn.disabled = false;
if (drawer) drawer.classList.add('open');
}
function showTranslateError(msg) {
if (!sidebarHost) return;
const s = sidebarHost.shadowRoot;
const status = s.querySelector('#trans-status');
const runBtn = s.querySelector('#trans-run');
if (status) { status.style.display = 'block'; status.className = 'status err'; status.textContent = msg; }
if (runBtn) runBtn.disabled = false;
}
function removeSidebar() {
if (sidebarHost) { sidebarHost.remove(); sidebarHost = null; }
}
function escapeHtml(text) {
const d = document.createElement('div'); d.textContent = text; return d.innerHTML;
}
function flashBtn(btn, msg) {
const old = btn.textContent;
btn.textContent = msg;
setTimeout(() => btn.textContent = old, 1200);
}
})();