334 lines
13 KiB
JavaScript
334 lines
13 KiB
JavaScript
/**
|
|
* TalkingQ Platform - Hotword Library Page
|
|
* 热词库管理:统一维护 ASR 热词组,供应用配置选择绑定。
|
|
*/
|
|
|
|
(function() {
|
|
function ensureHotwordGroups() {
|
|
if (!window.__agentHotwordGroups) {
|
|
window.__agentHotwordGroups = [
|
|
{
|
|
id: 'hw-001',
|
|
name: '品牌基础热词',
|
|
mode: 'manual',
|
|
words: [
|
|
{ text: 'TalkingQ', lang: '英文' },
|
|
{ text: '小Q', lang: '中文' },
|
|
{ text: '故事机', lang: '中文' },
|
|
],
|
|
updatedAt: '2026-05-20 11:57:38',
|
|
},
|
|
{
|
|
id: 'hw-002',
|
|
name: '儿童内容热词',
|
|
mode: 'manual',
|
|
words: [
|
|
{ text: '白雪公主', lang: '中文' },
|
|
{ text: '睡前故事', lang: '中文' },
|
|
],
|
|
updatedAt: '2026-05-20 12:08:16',
|
|
},
|
|
];
|
|
}
|
|
return window.__agentHotwordGroups;
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
const div = document.createElement('div');
|
|
div.textContent = value ?? '';
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function escapeAttr(value) {
|
|
return escapeHtml(value).replace(/"/g, '"').replace(/'/g, ''');
|
|
}
|
|
|
|
function formatHotwordTime(date = new Date()) {
|
|
return date.toLocaleString('zh-CN', {
|
|
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
hour: '2-digit', minute: '2-digit', second: '2-digit',
|
|
hour12: false,
|
|
}).replace(/\//g, '-');
|
|
}
|
|
|
|
function getHotwordPreview(group) {
|
|
const preview = group.words.slice(0, 2).map(word => word.text).join('、') || '暂无热词';
|
|
return group.words.length > 2 ? `${preview} 等 ${group.words.length} 个` : `${preview} · ${group.words.length} 个`;
|
|
}
|
|
|
|
function renderHotwordRows(words) {
|
|
if (!words.length) {
|
|
return `<div class="hotword-empty-row">点击“添加热词”开始配置,最多可添加 500 个热词。</div>`;
|
|
}
|
|
return words.map((word, index) => `
|
|
<div class="hotword-entry-row" data-index="${index}">
|
|
<input class="form-control form-control-sm hotword-text-input" maxlength="15" value="${escapeAttr(word.text || '')}" placeholder="热词,最多 15 字">
|
|
<select class="form-control form-control-sm hotword-lang-select">
|
|
<option value="中文" ${word.lang === '中文' ? 'selected' : ''}>中文</option>
|
|
<option value="英文" ${word.lang === '英文' ? 'selected' : ''}>英文</option>
|
|
</select>
|
|
<button class="btn btn-ghost btn-sm hotword-row-delete" type="button" data-index="${index}" style="color:var(--danger)">
|
|
<i data-lucide="trash-2" style="width:13px;height:13px"></i> 删除
|
|
</button>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function renderHotwordGroupList(groups) {
|
|
if (!groups.length) {
|
|
return `
|
|
<div class="empty-state" style="padding:48px 20px">
|
|
<i data-lucide="book-open-text" style="width:36px;height:36px;color:var(--text-muted);margin-bottom:12px"></i>
|
|
<p style="font-size:15px;font-weight:600;margin-bottom:6px">暂无热词组</p>
|
|
<p style="font-size:13px;color:var(--text-muted)">创建热词组后,可在智能体配置里选择绑定。</p>
|
|
</div>
|
|
`;
|
|
}
|
|
return groups.map(group => `
|
|
<div class="hotword-group-item" data-group-id="${group.id}">
|
|
<div class="hotword-group-main">
|
|
<div class="hotword-group-name">${escapeHtml(group.name)}</div>
|
|
<div class="hotword-group-preview">${escapeHtml(getHotwordPreview(group))}</div>
|
|
</div>
|
|
<div class="hotword-group-count">${group.words.length} 个</div>
|
|
<div class="hotword-group-time">${escapeHtml(group.updatedAt || '-')}</div>
|
|
<div class="hotword-group-actions">
|
|
<button class="btn btn-ghost btn-sm hotword-edit-btn" data-group-id="${group.id}">
|
|
<i data-lucide="edit-2" style="width:13px;height:13px"></i> 编辑
|
|
</button>
|
|
<button class="btn btn-ghost btn-sm hotword-delete-btn" data-group-id="${group.id}" style="color:var(--danger)">
|
|
<i data-lucide="trash-2" style="width:13px;height:13px"></i> 删除
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function notifyHotwordGroupsChanged() {
|
|
window.dispatchEvent(new CustomEvent('hotwordGroupsChanged', {
|
|
detail: { groups: ensureHotwordGroups() },
|
|
}));
|
|
}
|
|
|
|
function openHotwordEditor(groupId, afterSave) {
|
|
const groups = ensureHotwordGroups();
|
|
const editingGroup = groups.find(group => group.id === groupId);
|
|
let draftWords = editingGroup ? editingGroup.words.map(word => ({ ...word })) : [];
|
|
const modal = createModal({
|
|
title: editingGroup ? '编辑热词组' : '配置热词库',
|
|
size: 'xl',
|
|
cancelText: '取消',
|
|
confirmText: '确定',
|
|
content: `
|
|
<div class="hotword-editor">
|
|
<div class="form-group">
|
|
<label class="form-label">热词组名字<span class="required">*</span></label>
|
|
<input type="text" class="form-control" id="hotwordGroupNameInput" maxlength="50" value="${escapeAttr(editingGroup?.name || '')}" placeholder="请输入热词组名字,最多 50 字">
|
|
<div class="form-hint" id="hotwordGroupNameHint">${(editingGroup?.name || '').length}/50 字</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label class="form-label">添加方式</label>
|
|
<label class="radio-item selected" style="width:max-content">
|
|
<input type="radio" name="hotwordAddMode" value="manual" checked>
|
|
<span>手动添加热词</span>
|
|
</label>
|
|
<div class="form-hint">手动添加最多支持 500 个热词,每个热词最多 15 个字。</div>
|
|
</div>
|
|
<div class="hotword-editor-head">
|
|
<div>
|
|
<div class="hotword-manager-title">热词列表</div>
|
|
<div class="hotword-manager-desc" id="hotwordCountHint">${draftWords.length}/500 个</div>
|
|
</div>
|
|
<button class="btn btn-outline btn-sm" type="button" id="addHotwordRowBtn">
|
|
<i data-lucide="plus" style="width:13px;height:13px"></i> 添加热词
|
|
</button>
|
|
</div>
|
|
<div class="hotword-entry-list" id="hotwordEntryList">${renderHotwordRows(draftWords)}</div>
|
|
</div>
|
|
`,
|
|
onConfirm: async () => {
|
|
const nameInput = document.getElementById('hotwordGroupNameInput');
|
|
const groupName = nameInput?.value.trim() || '';
|
|
if (!groupName) {
|
|
Toast.warning('请填写热词组名字');
|
|
nameInput?.focus();
|
|
return false;
|
|
}
|
|
if (groupName.length > 50) {
|
|
Toast.warning('热词组名字不能超过 50 个字');
|
|
nameInput?.focus();
|
|
return false;
|
|
}
|
|
const words = readHotwordRows(modal.overlay);
|
|
if (!words.length) {
|
|
Toast.warning('请至少添加一个热词');
|
|
return false;
|
|
}
|
|
const invalid = words.find(word => !word.text || word.text.length > 15);
|
|
if (invalid) {
|
|
Toast.warning('请填写热词,且每个热词不能超过 15 个字');
|
|
return false;
|
|
}
|
|
const now = formatHotwordTime();
|
|
const payload = { name: groupName, mode: 'manual', words };
|
|
const api = typeof API !== 'undefined' ? API : window.API;
|
|
let result = null;
|
|
try {
|
|
if (editingGroup && api?.agentDev?.updateHotwordGroup) {
|
|
await api.agentDev.updateHotwordGroup(editingGroup.id, payload);
|
|
} else if (!editingGroup && api?.agentDev?.createHotwordGroup) {
|
|
result = await api.agentDev.createHotwordGroup(payload);
|
|
}
|
|
} catch (error) {
|
|
console.error('Save hotword group failed', error);
|
|
Toast.error('热词组保存失败,请稍后重试');
|
|
return false;
|
|
}
|
|
if (editingGroup) {
|
|
editingGroup.name = groupName;
|
|
editingGroup.mode = 'manual';
|
|
editingGroup.words = words;
|
|
editingGroup.updatedAt = now;
|
|
} else {
|
|
groups.unshift({
|
|
id: result?.data?.id || 'hw-' + Date.now(),
|
|
name: groupName,
|
|
mode: 'manual',
|
|
words,
|
|
updatedAt: now,
|
|
});
|
|
}
|
|
notifyHotwordGroupsChanged();
|
|
Toast.success(editingGroup ? '热词组已更新' : '热词组已创建');
|
|
afterSave?.();
|
|
},
|
|
});
|
|
|
|
const overlay = modal.overlay;
|
|
overlay.querySelector('.modal')?.classList.add('hotword-editor-modal');
|
|
const list = overlay.querySelector('#hotwordEntryList');
|
|
const countHint = overlay.querySelector('#hotwordCountHint');
|
|
|
|
function readHotwordRows(scope = overlay) {
|
|
return Array.from(scope.querySelectorAll('.hotword-entry-row')).map(row => ({
|
|
text: row.querySelector('.hotword-text-input')?.value.trim() || '',
|
|
lang: row.querySelector('.hotword-lang-select')?.value || '中文',
|
|
}));
|
|
}
|
|
|
|
function rerenderRows() {
|
|
list.innerHTML = renderHotwordRows(draftWords);
|
|
countHint.textContent = `${draftWords.length}/500 个`;
|
|
bindRowEvents();
|
|
lucide.createIcons({ elements: [overlay] });
|
|
}
|
|
|
|
function bindRowEvents() {
|
|
overlay.querySelectorAll('.hotword-row-delete').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
draftWords = readHotwordRows();
|
|
draftWords.splice(Number(btn.dataset.index), 1);
|
|
rerenderRows();
|
|
});
|
|
});
|
|
overlay.querySelectorAll('.hotword-text-input').forEach(input => {
|
|
input.addEventListener('input', () => {
|
|
if (input.value.length > 15) input.value = input.value.slice(0, 15);
|
|
});
|
|
});
|
|
}
|
|
|
|
overlay.querySelector('#addHotwordRowBtn')?.addEventListener('click', () => {
|
|
draftWords = readHotwordRows();
|
|
if (draftWords.length >= 500) {
|
|
Toast.warning('手动添加热词最多支持 500 个');
|
|
return;
|
|
}
|
|
draftWords.push({ text: '', lang: '中文' });
|
|
rerenderRows();
|
|
list.scrollTop = list.scrollHeight;
|
|
overlay.querySelector('.hotword-entry-row:last-child .hotword-text-input')?.focus();
|
|
});
|
|
overlay.querySelector('#hotwordGroupNameInput')?.addEventListener('input', e => {
|
|
overlay.querySelector('#hotwordGroupNameHint').textContent = `${e.target.value.length}/50 字`;
|
|
});
|
|
|
|
bindRowEvents();
|
|
lucide.createIcons({ elements: [overlay] });
|
|
}
|
|
|
|
window.__ensureAgentHotwordGroups = ensureHotwordGroups;
|
|
window.__getHotwordPreview = getHotwordPreview;
|
|
|
|
registerPage('hotword-library', function(container) {
|
|
function renderPage() {
|
|
const groups = ensureHotwordGroups();
|
|
container.innerHTML = `
|
|
<div class="page-header">
|
|
<div>
|
|
<div class="page-title">热词库管理</div>
|
|
<div class="page-desc">统一维护 ASR 热词组,智能体配置中只需选择绑定。</div>
|
|
</div>
|
|
<div class="page-actions">
|
|
<button class="btn btn-primary" id="createHotwordGroupPageBtn">
|
|
<i data-lucide="plus"></i> 创建热词组
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card" style="padding:16px">
|
|
<div class="hotword-group-list">${renderHotwordGroupList(groups)}</div>
|
|
</div>
|
|
`;
|
|
lucide.createIcons({ elements: [container] });
|
|
bindEvents();
|
|
}
|
|
|
|
function bindEvents() {
|
|
container.querySelector('#createHotwordGroupPageBtn')?.addEventListener('click', () => {
|
|
openHotwordEditor(null, renderPage);
|
|
});
|
|
container.querySelectorAll('.hotword-edit-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => openHotwordEditor(btn.dataset.groupId, renderPage));
|
|
});
|
|
container.querySelectorAll('.hotword-delete-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const groups = ensureHotwordGroups();
|
|
const group = groups.find(item => item.id === btn.dataset.groupId);
|
|
if (!group) return;
|
|
createModal({
|
|
title: '删除热词组',
|
|
content: `<p style="font-size:14px;line-height:1.7">确定删除热词组 <strong>${escapeHtml(group.name)}</strong> 吗?删除后已绑定的智能体将不再使用该热词组。</p>`,
|
|
confirmText: '删除',
|
|
onConfirm: async () => {
|
|
const api = typeof API !== 'undefined' ? API : window.API;
|
|
try {
|
|
if (api?.agentDev?.deleteHotwordGroup) {
|
|
await api.agentDev.deleteHotwordGroup(group.id);
|
|
}
|
|
} catch (error) {
|
|
console.error('Delete hotword group failed', error);
|
|
Toast.error('热词组删除失败,请稍后重试');
|
|
return false;
|
|
}
|
|
const index = groups.findIndex(item => item.id === group.id);
|
|
if (index >= 0) groups.splice(index, 1);
|
|
if (window.__agentHotwordBindings) {
|
|
Object.keys(window.__agentHotwordBindings).forEach(appId => {
|
|
if (Array.isArray(window.__agentHotwordBindings[appId])) {
|
|
window.__agentHotwordBindings[appId] = window.__agentHotwordBindings[appId].filter(id => id !== group.id);
|
|
}
|
|
});
|
|
}
|
|
notifyHotwordGroupsChanged();
|
|
renderPage();
|
|
Toast.success('热词组已删除');
|
|
},
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
renderPage();
|
|
});
|
|
})();
|