Files
TalkingQ_Dashboard/scripts/hotword-library.js
“yuanmengy” a225272d1e data&device ready
2026-06-05 17:11:56 +08:00

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