Update
This commit is contained in:
+65
-14
@@ -1,6 +1,6 @@
|
|||||||
console.log('[XAI Background] ✅ Service worker started');
|
console.log('[XAI Background] ✅ Service worker started');
|
||||||
|
|
||||||
// ===== MENU =====
|
// ===== TẠO MENU =====
|
||||||
chrome.runtime.onInstalled.addListener(() => {
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
chrome.contextMenus.create({
|
chrome.contextMenus.create({
|
||||||
id: 'writeComment',
|
id: 'writeComment',
|
||||||
@@ -8,25 +8,43 @@ chrome.runtime.onInstalled.addListener(() => {
|
|||||||
contexts: ['all'],
|
contexts: ['all'],
|
||||||
documentUrlPatterns: ['https://x.com/*']
|
documentUrlPatterns: ['https://x.com/*']
|
||||||
});
|
});
|
||||||
|
chrome.contextMenus.create({
|
||||||
|
id: 'translateText',
|
||||||
|
title: '🌐 Dịch văn bản',
|
||||||
|
contexts: ['selection'],
|
||||||
|
documentUrlPatterns: ['https://x.com/*', 'http://*/*', 'https://*/*']
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// FIRE & FORGET — không đợi content trả lời
|
// ===== CONTEXT MENU CLICK =====
|
||||||
chrome.contextMenus.onClicked.addListener((info, tab) => {
|
chrome.contextMenus.onClicked.addListener((info, tab) => {
|
||||||
if (info.menuItemId !== 'writeComment' || !tab?.id) return;
|
if (!tab?.id) return;
|
||||||
|
|
||||||
|
if (info.menuItemId === 'writeComment') {
|
||||||
chrome.tabs.sendMessage(tab.id, { action: 'OPEN_FORM' }).catch(() => {});
|
chrome.tabs.sendMessage(tab.id, { action: 'OPEN_FORM' }).catch(() => {});
|
||||||
|
}
|
||||||
|
if (info.menuItemId === 'translateText' && info.selectionText) {
|
||||||
|
chrome.tabs.sendMessage(tab.id, { action: 'OPEN_TRANSLATE_FORM', text: info.selectionText }).catch(() => {});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// LUÔN GỌI sendResponse(), KHÔNG BAO GIỜ return true
|
// ===== MESSAGE LISTENER =====
|
||||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||||
if (request.action === 'GENERATE_COMMENT') {
|
if (request.action === 'GENERATE_COMMENT') {
|
||||||
handleGenerate(request.data, sender.tab.id);
|
handleGenerate(request.data, sender.tab.id);
|
||||||
sendResponse({ received: true });
|
sendResponse({ received: true });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (request.action === 'TRANSLATE_TEXT') {
|
||||||
|
handleTranslate(request.data, sender.tab.id);
|
||||||
|
sendResponse({ received: true });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
sendResponse({ unknown: true });
|
sendResponse({ unknown: true });
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===== COMMENT API =====
|
||||||
async function handleGenerate({ text, lang, tone, angle }, tabId) {
|
async function handleGenerate({ text, lang, tone, angle }, tabId) {
|
||||||
const cfg = await chrome.storage.local.get(['apiUrl', 'apiKey']);
|
const cfg = await chrome.storage.local.get(['apiUrl', 'apiKey']);
|
||||||
if (!cfg.apiUrl || !cfg.apiKey) {
|
if (!cfg.apiUrl || !cfg.apiKey) {
|
||||||
@@ -38,26 +56,59 @@ async function handleGenerate({ text, lang, tone, angle }, tabId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload = {
|
|
||||||
originalPost: text,
|
|
||||||
language: lang,
|
|
||||||
tone: tone?tone.toLowerCase():undefined,
|
|
||||||
angle: angle?angle.toLowerCase():undefined,
|
|
||||||
};
|
|
||||||
const res = await fetch(cfg.apiUrl, {
|
const res = await fetch(cfg.apiUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${cfg.apiKey}`
|
'Authorization': `Bearer ${cfg.apiKey}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify(payload)
|
body: JSON.stringify({ tweet_text: text, lang, tone, angle })
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
const comment = data.comment || data.text || JSON.stringify(data);
|
|
||||||
const model = data.model || '';
|
await chrome.tabs.sendMessage(tabId, {
|
||||||
await chrome.tabs.sendMessage(tabId, { action: 'SHOW_RESULT', comment , model}).catch(() => {});
|
action: 'SHOW_RESULT',
|
||||||
|
comment: data.comment || data.text || JSON.stringify(data),
|
||||||
|
model: data.model || '',
|
||||||
|
commentTransVi: data.commentTransVi || ''
|
||||||
|
}).catch(() => {});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
await chrome.tabs.sendMessage(tabId, { action: 'SHOW_ERROR', error: err.message }).catch(() => {});
|
await chrome.tabs.sendMessage(tabId, { action: 'SHOW_ERROR', error: err.message }).catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===== TRANSLATE API =====
|
||||||
|
async function handleTranslate({ text, target_lang }, tabId) {
|
||||||
|
const cfg = await chrome.storage.local.get(['apiUrl','apiKey','translateUrl','translateKey']);
|
||||||
|
const url = cfg.translateUrl || cfg.apiUrl;
|
||||||
|
const key = cfg.translateKey || cfg.apiKey;
|
||||||
|
|
||||||
|
if (!url || !key) {
|
||||||
|
await chrome.tabs.sendMessage(tabId, {
|
||||||
|
action: 'SHOW_TRANSLATE_ERROR',
|
||||||
|
error: '⚠️ Chưa cấu hình API.\n\n👉 Vào tab ⚙️ Config để nhập URL và Key.'
|
||||||
|
}).catch(() => {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${key}`
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ text, target_lang })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
|
||||||
|
await chrome.tabs.sendMessage(tabId, {
|
||||||
|
action: 'SHOW_TRANSLATE_RESULT',
|
||||||
|
content: data.content || data.text || data.translation || JSON.stringify(data)
|
||||||
|
}).catch(() => {});
|
||||||
|
} catch (err) {
|
||||||
|
await chrome.tabs.sendMessage(tabId, { action: 'SHOW_TRANSLATE_ERROR', error: err.message }).catch(() => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
+212
-40
@@ -2,8 +2,9 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
let lastTweetText = '';
|
let lastTweetText = '';
|
||||||
|
let lastSelectedText = '';
|
||||||
let sidebarHost = null;
|
let sidebarHost = null;
|
||||||
let isTyping = false; // chặn bấm paste nhiều lần
|
let isTyping = false;
|
||||||
|
|
||||||
// ===== DATA =====
|
// ===== DATA =====
|
||||||
const LANGS = [
|
const LANGS = [
|
||||||
@@ -63,8 +64,13 @@
|
|||||||
return tone === 'EMPATHETIC' ? [...ANGLE_DEFAULT, ...ANGLE_EMPATHY] : [...ANGLE_DEFAULT];
|
return tone === 'EMPATHETIC' ? [...ANGLE_DEFAULT, ...ANGLE_EMPATHY] : [...ANGLE_DEFAULT];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== A. CAPTURE TWEET =====
|
// ===== A. CAPTURE TWEET & SELECTED TEXT =====
|
||||||
document.addEventListener('contextmenu', (e) => {
|
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');
|
const article = e.target.closest('article');
|
||||||
if (!article) { lastTweetText = ''; return; }
|
if (!article) { lastTweetText = ''; return; }
|
||||||
const textEl =
|
const textEl =
|
||||||
@@ -74,7 +80,7 @@
|
|||||||
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 =====
|
// ===== B. LISTENER =====
|
||||||
chrome.runtime.onMessage.addListener((req, sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((req, sender, sendResponse) => {
|
||||||
if (req.action === 'OPEN_FORM') {
|
if (req.action === 'OPEN_FORM') {
|
||||||
if (!lastTweetText) {
|
if (!lastTweetText) {
|
||||||
@@ -85,8 +91,19 @@
|
|||||||
sendResponse({ ok: true });
|
sendResponse({ ok: true });
|
||||||
return false;
|
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') {
|
if (req.action === 'SHOW_RESULT') {
|
||||||
showResult(req.comment, req.model);
|
showResult(req.comment, req.model, req.commentTransVi);
|
||||||
sendResponse({ ok: true });
|
sendResponse({ ok: true });
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -95,6 +112,16 @@
|
|||||||
sendResponse({ ok: true });
|
sendResponse({ ok: true });
|
||||||
return false;
|
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 });
|
sendResponse({ ok: false, unknown: true });
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
@@ -152,8 +179,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ===== D. SIDEBAR UI =====
|
// ===== 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) {
|
function openSidebar(tweetText) {
|
||||||
removeSidebar();
|
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');
|
const host = document.createElement('div');
|
||||||
host.id = 'x-ai-sidebar-host';
|
host.id = 'x-ai-sidebar-host';
|
||||||
@@ -189,8 +228,9 @@
|
|||||||
.tweet-box { background: #f7f9f9; border: 1px solid #cfd9de; border-radius: 10px; padding: 10px; font-size: 13px;
|
.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; }
|
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; }
|
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;
|
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-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; }
|
.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;
|
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; }
|
font-weight: 700; font-size: 15px; cursor: pointer; margin-top: 6px; }
|
||||||
@@ -204,6 +244,7 @@
|
|||||||
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; }
|
||||||
.status.err { background: #ffeaea; border: 1px solid #ffc5c5; color: #b00; }
|
.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; }
|
.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; }
|
.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 { margin-top: 8px; padding-top: 10px; border-top: 1px solid #eff3f4; display: none; flex-direction: column; gap: 8px; }
|
||||||
@@ -223,6 +264,7 @@
|
|||||||
<div class="header"><span>✍️ AI Comment</span><button class="btn-x">✕</button></div>
|
<div class="header"><span>✍️ AI Comment</span><button class="btn-x">✕</button></div>
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<button class="tab-btn active" data-tab="comment">Comment</button>
|
<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>
|
<button class="tab-btn" data-tab="config">⚙️ Config <span class="badge" id="cfg-missing" style="display:none">!</span></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -243,7 +285,8 @@
|
|||||||
<button class="primary" id="ai-run">🚀 Tạo Comment</button>
|
<button class="primary" id="ai-run">🚀 Tạo Comment</button>
|
||||||
|
|
||||||
<div class="status" id="ai-status"></div>
|
<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>
|
<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">
|
<div class="typing-opts" id="ai-typing-opts">
|
||||||
<label class="check-row">
|
<label class="check-row">
|
||||||
@@ -260,14 +303,38 @@
|
|||||||
</div>
|
</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 class="tab-content" id="tab-config">
|
||||||
<div style="font-size:13px; color:#536471; margin-bottom:6px;">
|
<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.
|
Cấu hình API. Nếu Translate URL để trống, sẽ dùng chung Comment URL.
|
||||||
</div>
|
</div>
|
||||||
<label>API URL</label>
|
<label>Comment API URL</label>
|
||||||
<input type="text" id="cfg-url" placeholder="https://api.yoursite.com/v1/generate">
|
<input type="text" id="cfg-url" placeholder="https://api.yoursite.com/v1/generate">
|
||||||
<label>API Key</label>
|
<label>Comment API Key</label>
|
||||||
<input type="password" id="cfg-key" placeholder="sk-...">
|
<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;">
|
<div style="display:flex; gap:8px;">
|
||||||
<button class="primary" id="cfg-save" style="flex:1">💾 Lưu</button>
|
<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>
|
<button class="primary green" id="cfg-test" style="flex:1">🧪 Test đọc</button>
|
||||||
@@ -306,6 +373,7 @@
|
|||||||
const angleHint= drawer.querySelector('#ai-angle-hint');
|
const angleHint= drawer.querySelector('#ai-angle-hint');
|
||||||
const runBtn = drawer.querySelector('#ai-run');
|
const runBtn = drawer.querySelector('#ai-run');
|
||||||
const statusEl = drawer.querySelector('#ai-status');
|
const statusEl = drawer.querySelector('#ai-status');
|
||||||
|
const transViEl= drawer.querySelector('#ai-trans-vi');
|
||||||
const copyHint = drawer.querySelector('#ai-hint');
|
const copyHint = drawer.querySelector('#ai-hint');
|
||||||
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');
|
||||||
@@ -350,7 +418,9 @@
|
|||||||
const angle = angleSel.value;
|
const angle = angleSel.value;
|
||||||
runBtn.disabled = true;
|
runBtn.disabled = true;
|
||||||
statusEl.style.display = 'block'; statusEl.className = 'status ok';
|
statusEl.style.display = 'block'; statusEl.className = 'status ok';
|
||||||
statusEl.textContent = '⏳ Đang gọi API...'; copyHint.style.display = 'none';
|
statusEl.textContent = '⏳ Đang gọi API...';
|
||||||
|
transViEl.style.display = 'none'; transViEl.textContent = '';
|
||||||
|
copyHint.style.display = 'none';
|
||||||
typingOpts.classList.remove('visible');
|
typingOpts.classList.remove('visible');
|
||||||
|
|
||||||
chrome.runtime.sendMessage(
|
chrome.runtime.sendMessage(
|
||||||
@@ -365,28 +435,19 @@
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// COPY BUTTON
|
// Comment buttons
|
||||||
copyBtn.addEventListener('click', async () => {
|
copyBtn.addEventListener('click', async () => {
|
||||||
const text = statusEl.textContent;
|
const text = statusEl.textContent;
|
||||||
if (!text || text.startsWith('⏳') || text.startsWith('Lỗi') || text.startsWith('⚠️')) {
|
if (!text || text.startsWith('⏳') || text.startsWith('Lỗi') || text.startsWith('⚠️')) {
|
||||||
const old = copyBtn.textContent;
|
flashBtn(copyBtn, '⚠️ Chưa có nội dung!');
|
||||||
copyBtn.textContent = '⚠️ Chưa có nội dung!';
|
|
||||||
setTimeout(() => copyBtn.textContent = old, 1200);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
const old = copyBtn.textContent;
|
flashBtn(copyBtn, '✅ Đã copy!');
|
||||||
copyBtn.textContent = '✅ Đã copy!';
|
} catch (e) { flashBtn(copyBtn, '❌ Lỗi copy'); }
|
||||||
setTimeout(() => copyBtn.textContent = old, 1200);
|
|
||||||
} catch (e) {
|
|
||||||
const old = copyBtn.textContent;
|
|
||||||
copyBtn.textContent = '❌ Lỗi copy';
|
|
||||||
setTimeout(() => copyBtn.textContent = old, 1200);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// PASTE BUTTON
|
|
||||||
pasteBtn.addEventListener('click', async () => {
|
pasteBtn.addEventListener('click', async () => {
|
||||||
const text = statusEl.textContent;
|
const text = statusEl.textContent;
|
||||||
if (!text || text.startsWith('⏳') || text.startsWith('Lỗi') || text.startsWith('⚠️')) {
|
if (!text || text.startsWith('⏳') || text.startsWith('Lỗi') || text.startsWith('⚠️')) {
|
||||||
@@ -394,9 +455,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isTyping) return;
|
if (isTyping) return;
|
||||||
isTyping = true;
|
isTyping = true; pasteBtn.disabled = true;
|
||||||
pasteBtn.disabled = true;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (typingChk.checked) {
|
if (typingChk.checked) {
|
||||||
pasteBtn.textContent = '⌨️ Đang gõ...';
|
pasteBtn.textContent = '⌨️ Đang gõ...';
|
||||||
@@ -410,18 +469,73 @@
|
|||||||
console.error('Typing error:', err);
|
console.error('Typing error:', err);
|
||||||
alert('Lỗi khi nhập: ' + err.message);
|
alert('Lỗi khi nhập: ' + err.message);
|
||||||
} finally {
|
} finally {
|
||||||
isTyping = false;
|
isTyping = false; pasteBtn.disabled = false;
|
||||||
pasteBtn.disabled = false;
|
|
||||||
pasteBtn.textContent = '📥 Dán vào ô reply';
|
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 =====
|
// ===== CONFIG TAB =====
|
||||||
const urlIn = drawer.querySelector('#cfg-url');
|
const urlIn = drawer.querySelector('#cfg-url');
|
||||||
const keyIn = drawer.querySelector('#cfg-key');
|
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 saveBtn = drawer.querySelector('#cfg-save');
|
||||||
const testBtn = drawer.querySelector('#cfg-test');
|
const testBtn = drawer.querySelector('#cfg-test');
|
||||||
const cfgStatus= drawer.querySelector('#cfg-status');
|
const cfgStatus = drawer.querySelector('#cfg-status');
|
||||||
const cfgBadge = drawer.querySelector('#cfg-missing');
|
const cfgBadge = drawer.querySelector('#cfg-missing');
|
||||||
|
|
||||||
function showCfgStatus(msg, isErr) {
|
function showCfgStatus(msg, isErr) {
|
||||||
@@ -429,32 +543,50 @@
|
|||||||
cfgStatus.className = 'status ' + (isErr ? 'err' : 'ok');
|
cfgStatus.className = 'status ' + (isErr ? 'err' : 'ok');
|
||||||
cfgStatus.textContent = msg;
|
cfgStatus.textContent = msg;
|
||||||
}
|
}
|
||||||
chrome.storage.local.get(['apiUrl', 'apiKey'], (cfg) => {
|
|
||||||
|
chrome.storage.local.get(['apiUrl','apiKey','translateUrl','translateKey'], (cfg) => {
|
||||||
if (cfg.apiUrl) urlIn.value = cfg.apiUrl;
|
if (cfg.apiUrl) urlIn.value = cfg.apiUrl;
|
||||||
if (cfg.apiKey) keyIn.value = cfg.apiKey;
|
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';
|
cfgBadge.style.display = (!cfg.apiUrl || !cfg.apiKey) ? 'inline-block' : 'none';
|
||||||
});
|
});
|
||||||
|
|
||||||
saveBtn.addEventListener('click', () => {
|
saveBtn.addEventListener('click', () => {
|
||||||
const url = urlIn.value.trim(), key = keyIn.value.trim();
|
const url = urlIn.value.trim(), key = keyIn.value.trim();
|
||||||
if (!url || !key) { showCfgStatus('❌ URL và Key không được để trống!', true); return; }
|
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; }
|
try { new URL(url); } catch { showCfgStatus('❌ URL không hợp lệ', true); return; }
|
||||||
chrome.storage.local.set({ apiUrl: url, apiKey: key }, () => {
|
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);
|
showCfgStatus('✅ Đã lưu!', false);
|
||||||
cfgBadge.style.display = 'none';
|
cfgBadge.style.display = 'none';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
testBtn.addEventListener('click', () => {
|
testBtn.addEventListener('click', () => {
|
||||||
chrome.storage.local.get(['apiUrl', 'apiKey'], (cfg) => {
|
chrome.storage.local.get(['apiUrl','apiKey','translateUrl','translateKey'], (cfg) => {
|
||||||
showCfgStatus(`URL: ${cfg.apiUrl || '(trống)'}\nKey: ${cfg.apiKey ? cfg.apiKey.slice(0,8)+'...' : '(trống)'}`, !cfg.apiUrl || !cfg.apiKey);
|
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 =====
|
// ===== SHOW RESULT / ERROR =====
|
||||||
function showResult(comment, model= '') {
|
function showResult(comment, model='', transVi='') {
|
||||||
if (!sidebarHost) return;
|
if (!sidebarHost) return;
|
||||||
const s = sidebarHost.shadowRoot;
|
const s = sidebarHost.shadowRoot;
|
||||||
const statusEl = s.querySelector('#ai-status');
|
const statusEl = s.querySelector('#ai-status');
|
||||||
|
const transViEl= s.querySelector('#ai-trans-vi');
|
||||||
const copyHint = s.querySelector('#ai-hint');
|
const copyHint = s.querySelector('#ai-hint');
|
||||||
const runBtn = s.querySelector('#ai-run');
|
const runBtn = s.querySelector('#ai-run');
|
||||||
const drawer = s.querySelector('.drawer');
|
const drawer = s.querySelector('.drawer');
|
||||||
@@ -465,28 +597,68 @@
|
|||||||
statusEl.style.display = 'block';
|
statusEl.style.display = 'block';
|
||||||
statusEl.className = 'status ok';
|
statusEl.className = 'status ok';
|
||||||
statusEl.textContent = comment;
|
statusEl.textContent = comment;
|
||||||
|
|
||||||
copyHint.textContent = model;
|
|
||||||
}
|
}
|
||||||
if (copyHint) copyHint.style.display = 'block';
|
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 (runBtn) runBtn.disabled = false;
|
||||||
if (drawer) drawer.classList.add('open');
|
if (drawer) drawer.classList.add('open');
|
||||||
if (typingOpts) typingOpts.classList.add('visible');
|
if (typingOpts) typingOpts.classList.add('visible');
|
||||||
}
|
}
|
||||||
|
|
||||||
function showError(msg) {
|
function showError(msg) {
|
||||||
if (!sidebarHost) return;
|
if (!sidebarHost) return;
|
||||||
const s = sidebarHost.shadowRoot;
|
const s = sidebarHost.shadowRoot;
|
||||||
const statusEl = s.querySelector('#ai-status');
|
const statusEl = s.querySelector('#ai-status');
|
||||||
|
const transViEl= s.querySelector('#ai-trans-vi');
|
||||||
const runBtn = s.querySelector('#ai-run');
|
const runBtn = s.querySelector('#ai-run');
|
||||||
const drawer = s.querySelector('.drawer');
|
const drawer = s.querySelector('.drawer');
|
||||||
if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'status err'; statusEl.textContent = msg; }
|
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 (runBtn) runBtn.disabled = false;
|
||||||
if (drawer) drawer.classList.add('open');
|
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() {
|
function removeSidebar() {
|
||||||
if (sidebarHost) { sidebarHost.remove(); sidebarHost = null; }
|
if (sidebarHost) { sidebarHost.remove(); sidebarHost = null; }
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
const d = document.createElement('div'); d.textContent = text; return d.innerHTML;
|
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);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
Reference in New Issue
Block a user