369 lines
14 KiB
JavaScript
369 lines
14 KiB
JavaScript
// ================================================================
|
||
// Ozon Seller Kit - 俄文文案(调用本地 FastAPI → 大模型)
|
||
// ================================================================
|
||
(function () {
|
||
const API_BASE = window.location.origin;
|
||
const MODEL_STORAGE_KEY = 'ozonAiCopyModel';
|
||
|
||
const sourceTextEl = document.getElementById('aiSourceText');
|
||
const generateBtn = document.getElementById('aiGenerateBtn');
|
||
const toggleBtn = document.getElementById('aiToggleBtn');
|
||
const bodyEl = document.getElementById('aiCopyBody');
|
||
const statusEl = document.getElementById('aiCopyStatus');
|
||
const modelSelectEl = document.getElementById('aiModelSelect');
|
||
const contextModelCodeEl = document.getElementById('aiContextModelCode');
|
||
const titlesContainerEl = document.getElementById('aiTitlesContainer');
|
||
const tagsContainerEl = document.getElementById('aiTagsContainer');
|
||
const copyAllTagsBtn = document.getElementById('aiCopyAllTagsBtn');
|
||
|
||
const descRuEl = document.getElementById('aiDescRu');
|
||
const descZhEl = document.getElementById('aiDescZh');
|
||
|
||
const productNameInput = document.getElementById('productName');
|
||
const modelCodeInput = document.getElementById('modelCode');
|
||
|
||
let lastTagsRu = [];
|
||
|
||
if (!sourceTextEl || !generateBtn) {
|
||
return;
|
||
}
|
||
|
||
function setStatus(message, isError) {
|
||
if (!statusEl) return;
|
||
statusEl.textContent = message || '';
|
||
statusEl.className = isError
|
||
? 'text-sm text-danger'
|
||
: 'text-sm text-gray-500';
|
||
}
|
||
|
||
function syncContextHints() {
|
||
// 注:生成文案不再带入「商品名」,故此处只同步型号提示
|
||
if (contextModelCodeEl) {
|
||
const model = (modelCodeInput && modelCodeInput.value || '').trim();
|
||
contextModelCodeEl.textContent = model || '--';
|
||
}
|
||
}
|
||
|
||
async function copyText(text, btn) {
|
||
const value = (text || '').trim();
|
||
if (!value) {
|
||
setStatus('没有可复制的内容', true);
|
||
return;
|
||
}
|
||
try {
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
await navigator.clipboard.writeText(value);
|
||
} else {
|
||
const ta = document.createElement('textarea');
|
||
ta.value = value;
|
||
ta.style.position = 'fixed';
|
||
ta.style.left = '-9999px';
|
||
document.body.appendChild(ta);
|
||
ta.select();
|
||
document.execCommand('copy');
|
||
document.body.removeChild(ta);
|
||
}
|
||
setStatus('已复制');
|
||
showCopyFeedback(btn);
|
||
} catch (err) {
|
||
setStatus('复制失败,请手动选择文本', true);
|
||
}
|
||
}
|
||
|
||
function showCopyFeedback(btn) {
|
||
if (!btn || btn.dataset.copyFeedbackActive) return;
|
||
|
||
if (btn.classList.contains('ai-tag-chip')) {
|
||
btn.classList.add('is-copied');
|
||
setTimeout(function () {
|
||
btn.classList.remove('is-copied');
|
||
}, 1200);
|
||
return;
|
||
}
|
||
|
||
const originalText = btn.textContent;
|
||
btn.dataset.copyFeedbackActive = '1';
|
||
btn.disabled = true;
|
||
btn.textContent = '已复制';
|
||
btn.classList.add('is-copied');
|
||
|
||
setTimeout(function () {
|
||
btn.textContent = originalText;
|
||
btn.disabled = false;
|
||
btn.classList.remove('is-copied');
|
||
delete btn.dataset.copyFeedbackActive;
|
||
}, 1200);
|
||
}
|
||
|
||
function toStringList(value) {
|
||
return Array.isArray(value)
|
||
? value.map(function (item) { return String(item).trim(); })
|
||
: [];
|
||
}
|
||
|
||
function renderTitles(titlesRu, titlesZh) {
|
||
if (!titlesContainerEl) return;
|
||
const ruList = toStringList(titlesRu).filter(Boolean);
|
||
const zhList = toStringList(titlesZh);
|
||
|
||
titlesContainerEl.innerHTML = '';
|
||
if (!ruList.length) {
|
||
const empty = document.createElement('p');
|
||
empty.className = 'text-sm text-gray-500';
|
||
empty.textContent = '生成后将在此展示 2 个推荐标题';
|
||
titlesContainerEl.appendChild(empty);
|
||
return;
|
||
}
|
||
|
||
ruList.forEach(function (ru, index) {
|
||
const card = document.createElement('div');
|
||
card.className = 'ai-title-card';
|
||
|
||
const head = document.createElement('div');
|
||
head.className = 'ai-title-card-head';
|
||
|
||
const label = document.createElement('span');
|
||
label.className = 'ai-title-card-index';
|
||
label.textContent = '方案 ' + (index + 1) + ' · ' + ru.length + ' 字符';
|
||
|
||
const actions = document.createElement('div');
|
||
actions.className = 'flex items-center gap-2 shrink-0';
|
||
|
||
const copyBtn = document.createElement('button');
|
||
copyBtn.type = 'button';
|
||
copyBtn.className = 'ai-copy-trigger px-3 py-1.5 text-sm rounded-lg bg-primary/20 hover:bg-primary/30 text-primary transition-colors duration-200';
|
||
copyBtn.textContent = '复制俄文';
|
||
copyBtn.addEventListener('click', function () { copyText(ru, copyBtn); });
|
||
|
||
const fillBtn = document.createElement('button');
|
||
fillBtn.type = 'button';
|
||
fillBtn.className = 'px-3 py-1.5 text-sm rounded-lg bg-secondary/20 hover:bg-secondary/30 text-secondary transition-colors duration-200';
|
||
fillBtn.textContent = '填入商品名';
|
||
fillBtn.addEventListener('click', function () {
|
||
fillProductName(zhList[index] || '');
|
||
});
|
||
|
||
actions.appendChild(copyBtn);
|
||
actions.appendChild(fillBtn);
|
||
head.appendChild(label);
|
||
head.appendChild(actions);
|
||
|
||
const ruLine = document.createElement('p');
|
||
ruLine.className = 'ai-title-card-ru';
|
||
ruLine.textContent = ru;
|
||
|
||
const zhLine = document.createElement('p');
|
||
zhLine.className = 'ai-title-card-zh';
|
||
zhLine.textContent = zhList[index] || '—';
|
||
|
||
card.appendChild(head);
|
||
card.appendChild(ruLine);
|
||
card.appendChild(zhLine);
|
||
titlesContainerEl.appendChild(card);
|
||
});
|
||
}
|
||
|
||
function renderTags(tagsRu, tagsZh) {
|
||
lastTagsRu = toStringList(tagsRu).filter(Boolean);
|
||
const zhList = toStringList(tagsZh);
|
||
|
||
if (!tagsContainerEl) return;
|
||
tagsContainerEl.innerHTML = '';
|
||
|
||
if (!lastTagsRu.length) {
|
||
const empty = document.createElement('p');
|
||
empty.className = 'text-sm text-gray-500';
|
||
empty.textContent = '生成后将在此展示标签,点击可复制俄文';
|
||
tagsContainerEl.appendChild(empty);
|
||
return;
|
||
}
|
||
|
||
lastTagsRu.forEach(function (ru, index) {
|
||
const chip = document.createElement('button');
|
||
chip.type = 'button';
|
||
chip.className = 'ai-tag-chip';
|
||
chip.title = '点击复制俄文标签';
|
||
chip.setAttribute('data-tag-ru', ru);
|
||
|
||
const ruSpan = document.createElement('span');
|
||
ruSpan.className = 'ai-tag-chip-ru';
|
||
ruSpan.textContent = ru;
|
||
|
||
const sep = document.createElement('span');
|
||
sep.className = 'ai-tag-chip-sep';
|
||
sep.textContent = '|';
|
||
|
||
const zhSpan = document.createElement('span');
|
||
zhSpan.className = 'ai-tag-chip-zh';
|
||
zhSpan.textContent = zhList[index] || '—';
|
||
|
||
chip.appendChild(ruSpan);
|
||
chip.appendChild(sep);
|
||
chip.appendChild(zhSpan);
|
||
tagsContainerEl.appendChild(chip);
|
||
});
|
||
}
|
||
|
||
function fillProductName(zhTitle) {
|
||
const zh = (zhTitle || '').trim();
|
||
if (!zh) {
|
||
setStatus('该标题没有中文对照', true);
|
||
return;
|
||
}
|
||
if (!productNameInput) {
|
||
setStatus('未找到商品名字段', true);
|
||
return;
|
||
}
|
||
productNameInput.value = zh;
|
||
productNameInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||
syncContextHints();
|
||
setStatus('已填入商品名');
|
||
}
|
||
|
||
function fillResult(data) {
|
||
renderTitles(data.titles_ru, data.titles_zh);
|
||
descRuEl.textContent = data.description_ru || '';
|
||
descZhEl.textContent = data.description_zh || '';
|
||
renderTags(data.tags_ru, data.tags_zh);
|
||
}
|
||
|
||
async function loadModels() {
|
||
if (!modelSelectEl) return;
|
||
try {
|
||
const resp = await fetch(API_BASE + '/api/ai/models');
|
||
const data = await resp.json();
|
||
if (!resp.ok) {
|
||
throw new Error((data && data.detail) || ('HTTP ' + resp.status));
|
||
}
|
||
const models = Array.isArray(data.models) ? data.models : [];
|
||
const saved = localStorage.getItem(MODEL_STORAGE_KEY) || '';
|
||
const preferred = models.some(function (m) { return m.id === saved; })
|
||
? saved
|
||
: (data.default || (models[0] && models[0].id) || '');
|
||
|
||
modelSelectEl.innerHTML = '';
|
||
if (!models.length) {
|
||
modelSelectEl.innerHTML = '<option value="">无可用模型</option>';
|
||
return;
|
||
}
|
||
models.forEach(function (m) {
|
||
const opt = document.createElement('option');
|
||
opt.value = m.id;
|
||
opt.textContent = m.label || m.id;
|
||
if (m.id === preferred) opt.selected = true;
|
||
modelSelectEl.appendChild(opt);
|
||
});
|
||
localStorage.setItem(MODEL_STORAGE_KEY, modelSelectEl.value);
|
||
} catch (err) {
|
||
modelSelectEl.innerHTML = '<option value="">模型列表加载失败</option>';
|
||
setStatus(err && err.message ? err.message : '模型列表加载失败', true);
|
||
}
|
||
}
|
||
|
||
if (modelSelectEl) {
|
||
modelSelectEl.addEventListener('change', function () {
|
||
if (modelSelectEl.value) {
|
||
localStorage.setItem(MODEL_STORAGE_KEY, modelSelectEl.value);
|
||
}
|
||
});
|
||
}
|
||
|
||
if (toggleBtn && bodyEl) {
|
||
toggleBtn.addEventListener('click', function () {
|
||
const collapsed = bodyEl.classList.toggle('hidden');
|
||
toggleBtn.textContent = collapsed ? '展开' : '收起';
|
||
toggleBtn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
|
||
});
|
||
}
|
||
|
||
async function generateCopy() {
|
||
const sourceText = (sourceTextEl.value || '').trim();
|
||
if (sourceText.length < 10) {
|
||
setStatus('请先粘贴至少 10 个字符的商品资料', true);
|
||
sourceTextEl.focus();
|
||
return;
|
||
}
|
||
|
||
const payload = {
|
||
source_text: sourceText,
|
||
model_code: (modelCodeInput && modelCodeInput.value || '').trim(),
|
||
model: (modelSelectEl && modelSelectEl.value || '').trim(),
|
||
};
|
||
|
||
generateBtn.disabled = true;
|
||
const originalLabel = generateBtn.textContent;
|
||
generateBtn.textContent = '生成中…';
|
||
setStatus('正在调用大模型…');
|
||
|
||
try {
|
||
const resp = await fetch(API_BASE + '/api/ai/copy', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(payload),
|
||
});
|
||
|
||
let body = null;
|
||
try {
|
||
body = await resp.json();
|
||
} catch (parseErr) {
|
||
body = null;
|
||
}
|
||
|
||
if (!resp.ok) {
|
||
const detail = body && body.detail
|
||
? (typeof body.detail === 'string' ? body.detail : JSON.stringify(body.detail))
|
||
: ('HTTP ' + resp.status);
|
||
throw new Error(detail);
|
||
}
|
||
|
||
fillResult(body);
|
||
const usage = body.usage || {};
|
||
const modelName = body.model ? (' · ' + body.model) : '';
|
||
setStatus(
|
||
'生成完成' + modelName +
|
||
(usage.prompt_tokens || usage.completion_tokens
|
||
? '(tokens: ' + (usage.prompt_tokens || 0) + '+' + (usage.completion_tokens || 0) + ')'
|
||
: '')
|
||
);
|
||
} catch (err) {
|
||
setStatus(err && err.message ? err.message : '生成失败', true);
|
||
} finally {
|
||
generateBtn.disabled = false;
|
||
generateBtn.textContent = originalLabel;
|
||
}
|
||
}
|
||
|
||
generateBtn.addEventListener('click', generateCopy);
|
||
|
||
document.querySelectorAll('.ai-copy-btn').forEach(function (btn) {
|
||
btn.addEventListener('click', function () {
|
||
const sourceId = btn.getAttribute('data-copy-source');
|
||
const el = sourceId ? document.getElementById(sourceId) : null;
|
||
copyText(el ? el.textContent : '', btn);
|
||
});
|
||
});
|
||
|
||
if (tagsContainerEl) {
|
||
tagsContainerEl.addEventListener('click', function (event) {
|
||
const chip = event.target.closest('.ai-tag-chip');
|
||
if (!chip || !tagsContainerEl.contains(chip)) return;
|
||
copyText(chip.getAttribute('data-tag-ru') || '', chip);
|
||
});
|
||
}
|
||
|
||
if (copyAllTagsBtn) {
|
||
copyAllTagsBtn.addEventListener('click', function () {
|
||
copyText(lastTagsRu.join(', '), copyAllTagsBtn);
|
||
});
|
||
}
|
||
|
||
if (productNameInput) {
|
||
productNameInput.addEventListener('input', syncContextHints);
|
||
}
|
||
if (modelCodeInput) {
|
||
modelCodeInput.addEventListener('input', syncContextHints);
|
||
}
|
||
syncContextHints();
|
||
loadModels();
|
||
})();
|