Files
trade-assistant/browser-extension/popup/popup.js
T

472 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* TradeMate Popup - Main UI Logic
*/
import {
login,
translate,
generateReply,
searchLeads,
generateMarketing,
getBalance,
getSubscriptionPlans,
getCreditPackages,
ApiError,
} from '../api/client.js';
// ─── State ───────────────────────────────────────────
let creditsState = {
balance: 0,
dailyFreeCharsLeft: 0,
planName: 'Free',
};
// ─── DOM refs ────────────────────────────────────────
const $ = (id) => document.getElementById(id);
const screenSettings = $('screen-settings');
const screenMain = $('screen-main');
const settingsUrl = $('settings-url');
const settingsUser = $('settings-username');
const settingsPass = $('settings-password');
const settingsLogin = $('settings-login');
const settingsStatus = $('settings-status');
const translateInput = $('translate-input');
const translateTarget = $('translate-target');
const translateBtn = $('translate-btn');
const translateResult = $('translate-result');
const replyInput = $('reply-input');
const replyTone = $('reply-tone');
const replyBtn = $('reply-btn');
const replyResult = $('reply-result');
const discoveryProduct = $('discovery-product');
const discoveryMarket = $('discovery-market');
const discoveryBtn = $('discovery-btn');
const discoveryResult = $('discovery-result');
const marketingName = $('marketing-name');
const marketingDesc = $('marketing-desc');
const marketingStyle = $('marketing-style');
const marketingBtn = $('marketing-btn');
const marketingResult = $('marketing-result');
const creditsDisplay = $('credits-display');
const btnSettings = $('btn-settings');
const upgradeBanner = $('upgrade-banner');
const upgradeBannerText = $('upgrade-banner-text');
const upgradeBannerBtn = $('upgrade-banner-btn');
const upgradeBannerClose = $('upgrade-banner-close');
const upgradeModalOverlay = $('upgrade-modal-overlay');
const modalClose = $('modal-close');
const packageModalOverlay = $('package-modal-overlay');
const pkgModalClose = $('pkg-modal-close');
// ─── CONSTANTS ───────────────────────────────────────
const CONSUMPTION_LABELS = {
lead_search: { name: '客户搜索', cost: 10 },
marketing_content: { name: '营销生成', cost: 5 },
reply_suggest: { name: '智能回复', cost: 2 },
translate_per_1000chars: { name: '翻译', cost: 1 },
quotation: { name: '报价单', cost: 2 },
followup_scan: { name: '跟进扫描', cost: 2 },
};
// ─── Init ────────────────────────────────────────────
async function init() {
const { apiUrl, token } = await chrome.storage.local.get(['trademate_api_url', 'trademate_token']);
if (apiUrl) settingsUrl.value = apiUrl;
if (apiUrl && token) {
showScreen('main');
await refreshCredits();
} else {
showScreen('settings');
}
setupTabs();
setupActions();
setupUpgradeUI();
}
function showScreen(screen) {
screenSettings.style.display = screen === 'settings' ? 'block' : 'none';
screenMain.style.display = screen === 'main' ? 'block' : 'none';
}
document.addEventListener('DOMContentLoaded', init);
// ─── Credits ─────────────────────────────────────────
async function refreshCredits() {
try {
const bal = await getBalance();
creditsState.balance = bal.balance;
creditsState.dailyFreeCharsLeft = bal.dailyFreeTranslateCharsLeft;
// Determine plan name from subscription data
if (bal.subscription?.plan_id) {
creditsState.planName = 'Starter';
// Could fetch plan name from subscription-plans endpoint for accuracy
} else {
creditsState.planName = 'Free';
}
updateCreditsDisplay();
hideUpgradeBanner(); // clear any stale credit error
return bal;
} catch (err) {
console.warn('Failed to fetch credits:', err.message);
updateCreditsDisplay(); // show fallback
return null;
}
}
function updateCreditsDisplay() {
const { balance, planName, dailyFreeCharsLeft } = creditsState;
let parts = [];
if (planName !== 'Free') {
parts.push(`${planName}`);
}
if (balance > 0) {
parts.push(`${balance} 积分`);
}
parts.push(`剩余`);
creditsDisplay.textContent = parts.join(' · ');
// Make credits clickable to upgrade
creditsDisplay.style.cursor = 'pointer';
creditsDisplay.title = '点击查看套餐';
creditsDisplay.onclick = () => showUpgradeModal();
}
function showCreditErrorBanner(detail, actionLabel = '升级') {
upgradeBannerText.textContent = detail || '次数不足';
upgradeBannerBtn.textContent = actionLabel;
upgradeBanner.style.display = 'flex';
}
function hideUpgradeBanner() {
upgradeBanner.style.display = 'none';
}
// ─── Low Credit Warning ──────────────────────────────
function checkLowCredits(consumed) {
const cost = consumed || 0;
const remaining = creditsState.balance - cost;
if (remaining <= 5 && creditsState.planName === 'Free') {
// Almost out - show subtle hint in footer
creditsDisplay.textContent = `⚠ 仅剩 ${Math.max(0, remaining)} 积分 · 升级 ↑`;
creditsDisplay.style.cursor = 'pointer';
creditsDisplay.onclick = () => showLowCreditsPrompt();
}
}
function showLowCreditsPrompt() {
showCreditErrorBanner('体验积分即将用完,升级后继续使用', '查看套餐');
}
// ─── Upgrade Banner ──────────────────────────────────
upgradeBannerBtn.addEventListener('click', () => {
showUpgradeModal();
});
upgradeBannerClose.addEventListener('click', () => {
hideUpgradeBanner();
});
// ─── Upgrade Modal ───────────────────────────────────
function showUpgradeModal() {
upgradeModalOverlay.style.display = 'flex';
}
function hideUpgradeModal() {
upgradeModalOverlay.style.display = 'none';
}
modalClose.addEventListener('click', hideUpgradeModal);
upgradeModalOverlay.addEventListener('click', (e) => {
if (e.target === upgradeModalOverlay) hideUpgradeModal();
});
// ─── Package Modal (low-commitment) ──────────────────
function showPackageModal() {
packageModalOverlay.style.display = 'flex';
}
function hidePackageModal() {
packageModalOverlay.style.display = 'none';
}
pkgModalClose.addEventListener('click', hidePackageModal);
packageModalOverlay.addEventListener('click', (e) => {
if (e.target === packageModalOverlay) hidePackageModal();
});
// ─── Plan/Pkg button handlers ────────────────────────
function setupUpgradeUI() {
document.querySelectorAll('.plan-btn[data-plan]').forEach(btn => {
btn.addEventListener('click', () => {
const plan = btn.dataset.plan;
// Open backend payment page or settings page
// For now, open the workspace credits page
chrome.tabs.create({
url: `${settingsUrl.value?.replace(/\/+$/, '') || 'https://trade.yuzhiran.com'}/workspace/credits`,
});
hideUpgradeModal();
});
});
document.querySelectorAll('.plan-btn[data-pkg]').forEach(btn => {
btn.addEventListener('click', () => {
const pkg = btn.dataset.pkg;
chrome.tabs.create({
url: `${settingsUrl.value?.replace(/\/+$/, '') || 'https://trade.yuzhiran.com'}/workspace/credits`,
});
hidePackageModal();
});
});
// Package link in upgrade modal footer
const pkgLink = document.createElement('a');
pkgLink.href = '#';
pkgLink.textContent = '购买积分包(低至 ¥2.9';
pkgLink.style.cssText = 'font-size:12px;color:#2563eb;cursor:pointer;';
pkgLink.onclick = (e) => {
e.preventDefault();
hideUpgradeModal();
showPackageModal();
};
document.querySelector('.modal-footer .hint')?.after(pkgLink);
}
// ─── Loading / Result helpers ────────────────────────
function setLoading(btn, loading) {
btn.disabled = loading;
btn.textContent = loading ? '处理中...' : btn.dataset.originalText || btn.textContent;
if (!btn.dataset.originalText) btn.dataset.originalText = btn.textContent;
}
function showResult(el, html) {
el.innerHTML = html;
}
function showError(el, msg) {
el.innerHTML = `<div class="error">${escapeHtml(msg)}</div>`;
}
function handleApiError(el, err) {
if (err instanceof ApiError && err.isCreditError) {
// 402 - out of credits: show result + upgrade banner
showResult(el, `<div class="error credit-error">
⚡ 积分不足<br>
<span class="credit-error-hint">${escapeHtml(err.message)}</span>
</div>`);
showCreditErrorBanner('积分不足,升级后继续使用');
} else {
showError(el, err.message);
}
}
function escapeHtml(str) {
const d = document.createElement('div');
d.textContent = str;
return d.innerHTML;
}
// ─── Tabs ────────────────────────────────────────────
function setupTabs() {
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
tab.classList.add('active');
const target = document.getElementById(`tab-${tab.dataset.tab}`);
if (target) target.classList.add('active');
});
});
}
// ─── Settings ────────────────────────────────────────
settingsLogin.addEventListener('click', async () => {
const url = settingsUrl.value.trim();
const username = settingsUser.value.trim();
const password = settingsPass.value.trim();
if (!url) { settingsStatus.textContent = '请输入 API 地址'; settingsStatus.className = 'status error'; return; }
if (!username || !password) { settingsStatus.textContent = '请输入用户名和密码'; settingsStatus.className = 'status error'; return; }
setLoading(settingsLogin, true);
settingsStatus.textContent = '';
try {
const token = await login(username, password, url);
await chrome.storage.local.set({
trademate_api_url: url,
trademate_token: token,
});
settingsStatus.textContent = '登录成功!';
settingsStatus.className = 'status success';
setTimeout(() => {
showScreen('main');
refreshCredits();
}, 800);
} catch (err) {
settingsStatus.textContent = err.message;
settingsStatus.className = 'status error';
} finally {
setLoading(settingsLogin, false);
}
});
btnSettings.addEventListener('click', () => {
settingsPass.value = '';
showScreen('settings');
});
// ─── Action Handlers ─────────────────────────────────
function setupActions() {
// Translate
translateBtn.addEventListener('click', async () => {
const text = translateInput.value.trim();
if (!text) return;
setLoading(translateBtn, true);
showResult(translateResult, '<div class="loading"><span class="spinner"></span>翻译中...</div>');
try {
const result = await translate(text, translateTarget.value);
const translated = result.translated_text || '(无结果)';
showResult(translateResult, `
<button class="copy-btn" onclick="navigator.clipboard.writeText(this.parentElement.querySelector('.t-text').textContent)">复制</button>
<div class="t-text">${escapeHtml(translated)}</div>
${result.provider_used ? `<div style="color:#94a3b8;font-size:11px;margin-top:4px;">provider: ${result.provider_used}</div>` : ''}
`);
// Refresh credits (translation may consume daily free or credits)
refreshCredits();
} catch (err) {
handleApiError(translateResult, err);
} finally {
setLoading(translateBtn, false);
}
});
// Reply
replyBtn.addEventListener('click', async () => {
const inquiry = replyInput.value.trim();
if (!inquiry) return;
setLoading(replyBtn, true);
showResult(replyResult, '<div class="loading"><span class="spinner"></span>生成中...</div>');
try {
const result = await generateReply(inquiry, replyTone.value);
const suggestions = result.suggestions || result.results || [];
let html = suggestions.map((s, i) => `
<div class="item">
<div class="item-label">回复 ${i + 1}${s.tone ? ` (${s.tone})` : ''}</div>
<button class="copy-btn" onclick="navigator.clipboard.writeText(this.parentElement.querySelector('.r-text').textContent)">复制</button>
<div class="r-text">${escapeHtml(s.reply || s.content || s)}</div>
</div>
`).join('');
if (!html) html = '(无回复建议)';
showResult(replyResult, html);
refreshCredits();
} catch (err) {
handleApiError(replyResult, err);
} finally {
setLoading(replyBtn, false);
}
});
// Discovery
discoveryBtn.addEventListener('click', async () => {
const product = discoveryProduct.value.trim();
if (!product) return;
setLoading(discoveryBtn, true);
showResult(discoveryResult, '<div class="loading"><span class="spinner"></span>搜索中...</div>');
try {
const result = await searchLeads(product, discoveryMarket.value.trim() || 'US');
const leads = result.data || result.results || [];
// Response may include credits_remaining
if (result.credits_remaining !== undefined) {
creditsState.balance = result.credits_remaining;
updateCreditsDisplay();
checkLowCredits(0);
}
if (!Array.isArray(leads)) {
showResult(discoveryResult, escapeHtml(JSON.stringify(leads, null, 2)));
return;
}
let html = leads.map((l, i) => `
<div class="item">
<div class="item-label">${escapeHtml(l.title || l.company_name || `结果 ${i+1}`)}</div>
<div>${escapeHtml(l.description || l.snippet || '')}</div>
${l.url ? `<div><a href="${escapeHtml(l.url)}" target="_blank" style="color:#2563eb;font-size:12px;">${escapeHtml(l.url)}</a></div>` : ''}
${l.emails?.length ? `<div style="font-size:12px;color:#64748b;">📧 ${l.emails.join(', ')}</div>` : ''}
${l.relevance_score ? `<div style="font-size:11px;color:#94a3b8;">评分: ${l.relevance_score}</div>` : ''}
</div>
`).join('');
if (!html) html = '未找到匹配客户';
showResult(discoveryResult, html);
refreshCredits();
} catch (err) {
handleApiError(discoveryResult, err);
} finally {
setLoading(discoveryBtn, false);
}
});
// Marketing
marketingBtn.addEventListener('click', async () => {
const name = marketingName.value.trim();
const desc = marketingDesc.value.trim();
if (!name || !desc) return;
setLoading(marketingBtn, true);
showResult(marketingResult, '<div class="loading"><span class="spinner"></span>生成中...</div>');
try {
const result = await generateMarketing(name, desc, marketingStyle.value);
const contents = result.results || [];
if (result.credits_remaining !== undefined) {
creditsState.balance = result.credits_remaining;
updateCreditsDisplay();
checkLowCredits(5);
}
let html = contents.map((c, i) => `
<div class="item">
<div class="item-label">文案 ${i + 1}</div>
<button class="copy-btn" onclick="navigator.clipboard.writeText(this.parentElement.querySelector('.m-text').textContent)">复制</button>
<div class="m-text">${escapeHtml(c.content || c)}</div>
</div>
`).join('');
if (!html) html = '(无结果)';
showResult(marketingResult, html);
refreshCredits();
} catch (err) {
handleApiError(marketingResult, err);
} finally {
setLoading(marketingBtn, false);
}
});
// Enter key support
[translateInput, replyInput, discoveryProduct, marketingName].forEach(el => {
el.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
const btn = el.closest('.tab-content')?.querySelector('.btn-primary');
if (btn) btn.click();
}
});
});
}
// Auto-init on load
init();