// ================================================================
// 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 = '';
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 = '';
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();
})();