feat: add product strategy, agent skills (3 SKILL.md), and Chrome browser extension
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>TradeMate 浏览器插件安装指南</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, 'PingFang SC', sans-serif; max-width: 720px; margin: 40px auto; padding: 0 20px; color: #333; line-height: 1.6; }
|
||||
h1 { color: #2563eb; }
|
||||
.step { display: flex; gap: 16px; margin-bottom: 24px; padding: 16px; background: #f8fafc; border-radius: 12px; }
|
||||
.num { width: 32px; height: 32px; background: #2563eb; color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 700; flex-shrink: 0; }
|
||||
code { background: #e8edf5; padding: 2px 8px; border-radius: 4px; font-size: 13px; color: #2563eb; }
|
||||
img { max-width: 100%; border: 1px solid #e0e0e0; border-radius: 8px; margin: 8px 0; }
|
||||
.tip { background: #fef3c7; border-left: 4px solid #f59e0b; padding: 12px 16px; border-radius: 8px; font-size: 14px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>TradeMate 浏览器插件</h1>
|
||||
<p>Chrome 扩展 — 划词翻译、客户发现、营销生成,与网页工作台数据互通</p>
|
||||
|
||||
<h2>安装步骤</h2>
|
||||
|
||||
<div class="step">
|
||||
<div class="num">1</div>
|
||||
<div>
|
||||
<strong>下载项目</strong>
|
||||
<p>克隆或下载 <code>trade-assistant</code> 项目,或直接下载 <code>browser-extension/</code> 目录</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="num">2</div>
|
||||
<div>
|
||||
<strong>打开扩展管理页面</strong>
|
||||
<p>Chrome 地址栏输入 <code>chrome://extensions/</code> 并回车</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="num">3</div>
|
||||
<div>
|
||||
<strong>开启开发者模式</strong>
|
||||
<p>页面右上角"开发者模式"开关 → 打开</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="num">4</div>
|
||||
<div>
|
||||
<strong>加载扩展</strong>
|
||||
<p>点击"加载已解压的扩展程序" → 选择项目的 <code>browser-extension/</code> 目录</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="step">
|
||||
<div class="num">5</div>
|
||||
<div>
|
||||
<strong>配置使用</strong>
|
||||
<p>点击浏览器工具栏的 TradeMate 图标 → 输入你的 API 地址和登录凭据</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tip">
|
||||
<strong>提示:</strong>登录后自动保存 Token,所有功能使用你的 TradeMate 账号积分。
|
||||
一个账号在网页端、插件端通用。
|
||||
</div>
|
||||
|
||||
<h2>功能一览</h2>
|
||||
<ul>
|
||||
<li><strong>翻译</strong> — 输入翻译 + 选中文本右键翻译</li>
|
||||
<li><strong>智能回复</strong> — 根据客户询盘生成回复建议</li>
|
||||
<li><strong>客户发现</strong> — Google 搜索潜在客户</li>
|
||||
<li><strong>营销文案</strong> — AI 生成产品营销内容</li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,64 @@
|
||||
# TradeMate 浏览器插件
|
||||
|
||||
AI 外贸助手 Chrome 扩展 — 翻译、客户发现、营销文案生成。
|
||||
|
||||
## 功能
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 🌐 翻译 | 中英互译 + 多语种,支持划词右键翻译 |
|
||||
| 💬 智能回复 | 根据客户询盘生成专业/友好回复建议 |
|
||||
| 🔍 客户发现 | Google 搜索潜在客户,提取联系方式 |
|
||||
| 📝 营销文案 | 产品描述营销文案生成 |
|
||||
|
||||
## 安装
|
||||
|
||||
### 开发者模式
|
||||
|
||||
1. 打开 Chrome 浏览器,进入 `chrome://extensions/`
|
||||
2. 开启"开发者模式"
|
||||
3. 点击"加载已解压的扩展程序",选择 `browser-extension/` 目录
|
||||
|
||||
### Chrome Web Store(待上架)
|
||||
|
||||
*Coming soon*
|
||||
|
||||
## 使用
|
||||
|
||||
1. 点击浏览器工具栏的 TradeMate 图标
|
||||
2. 首次使用需在设置页面输入 API 地址和登录凭据
|
||||
3. 登录后自动保存 Token,下次直接使用
|
||||
|
||||
### 划词翻译
|
||||
|
||||
选中网页上的文字 → 右键 → "TradeMate 翻译选中文本"
|
||||
|
||||
## 配置要求
|
||||
|
||||
需要一个正在运行的 TradeMate 后端实例:
|
||||
- API 地址:`https://your-domain.com`
|
||||
- 登录凭据:TradeMate 平台的用户名和密码
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Chrome Extension Manifest V3
|
||||
- Vanilla JS (无构建依赖)
|
||||
- 调用 TradeMate 后端 API
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
browser-extension/
|
||||
├── manifest.json # 扩展配置
|
||||
├── popup/ # 弹出窗口
|
||||
│ ├── index.html # UI 主界面
|
||||
│ ├── popup.js # 交互逻辑
|
||||
│ └── popup.css # 样式
|
||||
├── background/ # Service Worker
|
||||
│ └── background.js # 后台处理(右键菜单)
|
||||
├── content/ # 内容脚本
|
||||
│ └── content.js # 页面内浮动提示框
|
||||
├── api/ # API 客户端
|
||||
│ └── client.js # 后端 API 封装
|
||||
└── icons/ # 图标
|
||||
```
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* TradeMate API Client
|
||||
* Communicates with TradeMate backend from the browser extension.
|
||||
* All functions are async, return parsed JSON or throw on error.
|
||||
* Errors from 402/403 carry structured data for upgrade prompts.
|
||||
*/
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
API_URL: 'trademate_api_url',
|
||||
TOKEN: 'trademate_token',
|
||||
};
|
||||
|
||||
export class ApiError extends Error {
|
||||
constructor(message, status, data = {}) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
this.status = status;
|
||||
this.data = data; // parsed response body (may contain credits_remaining)
|
||||
this.isCreditError = status === 402;
|
||||
}
|
||||
}
|
||||
|
||||
async function getConfig() {
|
||||
const { apiUrl, token } = await chrome.storage.local.get([STORAGE_KEYS.API_URL, STORAGE_KEYS.TOKEN]);
|
||||
if (!apiUrl) throw new ApiError('请先设置 API 地址', 0);
|
||||
if (!token) throw new ApiError('请先设置 API Token', 0);
|
||||
return { apiUrl: apiUrl.replace(/\/+$/, ''), token };
|
||||
}
|
||||
|
||||
async function request(path, options = {}) {
|
||||
const { apiUrl, token } = await getConfig();
|
||||
const url = `${apiUrl}${path}`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: options.method || 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
...options.headers,
|
||||
},
|
||||
body: options.body ? JSON.stringify(options.body) : undefined,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
let detail = `HTTP ${res.status}`;
|
||||
let body = {};
|
||||
try {
|
||||
body = await res.json();
|
||||
detail = body.detail || detail;
|
||||
} catch {}
|
||||
throw new ApiError(detail, res.status, body);
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/** Login: get JWT token from username + password */
|
||||
export async function login(username, password, apiUrl) {
|
||||
const url = `${apiUrl.replace(/\/+$/, '')}/api/v1/auth/login`;
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
let detail = '登录失败';
|
||||
try { const e = await res.json(); detail = e.detail || detail; } catch {}
|
||||
throw new ApiError(detail, res.status);
|
||||
}
|
||||
const data = await res.json();
|
||||
return data.access_token;
|
||||
}
|
||||
|
||||
/** Get current credit balance */
|
||||
export async function getBalance() {
|
||||
const data = await request('/api/v1/credits/balance', { method: 'GET' });
|
||||
return {
|
||||
balance: data.balance ?? 0,
|
||||
totalPurchased: data.total_purchased ?? 0,
|
||||
totalUsed: data.total_used ?? 0,
|
||||
subscription: data.subscription ?? null,
|
||||
freeTrialUsed: data.free_trial_used ?? false,
|
||||
dailyFreeTranslateCharsLeft: data.daily_free_translate_chars_left ?? 0,
|
||||
rates: data.rates ?? {},
|
||||
};
|
||||
}
|
||||
|
||||
/** Get available subscription plans */
|
||||
export async function getSubscriptionPlans() {
|
||||
return request('/api/v1/credits/subscription-plans', { method: 'GET' });
|
||||
}
|
||||
|
||||
/** Get available credit packages */
|
||||
export async function getCreditPackages() {
|
||||
return request('/api/v1/credits/packages', { method: 'GET' });
|
||||
}
|
||||
|
||||
/** Translate text */
|
||||
export async function translate(text, targetLang = 'zh', sourceLang = 'auto') {
|
||||
return request('/api/v1/translate', {
|
||||
body: { text, target_lang: targetLang, source_lang: sourceLang, context: 'trade' },
|
||||
});
|
||||
}
|
||||
|
||||
/** Generate reply suggestions */
|
||||
export async function generateReply(inquiry, tone = 'professional', count = 3) {
|
||||
return request('/api/v1/translate/reply', {
|
||||
body: { inquiry, tone, count },
|
||||
});
|
||||
}
|
||||
|
||||
/** Search for potential customers */
|
||||
export async function searchLeads(productDescription, targetMarket = 'US') {
|
||||
return request('/api/v1/discovery/search', {
|
||||
body: { product_description: productDescription, target_market: targetMarket },
|
||||
});
|
||||
}
|
||||
|
||||
/** Generate marketing copy */
|
||||
export async function generateMarketing(productName, description, style = 'professional', count = 3) {
|
||||
return request('/api/v1/marketing/generate', {
|
||||
body: {
|
||||
product_name: productName,
|
||||
description,
|
||||
style,
|
||||
count,
|
||||
target: 'US importers',
|
||||
language: 'en',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Generate keywords */
|
||||
export async function generateKeywords(productName, description, count = 10) {
|
||||
return request('/api/v1/marketing/keywords', {
|
||||
body: { product_name: productName, description, count, language: 'en' },
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* TradeMate Background Service Worker
|
||||
* Handles context menus and clipboard operations.
|
||||
*/
|
||||
|
||||
import { translate } from '../api/client.js';
|
||||
|
||||
chrome.runtime.onInstalled.addListener(() => {
|
||||
chrome.contextMenus.create({
|
||||
id: 'translate-selection',
|
||||
title: 'TradeMate 翻译选中文本',
|
||||
contexts: ['selection'],
|
||||
});
|
||||
});
|
||||
|
||||
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
|
||||
if (info.menuItemId === 'translate-selection') {
|
||||
const selectedText = info.selectionText.trim();
|
||||
if (!selectedText) return;
|
||||
|
||||
try {
|
||||
const result = await translate(selectedText, 'zh');
|
||||
const translated = result.translated_text || '翻译失败';
|
||||
|
||||
// Try to send to content script for display
|
||||
if (tab?.id) {
|
||||
chrome.tabs.sendMessage(tab.id, {
|
||||
action: 'showTranslation',
|
||||
original: selectedText,
|
||||
translated,
|
||||
}).catch(() => {
|
||||
// Fallback: show in notification
|
||||
notifyResult(translated);
|
||||
});
|
||||
} else {
|
||||
notifyResult(translated);
|
||||
}
|
||||
} catch (err) {
|
||||
notifyResult(`错误: ${err.message}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function notifyResult(text) {
|
||||
chrome.notifications.create({
|
||||
type: 'basic',
|
||||
iconUrl: 'icons/icon48.png',
|
||||
title: 'TradeMate 翻译',
|
||||
message: text.slice(0, 200),
|
||||
});
|
||||
}
|
||||
|
||||
// Listen for popup API calls that need background processing
|
||||
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
||||
if (request.action === 'translateContext') {
|
||||
translate(request.text, request.targetLang)
|
||||
.then(result => sendResponse({ ok: true, data: result }))
|
||||
.catch(err => sendResponse({ ok: false, error: err.message }));
|
||||
return true; // Keep channel open for async response
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* TradeMate Content Script
|
||||
* Displays translation results on the page via a floating tooltip.
|
||||
*/
|
||||
|
||||
chrome.runtime.onMessage.addListener((request) => {
|
||||
if (request.action === 'showTranslation') {
|
||||
showTooltip(request.original, request.translated);
|
||||
}
|
||||
});
|
||||
|
||||
function showTooltip(original, translated) {
|
||||
const existing = document.getElementById('trademate-tooltip');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const tooltip = document.createElement('div');
|
||||
tooltip.id = 'trademate-tooltip';
|
||||
tooltip.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 999999;
|
||||
max-width: 400px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
animation: slideIn 0.3s ease;
|
||||
`;
|
||||
|
||||
tooltip.innerHTML = `
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
|
||||
<strong style="color:#2563eb;">TradeMate 翻译</strong>
|
||||
<button id="trademate-close" style="
|
||||
background:none;border:none;font-size:18px;cursor:pointer;color:#999;padding:0 4px;
|
||||
">×</button>
|
||||
</div>
|
||||
<div style="margin-bottom:8px;color:#666;font-size:13px;">${escapeHtml(original)}</div>
|
||||
<div style="color:#333;font-size:15px;font-weight:500;">${escapeHtml(translated)}</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(tooltip);
|
||||
|
||||
document.getElementById('trademate-close').onclick = () => tooltip.remove();
|
||||
|
||||
// Auto-remove after 15 seconds
|
||||
setTimeout(() => { if (tooltip.parentNode) tooltip.remove(); }, 15000);
|
||||
|
||||
// Add animation style
|
||||
if (!document.getElementById('trademate-style')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'trademate-style';
|
||||
style.textContent = `
|
||||
@keyframes slideIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 360 B |
Binary file not shown.
|
After Width: | Height: | Size: 82 B |
Binary file not shown.
|
After Width: | Height: | Size: 157 B |
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "TradeMate - AI 外贸助手",
|
||||
"version": "1.0.0",
|
||||
"description": "AI-powered foreign trade assistant: translate inquiries, discover customers, generate marketing content",
|
||||
"permissions": [
|
||||
"storage",
|
||||
"contextMenus",
|
||||
"activeTab"
|
||||
],
|
||||
"host_permissions": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"action": {
|
||||
"default_popup": "popup/index.html",
|
||||
"default_title": "TradeMate",
|
||||
"default_icon": {
|
||||
"16": "icons/icon16.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
},
|
||||
"background": {
|
||||
"service_worker": "background/background.js",
|
||||
"type": "module"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["content/content.js"],
|
||||
"run_at": "document_end"
|
||||
}
|
||||
],
|
||||
"icons": {
|
||||
"16": "icons/icon16.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
}
|
||||
}
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
# Package TradeMate browser extension for Chrome Web Store or manual install
|
||||
# Usage: bash package.sh
|
||||
|
||||
set -e
|
||||
DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cd "$DIR"
|
||||
|
||||
OUTPUT="trademate-extension.zip"
|
||||
|
||||
# Clean previous
|
||||
rm -f "$OUTPUT"
|
||||
|
||||
# Zip required files
|
||||
zip -r "$OUTPUT" \
|
||||
manifest.json \
|
||||
popup/index.html popup/popup.js popup/popup.css \
|
||||
background/background.js \
|
||||
content/content.js \
|
||||
api/client.js \
|
||||
icons/icon16.png icons/icon48.png icons/icon128.png \
|
||||
-x "*.DS_Store" -x "*/.git/*"
|
||||
|
||||
echo "✅ Packaged: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))"
|
||||
echo ""
|
||||
echo "To install:"
|
||||
echo " 1. Open chrome://extensions/"
|
||||
echo " 2. Enable Developer mode"
|
||||
echo " 3. Drag $OUTPUT onto the page"
|
||||
echo " OR: Load unpacked → select browser-extension/ directory"
|
||||
@@ -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">×</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">×</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">×</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>
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user