diff --git a/chrom-ext/background.js b/chrom-ext/background.js index d01afe7..9f2f7dd 100644 --- a/chrom-ext/background.js +++ b/chrom-ext/background.js @@ -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.contextMenus.create({ id: 'writeComment', title: '✍️ Viết comment', contexts: ['all'], - documentUrlPatterns: ['https://x.com/*', 'https://twitter.com/*'] + documentUrlPatterns: ['https://x.com/*'] }); }); -// ===== 2. KHI CLICK VÀO MENU ===== -chrome.contextMenus.onClicked.addListener((info, tab) => { - if (info.menuItemId === 'writeComment') { - chrome.tabs.sendMessage(tab.id, { action: 'OPEN_FORM' }).catch(() => { - // Nếu content script chưa sẵn sàng (hiếm) thì bỏ qua - }); +chrome.contextMenus.onClicked.addListener(async (info, tab) => { + if (info.menuItemId !== 'writeComment' || !tab?.id) return; + try { + await chrome.tabs.sendMessage(tab.id, { action: 'OPEN_FORM' }); + } 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) => { if (request.action === 'GENERATE_COMMENT') { callYourAPI(request.data, sender.tab.id); + return true; } }); -async function callYourAPI({ - text, - tone, - angle, - 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 +async function callYourAPI({ text, lang, tone, angle }, tabId) { + const payload = { originalPost: text, language:lang, tone, angle }; + console.log('[XAI Background] ⏳ Gọi API:', API_URL); + console.log('[XAI Background] 📤 Payload:', JSON.stringify(payload, null, 2)); - const res = await fetch(`${API_URL}${commentApi}`, { + try { + const res = await fetch(API_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${API_KEY}` }, - body: JSON.stringify({ - originalPost: text, - tone: tone, - angle: angle, - language, - }) + body: JSON.stringify(payload) }); + const data = await res.json(); + console.log('[XAI Background] 📥 Status:', res.status, '| JSON:', data); + 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); - - chrome.tabs.sendMessage(tabId, { action: 'SHOW_RESULT', comment }); + await chrome.tabs.sendMessage(tabId, { action: 'SHOW_RESULT', comment }); } 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 }); } } \ No newline at end of file diff --git a/chrom-ext/content.js b/chrom-ext/content.js index fadd2c5..a7e2ef6 100644 --- a/chrom-ext/content.js +++ b/chrom-ext/content.js @@ -1,210 +1,319 @@ (() => { '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) => { - const article = e.target.closest('article[data-testid="tweet"]'); - if (!article) { - selectedTweetText = ''; - return; - } - const textContainer = article.querySelector('[data-testid="tweetText"]'); - selectedTweetText = textContainer ? textContainer.innerText.trim() : ''; + 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. LẮNG NGHE MESSAGE TỪ BACKGROUND ===== + // ===== B. LẮNG NGHE BACKGROUND ===== chrome.runtime.onMessage.addListener((req, sender, sendResponse) => { if (req.action === 'OPEN_FORM') { - if (!selectedTweetText) { - 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.'); - return; + 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; } - openModal(selectedTweetText); - } else if (req.action === 'SHOW_RESULT') { - showResult(req.comment); - } else if (req.action === 'SHOW_ERROR') { - showError(req.error); + openSidebar(lastTweetText); + return true; } + 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) ===== - function openModal(tweetText) { - removeModal(); // Xóa cũ nếu có + // ===== C. SIDEBAR ===== + function openSidebar(tweetText) { + removeSidebar(); const host = document.createElement('div'); - host.id = 'x-comment-ai-host'; + host.id = 'x-ai-sidebar-host'; Object.assign(host.style, { - position: 'fixed', - top: '0', - left: '0', - width: '100%', - height: '100%', - zIndex: '999999', - pointerEvents: 'none' + 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 = ` - .overlay { - position: fixed; inset: 0; - background: rgba(0,0,0,0.55); + .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; - 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; - width: 420px; max-width: 92vw; - border-radius: 16px; padding: 24px; + 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; - 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)} } - h3 { margin: 0; font-size: 20px; } + .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; } + .body { padding: 16px 18px; flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; } .tweet-box { background: #f7f9f9; border: 1px solid #cfd9de; border-radius: 10px; - padding: 12px; font-size: 14px; line-height: 1.4; - max-height: 120px; overflow-y: auto; color: #333; - } - 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; + 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 { 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 { - background: #1d9bf0; color: #fff; border: none; - font-weight: 700; cursor: pointer; margin-top: 4px; + 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.ghost { - 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 { 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; color: #0f1419; } .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'); - overlay.className = 'overlay'; - overlay.addEventListener('click', (e) => { - if (e.target === overlay) removeModal(); - }); + const fab = document.createElement('button'); + fab.className = 'fab'; fab.textContent = '🤖'; fab.title = 'AI Comment'; - const modal = document.createElement('div'); - modal.className = 'modal'; - modal.innerHTML = ` -