U
This commit is contained in:
+28
-15
@@ -1,10 +1,6 @@
|
||||
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 =====
|
||||
// ===== TẠO MENU =====
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
chrome.contextMenus.create({
|
||||
id: 'writeComment',
|
||||
@@ -23,38 +19,55 @@ chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ===== HANDLE GENERATE =====
|
||||
chrome.runtime.onMessage.addListener((request, sender) => {
|
||||
// ===== NHẬN YÊU CẦU TỪ CONTENT =====
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
if (request.action === 'GENERATE_COMMENT') {
|
||||
callYourAPI(request.data, sender.tab.id);
|
||||
return true;
|
||||
// Gọi API async riêng, không await ở đây
|
||||
handleGenerate(request.data, sender.tab.id);
|
||||
|
||||
// Đóng kênh ngay để tránh lỗi "channel closed"
|
||||
sendResponse({ received: true });
|
||||
}
|
||||
});
|
||||
|
||||
async function callYourAPI({ text, lang, tone, angle }, tabId) {
|
||||
// ===== GỌI API: ĐỌC CONFIG TỪ STORAGE =====
|
||||
async function handleGenerate({ text, lang, tone, angle }, tabId) {
|
||||
console.log('[XAI Background] ⏳ Đọc config từ storage...');
|
||||
|
||||
const config = await chrome.storage.local.get(['apiUrl', 'apiKey']);
|
||||
|
||||
if (!config.apiUrl || !config.apiKey) {
|
||||
console.error('[XAI Background] ❌ Thiếu API config');
|
||||
await chrome.tabs.sendMessage(tabId, {
|
||||
action: 'SHOW_ERROR',
|
||||
error: '⚠️ Chưa cấu hình API.\n\n👉 Bấm tab ⚙️ Config để nhập URL và Key.'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
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));
|
||||
console.log('[XAI Background] 🌐 URL:', config.apiUrl);
|
||||
|
||||
try {
|
||||
const res = await fetch(API_URL, {
|
||||
const res = await fetch(config.apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${API_KEY}`
|
||||
'Authorization': `Bearer ${config.apiKey}`
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const data = await res.json();
|
||||
console.log('[XAI Background] 📥 Status:', res.status, '| JSON:', data);
|
||||
console.log('[XAI Background] 📥 Response:', res.status, data);
|
||||
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
|
||||
const comment = data.comment || data.text || JSON.stringify(data);
|
||||
await chrome.tabs.sendMessage(tabId, { action: 'SHOW_RESULT', comment });
|
||||
} catch (err) {
|
||||
console.error('[XAI Background] ❌ Lỗi:', err.message);
|
||||
console.error('[XAI Background] ❌ Lỗi fetch:', err.message);
|
||||
await chrome.tabs.sendMessage(tabId, { action: 'SHOW_ERROR', error: err.message });
|
||||
}
|
||||
}
|
||||
+157
-123
@@ -6,67 +6,63 @@
|
||||
|
||||
// ===== DATA =====
|
||||
const LANGS = [
|
||||
{ value: 'ja', label: 'Nhật' },
|
||||
{ value: 'vi', label: 'Việt' },
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'ja', label: 'Nhat' },
|
||||
{ value: 'ko', label: 'Han Quoc' },
|
||||
{ value: 'ko', label: 'Hàn' },
|
||||
{ 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.' }
|
||||
{ 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' }
|
||||
];
|
||||
|
||||
// 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' }
|
||||
{ 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, 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' }
|
||||
{ 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' }
|
||||
{ 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];
|
||||
return tone === 'EMPATHETIC' ? [...ANGLE_EMPATHY] : [...ANGLE_DEFAULT];
|
||||
}
|
||||
|
||||
// ===== A. BẮT CHUỘT PHẢI =====
|
||||
// ===== DOM =====
|
||||
document.addEventListener('contextmenu', (e) => {
|
||||
const article = e.target.closest('article');
|
||||
if (!article) { lastTweetText = ''; return; }
|
||||
@@ -79,7 +75,6 @@
|
||||
: article.innerText.trim().slice(0, 600);
|
||||
});
|
||||
|
||||
// ===== B. LẮNG NGHE BACKGROUND =====
|
||||
chrome.runtime.onMessage.addListener((req, sender, sendResponse) => {
|
||||
if (req.action === 'OPEN_FORM') {
|
||||
if (!lastTweetText) {
|
||||
@@ -93,7 +88,7 @@
|
||||
if (req.action === 'SHOW_ERROR') { showError(req.error); return true; }
|
||||
});
|
||||
|
||||
// ===== C. SIDEBAR =====
|
||||
// ===== SIDEBAR =====
|
||||
function openSidebar(tweetText) {
|
||||
removeSidebar();
|
||||
|
||||
@@ -109,55 +104,40 @@
|
||||
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 { position: fixed; right: 24px; bottom: 24px; width: 56px; height: 56px; border-radius: 50%;
|
||||
background: #1d9bf0; color: #fff; font-size: 24px; display: flex; align-items: center; justify-content: center;
|
||||
cursor: pointer; box-shadow: 0 4px 16px rgba(0,0,0,0.25); border: none; pointer-events: auto; z-index: 1;
|
||||
transition: transform .15s ease; user-select: none; }
|
||||
.fab:hover { transform: scale(1.08); }
|
||||
.drawer {
|
||||
position: fixed; right: 0; top: 0;
|
||||
width: 400px; max-width: 100vw; height: 100vh;
|
||||
background: #fff; color: #0f1419;
|
||||
box-shadow: -5px 0 25px rgba(0,0,0,0.15);
|
||||
transform: translateX(100%);
|
||||
transition: transform .25s ease;
|
||||
display: flex; flex-direction: column;
|
||||
pointer-events: auto; z-index: 2;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
.drawer { position: fixed; right: 0; top: 0; width: 400px; max-width: 100vw; height: 100vh; background: #fff; color: #0f1419;
|
||||
box-shadow: -5px 0 25px rgba(0,0,0,0.15); transform: translateX(100%); transition: transform .25s ease;
|
||||
display: flex; flex-direction: column; pointer-events: auto; z-index: 2;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
|
||||
.drawer.open { transform: translateX(0); }
|
||||
.header {
|
||||
padding: 14px 18px; border-bottom: 1px solid #eff3f4;
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
font-weight: 700; font-size: 16px;
|
||||
}
|
||||
.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: 10px; font-size: 13px; line-height: 1.4;
|
||||
max-height: 150px; overflow-y: auto; color: #333;
|
||||
white-space: pre-wrap; word-break: break-word;
|
||||
}
|
||||
.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 { 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 {
|
||||
width: 100%; padding: 10px; border-radius: 9999px; border: none;
|
||||
background: #1d9bf0; color: #fff; font-weight: 700; font-size: 15px;
|
||||
cursor: pointer; margin-top: 6px;
|
||||
}
|
||||
select, input[type="text"], input[type="password"] { width: 100%; padding: 9px 10px; border-radius: 8px; border: 1px solid #cfd9de;
|
||||
font-size: 14px; background: #fff; box-sizing: border-box; }
|
||||
.hint-text { font-size: 12px; color: #888; line-height: 1.4; font-style: italic; }
|
||||
button.primary { width: 100%; padding: 10px; border-radius: 9999px; border: none; background: #1d9bf0; color: #fff;
|
||||
font-weight: 700; font-size: 15px; cursor: pointer; margin-top: 6px; }
|
||||
button.primary:hover { background: #1a8cd8; }
|
||||
button.primary:disabled { background: #8ecdf7; cursor: default; }
|
||||
.status { display: none; padding: 12px; border-radius: 10px; font-size: 14px; line-height: 1.5; white-space: pre-wrap; word-break: break-word; }
|
||||
.status.ok { background: #e8f6fe; border: 1px solid #b4dffc; color: #0f1419; }
|
||||
.status.ok { background: #e8f6fe; border: 1px solid #b4dffc; }
|
||||
.status.err { background: #ffeaea; border: 1px solid #ffc5c5; color: #b00; }
|
||||
.copy-hint { font-size: 12px; color: #536471; text-align: center; margin-top: 4px; 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; }
|
||||
`;
|
||||
|
||||
const fab = document.createElement('button');
|
||||
@@ -167,36 +147,76 @@
|
||||
drawer.className = 'drawer';
|
||||
drawer.innerHTML = `
|
||||
<div class="header"><span>✍️ AI Comment</span><button class="btn-x">✕</button></div>
|
||||
<div class="body">
|
||||
<div class="tabs">
|
||||
<button class="tab-btn active" data-tab="comment">Comment</button>
|
||||
<button class="tab-btn" data-tab="config">⚙️ Config <span class="badge" id="cfg-missing" style="display:none">!</span></button>
|
||||
</div>
|
||||
|
||||
<!-- TAB COMMENT -->
|
||||
<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">${buildLangOptions()}</select>
|
||||
<select id="ai-lang">${LANGS.map(l => `<option value="${l.value}">${l.label}</option>`).join('')}</select>
|
||||
|
||||
<label>Tone — Giọng điệu</label>
|
||||
<label>Tone</label>
|
||||
<select id="ai-tone"></select>
|
||||
<div class="hint-text" id="ai-tone-hint"></div>
|
||||
|
||||
<label>Angle — Góc tiếp cận</label>
|
||||
<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="copy-hint" id="ai-hint">📋 Copy kết quả và dán vào ô reply nhé!</div>
|
||||
<div class="copy-hint" id="ai-hint">📋 Copy kết quả và dán vào ô reply!</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB CONFIG -->
|
||||
<div class="tab-content" id="tab-config">
|
||||
<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.
|
||||
</div>
|
||||
|
||||
<label>API URL</label>
|
||||
<input type="text" id="cfg-url" placeholder="https://api.yoursite.com/v1/generate">
|
||||
|
||||
<label>API Key</label>
|
||||
<input type="password" id="cfg-key" placeholder="sk-...">
|
||||
|
||||
<div style="display:flex; gap:8px;">
|
||||
<button class="primary" id="cfg-save" style="flex:1">💾 Lưu</button>
|
||||
<button class="primary" id="cfg-test" style="flex:1;background:#17bf63;">🧪 Test đọc</button>
|
||||
</div>
|
||||
|
||||
<div class="status" id="cfg-status"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
shadow.appendChild(style);
|
||||
shadow.appendChild(fab);
|
||||
shadow.appendChild(drawer);
|
||||
|
||||
// Toggle drawer
|
||||
const toggleDrawer = () => drawer.classList.toggle('open');
|
||||
fab.addEventListener('click', toggleDrawer);
|
||||
drawer.querySelector('.btn-x').addEventListener('click', () => drawer.classList.remove('open'));
|
||||
requestAnimationFrame(() => drawer.classList.add('open'));
|
||||
|
||||
// Refs
|
||||
// Tab switching
|
||||
const tabBtns = drawer.querySelectorAll('.tab-btn');
|
||||
const tabContents = drawer.querySelectorAll('.tab-content');
|
||||
tabBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
tabBtns.forEach(b => b.classList.remove('active'));
|
||||
tabContents.forEach(c => c.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
drawer.querySelector(`#tab-${btn.dataset.tab}`).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// ===== COMMENT TAB LOGIC =====
|
||||
const langSel = drawer.querySelector('#ai-lang');
|
||||
const toneSel = drawer.querySelector('#ai-tone');
|
||||
const toneHint = drawer.querySelector('#ai-tone-hint');
|
||||
@@ -206,38 +226,26 @@
|
||||
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.text}</option>`
|
||||
).join('');
|
||||
if (selectedValue && items.find(i => i.value === selectedValue)) {
|
||||
sel.value = 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;
|
||||
@@ -245,44 +253,75 @@
|
||||
angleHint.textContent = aItem ? aItem.text : '';
|
||||
}
|
||||
|
||||
// Events
|
||||
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);
|
||||
});
|
||||
|
||||
langSel.addEventListener('change', () => updateTone(langSel.value, true));
|
||||
toneSel.addEventListener('change', onToneChange);
|
||||
angleSel.addEventListener('change', onAngleChange);
|
||||
|
||||
// Init
|
||||
updateTone(langSel.value, false);
|
||||
|
||||
// Generate
|
||||
runBtn.addEventListener('click', () => {
|
||||
const lang = langSel.value;
|
||||
const tone = toneSel.value;
|
||||
const angle = angleSel.value;
|
||||
|
||||
runBtn.disabled = true;
|
||||
statusEl.style.display = 'block';
|
||||
statusEl.className = 'status ok';
|
||||
statusEl.textContent = '⏳ Đang gọi API...';
|
||||
copyHint.style.display = 'none';
|
||||
|
||||
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;
|
||||
statusEl.textContent = 'Lỗi kết nối: ' + chrome.runtime.lastError.message;
|
||||
runBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// ===== CONFIG TAB LOGIC =====
|
||||
const urlIn = drawer.querySelector('#cfg-url');
|
||||
const keyIn = drawer.querySelector('#cfg-key');
|
||||
const saveBtn = drawer.querySelector('#cfg-save');
|
||||
const testBtn = drawer.querySelector('#cfg-test');
|
||||
const cfgStatus= drawer.querySelector('#cfg-status');
|
||||
const cfgBadge = drawer.querySelector('#cfg-missing');
|
||||
|
||||
function showCfgStatus(msg, isErr) {
|
||||
cfgStatus.style.display = 'block';
|
||||
cfgStatus.className = 'status ' + (isErr ? 'err' : 'ok');
|
||||
cfgStatus.textContent = msg;
|
||||
}
|
||||
|
||||
// ===== D. RESULT / ERROR =====
|
||||
// Đọc config hiện tại
|
||||
chrome.storage.local.get(['apiUrl', 'apiKey'], (cfg) => {
|
||||
if (cfg.apiUrl) urlIn.value = cfg.apiUrl;
|
||||
if (cfg.apiKey) keyIn.value = cfg.apiKey;
|
||||
cfgBadge.style.display = (!cfg.apiUrl || !cfg.apiKey) ? 'inline-block' : 'none';
|
||||
});
|
||||
|
||||
saveBtn.addEventListener('click', () => {
|
||||
const url = urlIn.value.trim();
|
||||
const key = keyIn.value.trim();
|
||||
if (!url || !key) {
|
||||
showCfgStatus('❌ URL và Key không được để trống!', true);
|
||||
return;
|
||||
}
|
||||
try { new URL(url); } catch {
|
||||
showCfgStatus('❌ URL không hợp lệ (cần có https://)', true);
|
||||
return;
|
||||
}
|
||||
chrome.storage.local.set({ apiUrl: url, apiKey: key }, () => {
|
||||
showCfgStatus('✅ Đã lưu! Bạn có thể đóng tab này và bấm Tạo Comment.', false);
|
||||
cfgBadge.style.display = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
testBtn.addEventListener('click', () => {
|
||||
chrome.storage.local.get(['apiUrl', 'apiKey'], (cfg) => {
|
||||
showCfgStatus(`URL: ${cfg.apiUrl || '(trống)'}\nKey: ${cfg.apiKey ? cfg.apiKey.slice(0,8)+'...' : '(trống)'}`, !cfg.apiUrl || !cfg.apiKey);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ===== RESULT / ERROR =====
|
||||
function showResult(comment) {
|
||||
if (!sidebarHost) return;
|
||||
const s = sidebarHost.shadowRoot;
|
||||
@@ -290,29 +329,24 @@
|
||||
const copyHint = s.querySelector('#ai-hint');
|
||||
const runBtn = s.querySelector('#ai-run');
|
||||
const drawer = s.querySelector('.drawer');
|
||||
|
||||
if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'status ok'; statusEl.textContent = comment; }
|
||||
if (copyHint) copyHint.style.display = 'block';
|
||||
if (runBtn) runBtn.disabled = false;
|
||||
if (drawer) drawer.classList.add('open');
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
if (!sidebarHost) return;
|
||||
const s = sidebarHost.shadowRoot;
|
||||
const statusEl = s.querySelector('#ai-status');
|
||||
const runBtn = s.querySelector('#ai-run');
|
||||
const drawer = s.querySelector('.drawer');
|
||||
|
||||
if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'status err'; statusEl.textContent = 'Lỗi: ' + msg; }
|
||||
if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'status err'; statusEl.textContent = msg; }
|
||||
if (runBtn) runBtn.disabled = false;
|
||||
if (drawer) drawer.classList.add('open');
|
||||
}
|
||||
|
||||
function removeSidebar() {
|
||||
if (sidebarHost) { sidebarHost.remove(); sidebarHost = null; }
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const d = document.createElement('div'); d.textContent = text; return d.innerHTML;
|
||||
}
|
||||
|
||||
+7
-10
@@ -1,16 +1,14 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "X Comment AI",
|
||||
"version": "1.0.0",
|
||||
"description": "Tạo comment cho X.com bằng AI của bạn",
|
||||
"version": "1.2.0",
|
||||
"description": "AI Comment cho X.com — có cấu hình",
|
||||
"permissions": [
|
||||
"contextMenus",
|
||||
"storage"
|
||||
],
|
||||
"host_permissions": [
|
||||
"https://x.com/*",
|
||||
"https://twitter.com/*",
|
||||
"https://api.yoursite.com/*"
|
||||
"https://x.com/*"
|
||||
],
|
||||
"background": {
|
||||
"service_worker": "background.js"
|
||||
@@ -18,8 +16,7 @@
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"https://x.com/*",
|
||||
"https://twitter.com/*"
|
||||
"https://x.com/*"
|
||||
],
|
||||
"js": [
|
||||
"content.js"
|
||||
@@ -28,8 +25,8 @@
|
||||
}
|
||||
],
|
||||
"icons": {
|
||||
"16": "icons/icon.webp",
|
||||
"48": "icons/icon.webp",
|
||||
"128": "icons/icon.webp"
|
||||
"16": "icons/icon.png",
|
||||
"48": "icons/icon.png",
|
||||
"128": "icons/icon.png"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user