/** * 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 = `
${escapeHtml(msg)}
`; } function handleApiError(el, err) { if (err instanceof ApiError && err.isCreditError) { // 402 - out of credits: show result + upgrade banner showResult(el, `
⚡ 积分不足
${escapeHtml(err.message)}
`); 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, '
翻译中...
'); try { const result = await translate(text, translateTarget.value); const translated = result.translated_text || '(无结果)'; showResult(translateResult, `
${escapeHtml(translated)}
${result.provider_used ? `
provider: ${result.provider_used}
` : ''} `); // 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, '
生成中...
'); try { const result = await generateReply(inquiry, replyTone.value); const suggestions = result.suggestions || result.results || []; let html = suggestions.map((s, i) => `
回复 ${i + 1}${s.tone ? ` (${s.tone})` : ''}
${escapeHtml(s.reply || s.content || s)}
`).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, '
搜索中...
'); 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) => `
${escapeHtml(l.title || l.company_name || `结果 ${i+1}`)}
${escapeHtml(l.description || l.snippet || '')}
${l.url ? `
${escapeHtml(l.url)}
` : ''} ${l.emails?.length ? `
📧 ${l.emails.join(', ')}
` : ''} ${l.relevance_score ? `
评分: ${l.relevance_score}
` : ''}
`).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, '
生成中...
'); 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) => `
文案 ${i + 1}
${escapeHtml(c.content || c)}
`).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();