Update
This commit is contained in:
+29
-32
@@ -1,63 +1,60 @@
|
|||||||
// ===== 1. TẠO CONTEXT MENU =====
|
console.log('[XAI Background] ✅ Service worker started');
|
||||||
|
|
||||||
|
// ⚠️ SỬA ENDPOINT & KEY
|
||||||
|
const API_URL = 'https://punch-scientific-electrical-antibodies.trycloudflare.com/content-writer/comment';
|
||||||
|
const API_KEY = 'YOUR_API_KEY_HERE';
|
||||||
|
|
||||||
|
// ===== MENU =====
|
||||||
chrome.runtime.onInstalled.addListener(() => {
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
chrome.contextMenus.create({
|
chrome.contextMenus.create({
|
||||||
id: 'writeComment',
|
id: 'writeComment',
|
||||||
title: '✍️ Viết comment',
|
title: '✍️ Viết comment',
|
||||||
contexts: ['all'],
|
contexts: ['all'],
|
||||||
documentUrlPatterns: ['https://x.com/*', 'https://twitter.com/*']
|
documentUrlPatterns: ['https://x.com/*']
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== 2. KHI CLICK VÀO MENU =====
|
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
||||||
chrome.contextMenus.onClicked.addListener((info, tab) => {
|
if (info.menuItemId !== 'writeComment' || !tab?.id) return;
|
||||||
if (info.menuItemId === 'writeComment') {
|
try {
|
||||||
chrome.tabs.sendMessage(tab.id, { action: 'OPEN_FORM' }).catch(() => {
|
await chrome.tabs.sendMessage(tab.id, { action: 'OPEN_FORM' });
|
||||||
// Nếu content script chưa sẵn sàng (hiếm) thì bỏ qua
|
} catch (err) {
|
||||||
});
|
console.error('[XAI Background] ❌ Không gọi được content script:', err.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== 3. NHẬN YÊU CẦU TỪ CONTENT SCRIPT VÀ GỌI API =====
|
// ===== HANDLE GENERATE =====
|
||||||
chrome.runtime.onMessage.addListener((request, sender) => {
|
chrome.runtime.onMessage.addListener((request, sender) => {
|
||||||
if (request.action === 'GENERATE_COMMENT') {
|
if (request.action === 'GENERATE_COMMENT') {
|
||||||
callYourAPI(request.data, sender.tab.id);
|
callYourAPI(request.data, sender.tab.id);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function callYourAPI({
|
async function callYourAPI({ text, lang, tone, angle }, tabId) {
|
||||||
text,
|
const payload = { originalPost: text, language:lang, tone, angle };
|
||||||
tone,
|
console.log('[XAI Background] ⏳ Gọi API:', API_URL);
|
||||||
angle,
|
console.log('[XAI Background] 📤 Payload:', JSON.stringify(payload, null, 2));
|
||||||
language
|
|
||||||
}, tabId) {
|
|
||||||
try {
|
|
||||||
// ⚠️ THAY BẰNG ENDPOINT CỦA BẠN
|
|
||||||
const API_URL = 'https://gamerdota0042-himalayas.nord:3000';
|
|
||||||
const commentApi='/content-writer/comment'
|
|
||||||
const API_KEY = 'YOUR_API_KEY_HERE'; // Nên chuyển sang chrome.storage nếu publish
|
|
||||||
|
|
||||||
const res = await fetch(`${API_URL}${commentApi}`, {
|
try {
|
||||||
|
const res = await fetch(API_URL, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${API_KEY}`
|
'Authorization': `Bearer ${API_KEY}`
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(payload)
|
||||||
originalPost: text,
|
|
||||||
tone: tone,
|
|
||||||
angle: angle,
|
|
||||||
language,
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const data = await res.json();
|
||||||
|
console.log('[XAI Background] 📥 Status:', res.status, '| JSON:', data);
|
||||||
|
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
|
||||||
const data = await res.json();
|
|
||||||
// Giả định API trả về: { comment: "..." }
|
|
||||||
const comment = data.comment || data.text || JSON.stringify(data);
|
const comment = data.comment || data.text || JSON.stringify(data);
|
||||||
|
await chrome.tabs.sendMessage(tabId, { action: 'SHOW_RESULT', comment });
|
||||||
chrome.tabs.sendMessage(tabId, { action: 'SHOW_RESULT', comment });
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
chrome.tabs.sendMessage(tabId, { action: 'SHOW_ERROR', error: err.message });
|
console.error('[XAI Background] ❌ Lỗi:', err.message);
|
||||||
|
await chrome.tabs.sendMessage(tabId, { action: 'SHOW_ERROR', error: err.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+258
-149
@@ -1,210 +1,319 @@
|
|||||||
(() => {
|
(() => {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
let selectedTweetText = '';
|
let lastTweetText = '';
|
||||||
|
let sidebarHost = null;
|
||||||
|
|
||||||
// ===== A. LẮNG NGHE CHUỘT PHẢI ĐỂ LẤY TWEET TEXT =====
|
// ===== DATA =====
|
||||||
|
const LANGS = [
|
||||||
|
{ value: 'vi', label: 'Việt' },
|
||||||
|
{ value: 'en', label: 'English' },
|
||||||
|
{ value: 'ja', label: 'Nhat' },
|
||||||
|
{ value: 'ko', label: 'Han Quoc' },
|
||||||
|
{ value: 'cn', label: 'Trung' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Tone base (tất cả ngôn ngữ đều có)
|
||||||
|
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, biết trân trọng người khác.' },
|
||||||
|
{ value: 'PROVOCATIVE', text: 'provocative — Gợi mở suy nghĩ, hơi gây tranh cãi, thách thức các giả định.' },
|
||||||
|
{ value: 'AUTHORITATIVE', text: 'authoritative — giọng tự tin, uy quyền, chuyên nghiệp' },
|
||||||
|
{ value: 'SPICY', text: 'spicy — Tự tin, hơi đối đầu. KHÔNG giận dữ — chỉ thẳng.' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Tone chỉ dành cho ja
|
||||||
|
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, controversial takes' },
|
||||||
|
{ value: 'SAVAGE', text: 'savage — Chửi tục OK. Sass tối đa. Vui + ác + thông minh' }
|
||||||
|
];
|
||||||
|
|
||||||
|
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, không gây khó chịu' },
|
||||||
|
{ value: 'QUESTION', text: 'Đặt một câu hỏi tiếp theo thông minh' },
|
||||||
|
{ value: 'RELATE', text: 'Chia sẻ một trải nghiệm hoặc cảm xúc cá nhân tương tự như bài đăng gốc.' },
|
||||||
|
{ value: 'DEVIL_ADVOCATE', text: 'Hãy đóng vai trò người phản biện. Trình bày quan điểm trái chiều một cách công bằng mà không tỏ ra thù địch.' },
|
||||||
|
{ value: 'EXPAND', text: 'expand — Chọn 1 điểm phân tích sâu hơn với nhiều sắc thái khác nhau.' },
|
||||||
|
{ value: 'VALIDATE', text: 'validate — Khẳng định luận điểm = bằng chứng hoặc sự đồng tình mạnh mẽ, tăng cường độ tin cậy.' },
|
||||||
|
{ value: 'CTA', text: 'cta — Kết thúc bằng lời 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) {
|
||||||
|
if (tone === 'EMPATHETIC') return [...ANGLE_EMPATHY];
|
||||||
|
return [...ANGLE_DEFAULT];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== A. BẮT CHUỘT PHẢI =====
|
||||||
document.addEventListener('contextmenu', (e) => {
|
document.addEventListener('contextmenu', (e) => {
|
||||||
const article = e.target.closest('article[data-testid="tweet"]');
|
const article = e.target.closest('article');
|
||||||
if (!article) {
|
if (!article) { lastTweetText = ''; return; }
|
||||||
selectedTweetText = '';
|
const textEl =
|
||||||
return;
|
article.querySelector('[data-testid="tweetText"]') ||
|
||||||
}
|
article.querySelector('div[lang]') ||
|
||||||
const textContainer = article.querySelector('[data-testid="tweetText"]');
|
article.querySelector('div[dir="auto"]');
|
||||||
selectedTweetText = textContainer ? textContainer.innerText.trim() : '';
|
lastTweetText = textEl
|
||||||
|
? textEl.innerText.trim()
|
||||||
|
: article.innerText.trim().slice(0, 600);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== B. LẮNG NGHE MESSAGE TỪ BACKGROUND =====
|
// ===== B. LẮNG NGHE BACKGROUND =====
|
||||||
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 (!selectedTweetText) {
|
if (!lastTweetText) {
|
||||||
alert('Không tìm thấy nội dung tweet. Hãy chuột phải vào vùng văn bản của tweet.');
|
alert('Không tìm thấy tweet. Hãy chuột phải vào phần chữ của tweet.');
|
||||||
return;
|
return true;
|
||||||
}
|
}
|
||||||
openModal(selectedTweetText);
|
openSidebar(lastTweetText);
|
||||||
} else if (req.action === 'SHOW_RESULT') {
|
return true;
|
||||||
showResult(req.comment);
|
|
||||||
} else if (req.action === 'SHOW_ERROR') {
|
|
||||||
showError(req.error);
|
|
||||||
}
|
}
|
||||||
|
if (req.action === 'SHOW_RESULT') { showResult(req.comment); return true; }
|
||||||
|
if (req.action === 'SHOW_ERROR') { showError(req.error); return true; }
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== C. MỞ MODAL (DÙNG SHADOW DOM ĐỂ TRÁNH CSS XUNG ĐỘT) =====
|
// ===== C. SIDEBAR =====
|
||||||
function openModal(tweetText) {
|
function openSidebar(tweetText) {
|
||||||
removeModal(); // Xóa cũ nếu có
|
removeSidebar();
|
||||||
|
|
||||||
const host = document.createElement('div');
|
const host = document.createElement('div');
|
||||||
host.id = 'x-comment-ai-host';
|
host.id = 'x-ai-sidebar-host';
|
||||||
Object.assign(host.style, {
|
Object.assign(host.style, {
|
||||||
position: 'fixed',
|
position: 'fixed', top: '0', left: '0', width: '0', height: '0',
|
||||||
top: '0',
|
zIndex: '2147483647', overflow: 'visible', pointerEvents: 'none'
|
||||||
left: '0',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
zIndex: '999999',
|
|
||||||
pointerEvents: 'none'
|
|
||||||
});
|
});
|
||||||
document.body.appendChild(host);
|
document.body.appendChild(host);
|
||||||
|
sidebarHost = host;
|
||||||
|
|
||||||
const shadow = host.attachShadow({ mode: 'open' });
|
const shadow = host.attachShadow({ mode: 'open' });
|
||||||
|
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
.overlay {
|
.fab {
|
||||||
position: fixed; inset: 0;
|
position: fixed; right: 24px; bottom: 24px;
|
||||||
background: rgba(0,0,0,0.55);
|
width: 56px; height: 56px; border-radius: 50%;
|
||||||
|
background: #1d9bf0; color: #fff; font-size: 24px;
|
||||||
display: flex; align-items: center; justify-content: center;
|
display: flex; align-items: center; justify-content: center;
|
||||||
pointer-events: auto;
|
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;
|
||||||
}
|
}
|
||||||
.modal {
|
.fab:hover { transform: scale(1.08); }
|
||||||
|
.drawer {
|
||||||
|
position: fixed; right: 0; top: 0;
|
||||||
|
width: 400px; max-width: 100vw; height: 100vh;
|
||||||
background: #fff; color: #0f1419;
|
background: #fff; color: #0f1419;
|
||||||
width: 420px; max-width: 92vw;
|
box-shadow: -5px 0 25px rgba(0,0,0,0.15);
|
||||||
border-radius: 16px; padding: 24px;
|
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;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
box-shadow: 0 25px 50px rgba(0,0,0,0.25);
|
|
||||||
display: flex; flex-direction: column; gap: 14px;
|
|
||||||
animation: popIn 0.2s ease-out;
|
|
||||||
}
|
}
|
||||||
@keyframes popIn { from {opacity:0; transform:scale(0.96)} to {opacity:1; transform:scale(1)} }
|
.drawer.open { transform: translateX(0); }
|
||||||
h3 { margin: 0; font-size: 20px; }
|
.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; }
|
||||||
|
.body { padding: 16px 18px; flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; }
|
||||||
.tweet-box {
|
.tweet-box {
|
||||||
background: #f7f9f9; border: 1px solid #cfd9de; border-radius: 10px;
|
background: #f7f9f9; border: 1px solid #cfd9de; border-radius: 10px;
|
||||||
padding: 12px; font-size: 14px; line-height: 1.4;
|
padding: 10px; font-size: 13px; line-height: 1.4;
|
||||||
max-height: 120px; overflow-y: auto; color: #333;
|
max-height: 150px; overflow-y: auto; color: #333;
|
||||||
}
|
white-space: pre-wrap; word-break: break-word;
|
||||||
label { font-size: 13px; font-weight: 700; color: #536471; }
|
|
||||||
select, button {
|
|
||||||
width: 100%; padding: 10px 12px; border-radius: 8px;
|
|
||||||
border: 1px solid #cfd9de; font-size: 14px; outline: none;
|
|
||||||
}
|
}
|
||||||
|
label { font-size: 12px; font-weight: 700; color: #536471; text-transform: uppercase; letter-spacing: .3px; margin-top: 4px; }
|
||||||
|
select { width: 100%; padding: 9px 10px; border-radius: 8px; border: 1px solid #cfd9de; font-size: 14px; background: #fff; }
|
||||||
|
.hint-text { font-size: 12px; color: #888; line-height: 1.4; margin-top: 2px; font-style: italic; }
|
||||||
button.primary {
|
button.primary {
|
||||||
background: #1d9bf0; color: #fff; border: none;
|
width: 100%; padding: 10px; border-radius: 9999px; border: none;
|
||||||
font-weight: 700; cursor: pointer; margin-top: 4px;
|
background: #1d9bf0; color: #fff; font-weight: 700; font-size: 15px;
|
||||||
|
cursor: pointer; margin-top: 6px;
|
||||||
}
|
}
|
||||||
button.primary:hover { background: #1a8cd8; }
|
button.primary:hover { background: #1a8cd8; }
|
||||||
button.primary:disabled { background: #8ecdf7; cursor: default; }
|
button.primary:disabled { background: #8ecdf7; cursor: default; }
|
||||||
button.ghost {
|
.status { display: none; padding: 12px; border-radius: 10px; font-size: 14px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
|
||||||
background: transparent; color: #536471; border: 1px solid #cfd9de; margin-top: 6px;
|
|
||||||
}
|
|
||||||
.status {
|
|
||||||
display: none; padding: 12px; border-radius: 10px; font-size: 14px; line-height: 1.5; white-space: pre-wrap;
|
|
||||||
}
|
|
||||||
.status.ok { background: #e8f6fe; border: 1px solid #b4dffc; color: #0f1419; }
|
.status.ok { background: #e8f6fe; border: 1px solid #b4dffc; color: #0f1419; }
|
||||||
.status.err { background: #ffeaea; border: 1px solid #ffc5c5; color: #b00; }
|
.status.err { background: #ffeaea; border: 1px solid #ffc5c5; color: #b00; }
|
||||||
.copy-hint { font-size: 12px; color: #536471; margin-top: 6px; text-align: center; }
|
.copy-hint { font-size: 12px; color: #536471; text-align: center; margin-top: 4px; display: none; }
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const overlay = document.createElement('div');
|
const fab = document.createElement('button');
|
||||||
overlay.className = 'overlay';
|
fab.className = 'fab'; fab.textContent = '🤖'; fab.title = 'AI Comment';
|
||||||
overlay.addEventListener('click', (e) => {
|
|
||||||
if (e.target === overlay) removeModal();
|
|
||||||
});
|
|
||||||
|
|
||||||
const modal = document.createElement('div');
|
const drawer = document.createElement('div');
|
||||||
modal.className = 'modal';
|
drawer.className = 'drawer';
|
||||||
modal.innerHTML = `
|
drawer.innerHTML = `
|
||||||
<h3>✍️ Viết Comment AI</h3>
|
<div class="header"><span>✍️ AI Comment</span><button class="btn-x">✕</button></div>
|
||||||
<div>
|
<div class="body">
|
||||||
<label>Nội dung Tweet</label>
|
<div class="tweet-box">${escapeHtml(tweetText)}</div>
|
||||||
<div class="tweet-box" id="ai-tweet"></div>
|
|
||||||
|
<label>Ngôn ngữ đầu ra</label>
|
||||||
|
<select id="ai-lang">${buildLangOptions()}</select>
|
||||||
|
|
||||||
|
<label>Tone — Giọng điệu</label>
|
||||||
|
<select id="ai-tone"></select>
|
||||||
|
<div class="hint-text" id="ai-tone-hint"></div>
|
||||||
|
|
||||||
|
<label>Angle — Góc tiếp cận</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="copy-hint" id="ai-hint">📋 Copy kết quả và dán vào ô reply nhé!</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<label>Ngôn ngữ (Language)</label>
|
|
||||||
<select id="ai-language">
|
|
||||||
<option value="en">Anh</option>
|
|
||||||
<option value="ja">Nhật</option>
|
|
||||||
<option value="vi">Việt</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>Tone (Giọng điệu)</label>
|
|
||||||
<select id="ai-tone">
|
|
||||||
<option value="funny">😂 Hài hước</option>
|
|
||||||
<option value="professional">💼 Chuyên nghiệp</option>
|
|
||||||
<option value="friendly">😊 Thân thiện</option>
|
|
||||||
<option value="sarcastic">🌶️ Châm biếm</option>
|
|
||||||
<option value="neutral">😐 Trung lập</option>
|
|
||||||
<option value="excited">🤩 Hào hứng</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>Angle (Góc tiếp cận)</label>
|
|
||||||
<select id="ai-angle">
|
|
||||||
<option value="agree">👍 Đồng tình</option>
|
|
||||||
<option value="disagree">👎 Phản biện</option>
|
|
||||||
<option value="question">❓ Đặt câu hỏi</option>
|
|
||||||
<option value="add_info">➕ Bổ sung thông tin</option>
|
|
||||||
<option value="joke">🃏 Câu đùa / Meme</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<button class="primary" id="ai-run">🚀 Tạo Comment</button>
|
|
||||||
<div class="status" id="ai-status"></div>
|
|
||||||
<div class="copy-hint" id="ai-hint" style="display:none;">📋 Copy kết quả và tự dán vào ô comment nhé!</div>
|
|
||||||
<button class="ghost" id="ai-close">Đóng</button>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
overlay.appendChild(modal);
|
|
||||||
shadow.appendChild(style);
|
shadow.appendChild(style);
|
||||||
shadow.appendChild(overlay);
|
shadow.appendChild(fab);
|
||||||
|
shadow.appendChild(drawer);
|
||||||
|
|
||||||
// Điền text tweet
|
const toggleDrawer = () => drawer.classList.toggle('open');
|
||||||
shadow.getElementById('ai-tweet').textContent = tweetText;
|
fab.addEventListener('click', toggleDrawer);
|
||||||
|
drawer.querySelector('.btn-x').addEventListener('click', () => drawer.classList.remove('open'));
|
||||||
|
requestAnimationFrame(() => drawer.classList.add('open'));
|
||||||
|
|
||||||
|
// Refs
|
||||||
|
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 buildLangOptions() {
|
||||||
|
return LANGS.map(l => `<option value="${l.value}">${l.label}</option>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateSelect(sel, items, selectedValue) {
|
||||||
|
sel.innerHTML = items.map(it =>
|
||||||
|
`<option value="${it.value}">${it.value}</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 : '';
|
||||||
|
}
|
||||||
|
|
||||||
// Events
|
// Events
|
||||||
shadow.getElementById('ai-close').addEventListener('click', removeModal);
|
langSel.addEventListener('change', () => {
|
||||||
|
// Đổi lang: rebuild tone, nếu tone cũ không tồn tại trong list mới thì reset
|
||||||
|
updateTone(langSel.value, true);
|
||||||
|
});
|
||||||
|
|
||||||
shadow.getElementById('ai-run').addEventListener('click', () => {
|
toneSel.addEventListener('change', onToneChange);
|
||||||
const language = shadow.getElementById('ai-language').value;
|
angleSel.addEventListener('change', onAngleChange);
|
||||||
const tone = shadow.getElementById('ai-tone').value;
|
|
||||||
const angle = shadow.getElementById('ai-angle').value;
|
|
||||||
const status = shadow.getElementById('ai-status');
|
|
||||||
const btn = shadow.getElementById('ai-run');
|
|
||||||
|
|
||||||
btn.disabled = true;
|
// Init
|
||||||
status.style.display = 'block';
|
updateTone(langSel.value, false);
|
||||||
status.className = 'status ok';
|
|
||||||
status.innerHTML = '<em>⏳ Đang tạo comment...</em>';
|
|
||||||
|
|
||||||
chrome.runtime.sendMessage({
|
// Generate
|
||||||
action: 'GENERATE_COMMENT',
|
runBtn.addEventListener('click', () => {
|
||||||
data: { text: tweetText, tone, angle, language }
|
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 background: ' + chrome.runtime.lastError.message;
|
||||||
|
runBtn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== D. HIỂN THỊ KẾT QUẢ / LỖI =====
|
// ===== D. RESULT / ERROR =====
|
||||||
function showResult(comment) {
|
function showResult(comment) {
|
||||||
const host = document.getElementById('x-comment-ai-host');
|
if (!sidebarHost) return;
|
||||||
if (!host) return;
|
const s = sidebarHost.shadowRoot;
|
||||||
const s = host.shadowRoot;
|
const statusEl = s.querySelector('#ai-status');
|
||||||
const status = s.getElementById('ai-status');
|
const copyHint = s.querySelector('#ai-hint');
|
||||||
const btn = s.getElementById('ai-run');
|
const runBtn = s.querySelector('#ai-run');
|
||||||
const hint = s.getElementById('ai-hint');
|
const drawer = s.querySelector('.drawer');
|
||||||
|
|
||||||
status.style.display = 'block';
|
if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'status ok'; statusEl.textContent = comment; }
|
||||||
status.className = 'status ok';
|
if (copyHint) copyHint.style.display = 'block';
|
||||||
status.textContent = comment;
|
if (runBtn) runBtn.disabled = false;
|
||||||
|
if (drawer) drawer.classList.add('open');
|
||||||
hint.style.display = 'block';
|
|
||||||
if (btn) btn.disabled = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showError(msg) {
|
function showError(msg) {
|
||||||
const host = document.getElementById('x-comment-ai-host');
|
if (!sidebarHost) return;
|
||||||
if (!host) return;
|
const s = sidebarHost.shadowRoot;
|
||||||
const s = host.shadowRoot;
|
const statusEl = s.querySelector('#ai-status');
|
||||||
const status = s.getElementById('ai-status');
|
const runBtn = s.querySelector('#ai-run');
|
||||||
const btn = s.getElementById('ai-run');
|
const drawer = s.querySelector('.drawer');
|
||||||
|
|
||||||
status.style.display = 'block';
|
if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'status err'; statusEl.textContent = 'Lỗi: ' + msg; }
|
||||||
status.className = 'status err';
|
if (runBtn) runBtn.disabled = false;
|
||||||
status.textContent = 'Lỗi: ' + msg;
|
if (drawer) drawer.classList.add('open');
|
||||||
|
|
||||||
if (btn) btn.disabled = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeModal() {
|
function removeSidebar() {
|
||||||
const el = document.getElementById('x-comment-ai-host');
|
if (sidebarHost) { sidebarHost.remove(); sidebarHost = null; }
|
||||||
if (el) el.remove();
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const d = document.createElement('div'); d.textContent = text; return d.innerHTML;
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
Reference in New Issue
Block a user