Files
ozon-seller-kit/web/js/ai-copy.js
T
2026-08-10 14:19:08 +08:00

369 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ================================================================
// 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();
})();