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