feat: add product strategy, agent skills (3 SKILL.md), and Chrome browser extension

This commit is contained in:
wlt
2026-06-24 10:37:58 +08:00
parent 2ccaafe470
commit 7b03d803b5
19 changed files with 2279 additions and 0 deletions
+194
View File
@@ -0,0 +1,194 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TradeMate</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div id="app">
<!-- Header -->
<div class="header">
<h1 class="logo">TradeMate</h1>
<span class="badge">外贸助手</span>
</div>
<!-- Settings Screen -->
<div id="screen-settings" class="screen">
<h2>设置</h2>
<div class="form-group">
<label>API 地址</label>
<input type="text" id="settings-url" placeholder="https://your-domain.com" />
</div>
<div class="form-group">
<label>用户名</label>
<input type="text" id="settings-username" placeholder="登录邮箱/用户名" />
</div>
<div class="form-group">
<label>密码</label>
<input type="password" id="settings-password" placeholder="登录密码" />
</div>
<button id="settings-login" class="btn btn-primary">登录并保存</button>
<p id="settings-status" class="status"></p>
<p class="hint">登录后将自动获取 Token 并保存在本地</p>
</div>
<!-- Main Screen -->
<div id="screen-main" class="screen" style="display:none;">
<!-- Tab Navigation -->
<div class="tabs">
<button class="tab active" data-tab="translate">翻译</button>
<button class="tab" data-tab="reply">回复</button>
<button class="tab" data-tab="discovery">发现</button>
<button class="tab" data-tab="marketing">营销</button>
</div>
<!-- Tab: Translate -->
<div id="tab-translate" class="tab-content active">
<textarea id="translate-input" rows="4" placeholder="输入要翻译的文字..."></textarea>
<div class="row">
<select id="translate-target">
<option value="zh">中文</option>
<option value="en">英文</option>
<option value="ja">日语</option>
<option value="ko">韩语</option>
<option value="fr">法语</option>
<option value="de">德语</option>
<option value="es">西班牙语</option>
</select>
<button id="translate-btn" class="btn btn-primary">翻译</button>
</div>
<div id="translate-result" class="result-box"></div>
</div>
<!-- Tab: Reply -->
<div id="tab-reply" class="tab-content">
<textarea id="reply-input" rows="4" placeholder="粘贴客户询盘内容..."></textarea>
<div class="row">
<select id="reply-tone">
<option value="professional">专业</option>
<option value="friendly">友好</option>
</select>
<button id="reply-btn" class="btn btn-primary">生成回复</button>
</div>
<div id="reply-result" class="result-box"></div>
</div>
<!-- Tab: Discovery -->
<div id="tab-discovery" class="tab-content">
<input type="text" id="discovery-product" placeholder="产品名称/描述" />
<div class="row">
<input type="text" id="discovery-market" placeholder="目标市场 (US)" value="US" />
<button id="discovery-btn" class="btn btn-primary">搜索</button>
</div>
<div id="discovery-result" class="result-box"></div>
</div>
<!-- Tab: Marketing -->
<div id="tab-marketing" class="tab-content">
<input type="text" id="marketing-name" placeholder="产品名称" />
<textarea id="marketing-desc" rows="3" placeholder="产品描述..."></textarea>
<div class="row">
<select id="marketing-style">
<option value="professional">专业</option>
<option value="friendly">友好</option>
<option value="luxury">高端</option>
</select>
<button id="marketing-btn" class="btn btn-primary">生成文案</button>
</div>
<div id="marketing-result" class="result-box"></div>
</div>
</div>
<!-- Footer -->
<div class="footer">
<button id="btn-settings" class="btn-link">设置</button>
<span id="credits-display" class="credits"></span>
</div>
<!-- Upgrade Banner (shown on 402 or low credits) -->
<div id="upgrade-banner" class="upgrade-banner" style="display:none;">
<span id="upgrade-banner-text" class="upgrade-banner-text">次数不足</span>
<button id="upgrade-banner-btn" class="upgrade-btn">升级</button>
<button id="upgrade-banner-close" class="upgrade-close">&times;</button>
</div>
<!-- Upgrade Modal -->
<div id="upgrade-modal-overlay" class="modal-overlay" style="display:none;">
<div class="modal">
<div class="modal-header">
<h3>升级套餐</h3>
<button id="modal-close" class="modal-close-btn">&times;</button>
</div>
<div class="modal-body">
<div class="plan-card plan-starter">
<div class="plan-name">Starter</div>
<div class="plan-price">¥9.9<small>/月</small></div>
<ul class="plan-features">
<li>200 积分/月</li>
<li>无每日限制</li>
<li>翻译 + 客户发现 + 营销</li>
</ul>
<button class="btn btn-primary plan-btn" data-plan="starter">订阅</button>
</div>
<div class="plan-card plan-pro featured">
<div class="plan-badge">推荐</div>
<div class="plan-name">Professional</div>
<div class="plan-price">¥49<small>/月</small></div>
<ul class="plan-features">
<li>1000 积分/月</li>
<li>AI 数字员工</li>
<li>团队协作 (3人)</li>
</ul>
<button class="btn btn-primary plan-btn" data-plan="pro">订阅</button>
</div>
<div class="plan-card plan-enterprise">
<div class="plan-name">Enterprise</div>
<div class="plan-price">¥99<small>/月</small></div>
<ul class="plan-features">
<li>2500 积分/月</li>
<li>不限团队</li>
<li>API 调用 + SLA</li>
</ul>
<button class="btn btn-primary plan-btn" data-plan="enterprise">订阅</button>
</div>
</div>
<div class="modal-footer">
<span class="hint">订阅后所有产品线通用(网页/插件/Skills)</span>
</div>
</div>
</div>
<!-- Credit Package Modal (低门槛) -->
<div id="package-modal-overlay" class="modal-overlay" style="display:none;">
<div class="modal modal-sm">
<div class="modal-header">
<h3>购买积分包</h3>
<button id="pkg-modal-close" class="modal-close-btn">&times;</button>
</div>
<div class="modal-body">
<div class="plan-card plan-starter">
<div class="plan-name">50 积分</div>
<div class="plan-price">¥2.9</div>
<button class="btn btn-primary plan-btn" data-pkg="50">购买</button>
</div>
<div class="plan-card plan-pro featured">
<div class="plan-badge">超值</div>
<div class="plan-name">200 积分</div>
<div class="plan-price">¥9.9</div>
<button class="btn btn-primary plan-btn" data-pkg="200">购买</button>
</div>
<div class="plan-card plan-enterprise">
<div class="plan-name">600 积分</div>
<div class="plan-price">¥24.9</div>
<button class="btn btn-primary plan-btn" data-pkg="600">购买</button>
</div>
</div>
</div>
</div>
</div>
<script src="popup.js" type="module"></script>
</body>
</html>
+504
View File
@@ -0,0 +1,504 @@
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 400px;
min-height: 480px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-size: 14px;
color: #333;
background: #f8fafc;
}
#app {
display: flex;
flex-direction: column;
height: 100%;
min-height: 480px;
}
/* Header */
.header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: linear-gradient(135deg, #2563eb, #1d4ed8);
color: #fff;
}
.logo {
font-size: 18px;
font-weight: 700;
letter-spacing: 0.5px;
}
.badge {
font-size: 11px;
background: rgba(255,255,255,0.2);
padding: 2px 8px;
border-radius: 10px;
}
/* Screens */
.screen {
flex: 1;
padding: 16px;
overflow-y: auto;
}
/* Tabs */
.tabs {
display: flex;
gap: 4px;
margin-bottom: 12px;
background: #e8edf5;
border-radius: 8px;
padding: 4px;
}
.tab {
flex: 1;
padding: 8px 4px;
border: none;
background: transparent;
border-radius: 6px;
font-size: 13px;
cursor: pointer;
color: #64748b;
transition: all 0.2s;
font-weight: 500;
}
.tab:hover {
color: #2563eb;
background: rgba(37, 99, 235, 0.08);
}
.tab.active {
background: #fff;
color: #2563eb;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Form Elements */
input, textarea, select {
width: 100%;
padding: 10px 12px;
border: 1px solid #d1d5db;
border-radius: 8px;
font-size: 13px;
font-family: inherit;
outline: none;
transition: border-color 0.2s;
background: #fff;
}
input:focus, textarea:focus, select:focus {
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37,99,235,0.1);
}
textarea {
resize: vertical;
min-height: 60px;
margin-bottom: 8px;
}
input {
margin-bottom: 8px;
}
select {
margin-bottom: 0;
cursor: pointer;
}
.row {
display: flex;
gap: 8px;
align-items: flex-start;
}
.row select {
flex: 1;
}
.row input {
flex: 1;
margin-bottom: 0;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.btn-primary {
background: #2563eb;
color: #fff;
}
.btn-primary:hover {
background: #1d4ed8;
}
.btn-primary:active {
transform: scale(0.97);
}
.btn-link {
background: none;
border: none;
color: #64748b;
font-size: 13px;
cursor: pointer;
padding: 4px 8px;
}
.btn-link:hover {
color: #2563eb;
}
/* Result Box */
.result-box {
margin-top: 8px;
padding: 12px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
min-height: 40px;
font-size: 13px;
line-height: 1.6;
white-space: pre-wrap;
word-break: break-word;
}
.result-box:empty {
display: none;
}
.result-box .loading {
color: #64748b;
display: flex;
align-items: center;
gap: 8px;
}
.result-box .error {
color: #dc2626;
}
.result-box .item {
padding: 8px 0;
border-bottom: 1px solid #f0f0f0;
}
.result-box .item:last-child {
border-bottom: none;
}
.result-box .item-label {
font-weight: 600;
color: #2563eb;
margin-bottom: 4px;
}
.spinner {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid #e0e0e0;
border-top-color: #2563eb;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Settings */
.form-group {
margin-bottom: 12px;
}
.form-group label {
display: block;
font-size: 12px;
font-weight: 600;
color: #64748b;
margin-bottom: 4px;
}
.status {
margin-top: 8px;
font-size: 13px;
}
.status.success { color: #16a34a; }
.status.error { color: #dc2626; }
.hint {
font-size: 12px;
color: #94a3b8;
margin-top: 12px;
line-height: 1.5;
}
/* Footer */
.footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
border-top: 1px solid #e5e7eb;
background: #fff;
}
.credits {
font-size: 12px;
color: #94a3b8;
}
/* Inline Copy Button */
.copy-btn {
float: right;
padding: 2px 8px;
font-size: 11px;
border: 1px solid #d1d5db;
border-radius: 4px;
background: #fff;
cursor: pointer;
color: #64748b;
}
.copy-btn:hover {
background: #f0f4ff;
color: #2563eb;
}
/* ─── Upgrade Banner ─────────────────────────────── */
.upgrade-banner {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: linear-gradient(135deg, #fef3c7, #fde68a);
border-top: 1px solid #f59e0b;
font-size: 13px;
animation: slideUp 0.3s ease;
}
.upgrade-banner-text {
flex: 1;
color: #92400e;
font-weight: 500;
}
.upgrade-btn {
padding: 6px 16px;
border: none;
border-radius: 6px;
background: #f59e0b;
color: #fff;
font-size: 13px;
font-weight: 600;
cursor: pointer;
white-space: nowrap;
transition: background 0.2s;
}
.upgrade-btn:hover {
background: #d97706;
}
.upgrade-close {
background: none;
border: none;
font-size: 18px;
color: #92400e;
cursor: pointer;
padding: 0 4px;
opacity: 0.6;
}
.upgrade-close:hover {
opacity: 1;
}
@keyframes slideUp {
from { transform: translateY(100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
/* ─── Credit Error ───────────────────────────────── */
.credit-error {
padding: 12px;
background: #fffbeb;
border: 1px solid #fde68a;
border-radius: 8px;
text-align: center;
font-weight: 500;
}
.credit-error-hint {
display: block;
margin-top: 4px;
font-size: 12px;
font-weight: 400;
color: #92400e;
}
/* ─── Modal ───────────────────────────────────────── */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
animation: fadeIn 0.2s ease;
}
.modal {
background: #fff;
border-radius: 16px;
width: 360px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 16px 48px rgba(0,0,0,0.2);
animation: scaleIn 0.25s ease;
}
.modal-sm {
width: 300px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px 0;
}
.modal-header h3 {
font-size: 16px;
color: #1e293b;
}
.modal-close-btn {
background: none;
border: none;
font-size: 22px;
color: #94a3b8;
cursor: pointer;
padding: 0 4px;
}
.modal-close-btn:hover {
color: #64748b;
}
.modal-body {
padding: 16px 20px;
display: flex;
flex-direction: column;
gap: 12px;
}
.modal-footer {
padding: 8px 20px 16px;
text-align: center;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes scaleIn {
from { transform: scale(0.9); opacity: 0; }
to { transform: scale(1); opacity: 1; }
}
/* ─── Plan Cards ──────────────────────────────────── */
.plan-card {
padding: 14px 16px;
border: 1px solid #e5e7eb;
border-radius: 12px;
text-align: center;
position: relative;
transition: border-color 0.2s, box-shadow 0.2s;
}
.plan-card:hover {
border-color: #2563eb;
box-shadow: 0 2px 8px rgba(37,99,235,0.1);
}
.plan-card.featured {
border-color: #2563eb;
background: #f8faff;
}
.plan-badge {
position: absolute;
top: -10px;
right: 16px;
background: #2563eb;
color: #fff;
font-size: 11px;
padding: 2px 10px;
border-radius: 10px;
font-weight: 600;
}
.plan-name {
font-size: 14px;
font-weight: 700;
color: #1e293b;
margin-bottom: 4px;
}
.plan-price {
font-size: 24px;
font-weight: 800;
color: #2563eb;
margin-bottom: 8px;
}
.plan-price small {
font-size: 13px;
font-weight: 400;
color: #64748b;
}
.plan-features {
list-style: none;
font-size: 12px;
color: #64748b;
line-height: 1.8;
margin-bottom: 10px;
}
.plan-btn {
width: 100%;
}
+471
View File
@@ -0,0 +1,471 @@
/**
* 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();