feat:助手首次改版
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
// ================================================================
|
||||
// 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 contextProductNameEl = document.getElementById('aiContextProductName');
|
||||
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 (contextProductNameEl) {
|
||||
const name = (productNameInput && productNameInput.value || '').trim();
|
||||
contextProductNameEl.textContent = name || '--';
|
||||
}
|
||||
if (contextModelCodeEl) {
|
||||
const model = (modelCodeInput && modelCodeInput.value || '').trim();
|
||||
contextModelCodeEl.textContent = model || '--';
|
||||
}
|
||||
}
|
||||
|
||||
async function copyText(text) {
|
||||
const value = (text || '').trim();
|
||||
if (!value) {
|
||||
setStatus('没有可复制的内容', true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
setStatus('已复制');
|
||||
} catch (err) {
|
||||
setStatus('复制失败,请手动选择文本', true);
|
||||
}
|
||||
}
|
||||
|
||||
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 = '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); });
|
||||
|
||||
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,
|
||||
product_name: (productNameInput && productNameInput.value || '').trim(),
|
||||
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 : '');
|
||||
});
|
||||
});
|
||||
|
||||
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') || '');
|
||||
});
|
||||
}
|
||||
|
||||
if (copyAllTagsBtn) {
|
||||
copyAllTagsBtn.addEventListener('click', function () {
|
||||
copyText(lastTagsRu.join(', '));
|
||||
});
|
||||
}
|
||||
|
||||
if (productNameInput) {
|
||||
productNameInput.addEventListener('input', syncContextHints);
|
||||
}
|
||||
if (modelCodeInput) {
|
||||
modelCodeInput.addEventListener('input', syncContextHints);
|
||||
}
|
||||
syncContextHints();
|
||||
loadModels();
|
||||
})();
|
||||
Reference in New Issue
Block a user