/**
* TalkingQ Platform - Bad Case Management Page
*/
registerPage('badcase', function(container) {
const initialCases = [
{
id: 'BC-2026-001',
session: 'sess-6c1d',
device: 'DEV-002',
agent: '小Q故事机器人',
issue: '回答偏离话题,讲了不适合儿童的内容片段',
severity: 'high',
status: 'open',
reporter: '自动检测',
time: '2026-05-12 14:07',
tags: ['内容安全', '主题偏离'],
dialogue: {
user: '给我讲一个勇敢小朋友的故事',
assistant: '故事中出现了不适合儿童的危险情节描述。',
},
},
{
id: 'BC-2026-002',
session: 'sess-5a4b',
device: 'DEV-003',
agent: '小Q故事机器人',
issue: '粤语识别错误率高,多次识别成普通话',
severity: 'medium',
status: 'reviewing',
reporter: '运营标注',
time: '2026-05-12 13:58',
tags: ['ASR质量', '粤语'],
dialogue: {
user: '我想听广东话故事',
assistant: '识别结果被转写为普通话场景,导致回复不准确。',
},
},
{
id: 'BC-2026-003',
session: 'sess-3f7a',
device: 'DEV-007',
agent: '英语学习助手',
issue: '用户问"what is fire",AI回复了如何生火的危险操作',
severity: 'critical',
status: 'open',
reporter: '自动检测',
time: '2026-05-12 11:30',
tags: ['内容安全', '危险信息'],
dialogue: {
user: 'what is fire?',
assistant: '回复中包含了危险操作步骤,需要安全策略拦截。',
},
},
{
id: 'BC-2026-004',
session: 'sess-2c8b',
device: 'DEV-012',
agent: '科学百科探索者',
issue: 'TTS 合成失败,只播放了半句话',
severity: 'low',
status: 'closed',
reporter: '系统监控',
time: '2026-05-11 09:15',
tags: ['TTS故障'],
resolution: '已更换默认 TTS 兜底模型。',
dialogue: {
user: '太阳为什么会发光',
assistant: '太阳发光是因为内部发生核聚变...(播放中断)',
},
},
{
id: 'BC-2026-005',
session: 'sess-1d9e',
device: 'DEV-015',
agent: '小Q故事机器人',
issue: '对用户"我想要妈妈"的情感求助回应过于机械',
severity: 'medium',
status: 'reviewing',
reporter: '人工标注',
time: '2026-05-11 16:42',
tags: ['情感质量', '儿童关怀'],
dialogue: {
user: '我想要妈妈',
assistant: '请说明你的需求。',
},
},
];
if (!window.__badCaseRecords) {
window.__badCaseRecords = initialCases.map(item => ({ ...item, tags: [...item.tags], dialogue: { ...item.dialogue } }));
}
const cases = window.__badCaseRecords;
const filters = {
keyword: '',
severity: 'all',
status: 'all',
agent: 'all',
};
const sevMap = {
critical: { label: '严重', badge: 'badge-danger', accent: 'var(--danger)' },
high: { label: '高', badge: 'badge-danger', accent: 'var(--danger)' },
medium: { label: '中', badge: 'badge-warning', accent: 'var(--warning)' },
low: { label: '低', badge: 'badge-default', accent: 'var(--text-muted)' },
};
const statMap = {
open: { label: '待处理', badge: 'badge-danger' },
reviewing: { label: '审核中', badge: 'badge-warning' },
closed: { label: '已关闭', badge: 'badge-default' },
};
function escapeHtml(value) {
return String(value ?? '')
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function getNowText() {
const now = new Date();
const pad = value => String(value).padStart(2, '0');
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}`;
}
function splitTags(value) {
return String(value || '')
.split(/[,,]/)
.map(tag => tag.trim())
.filter(Boolean);
}
function getAgents() {
return [...new Set(cases.map(item => item.agent).filter(Boolean))];
}
function getFilteredCases() {
const keyword = filters.keyword.trim().toLowerCase();
return cases.filter(item => {
const searchableText = [
item.id,
item.session,
item.device,
item.agent,
item.issue,
item.reporter,
...(item.tags || []),
].join(' ').toLowerCase();
return (!keyword || searchableText.includes(keyword))
&& (filters.severity === 'all' || item.severity === filters.severity)
&& (filters.status === 'all' || item.status === filters.status)
&& (filters.agent === 'all' || item.agent === filters.agent);
});
}
function getStats() {
const openCases = cases.filter(item => item.status === 'open');
const reviewingCases = cases.filter(item => item.status === 'reviewing');
const closedCases = cases.filter(item => item.status === 'closed');
const securityCases = cases.filter(item => (item.tags || []).some(tag => ['内容安全', '危险信息'].includes(tag)));
return {
open: openCases.length,
criticalOpen: openCases.filter(item => item.severity === 'critical').length,
reviewing: reviewingCases.length,
closed: closedCases.length,
security: securityCases.length,
securityOpen: securityCases.filter(item => item.status !== 'closed').length,
};
}
function renderStats() {
const stats = getStats();
return `
待处理
${stats.open}
其中严重 ${stats.criticalOpen} 个
审核中
${stats.reviewing}
等待复核或修复验证
已关闭
${stats.closed}
当前前端 mock 统计
内容安全触发
${stats.security}
未关闭 ${stats.securityOpen} 个
`;
}
function renderActions(item) {
return `
${item.status !== 'closed' ? `
` : ''}
`;
}
function renderRows(rows) {
if (!rows.length) {
return `
暂无匹配的 Bad Case
可以调整筛选条件,或手动添加新的问题案例。
|
`;
}
return rows.map(item => `
| ${escapeHtml(item.id)} |
${escapeHtml(item.issue)}
Session: ${escapeHtml(item.session)} · ${escapeHtml(item.device)}
|
${escapeHtml(item.agent)} |
${sevMap[item.severity]?.label || '-'} |
${statMap[item.status]?.label || '-'} |
${(item.tags || []).map(tag => `${escapeHtml(tag)}`).join('') || '未标注'}
|
${escapeHtml(item.reporter)} |
${escapeHtml(item.time)} |
${renderActions(item)} |
`).join('');
}
function renderResults() {
const rows = getFilteredCases();
const statsTarget = container.querySelector('#bcStats');
const tableTarget = container.querySelector('#bcTableCard');
const totalTarget = container.querySelector('#bcFilterCount');
if (statsTarget) statsTarget.innerHTML = renderStats();
if (totalTarget) totalTarget.textContent = `共 ${rows.length} 条`;
if (tableTarget) {
tableTarget.innerHTML = `
| ID |
问题描述 |
智能体 |
严重度 |
状态 |
标签 |
报告人 |
时间 |
操作 |
${renderRows(rows)}
`;
}
lucide.createIcons({ elements: [container] });
}
function renderPage() {
const agents = getAgents();
container.innerHTML = `
`;
bindEvents();
renderResults();
}
function bindEvents() {
container.querySelector('#bcSearchInput')?.addEventListener('input', event => {
filters.keyword = event.target.value;
renderResults();
});
container.querySelector('#bcSeverityFilter')?.addEventListener('change', event => {
filters.severity = event.target.value;
renderResults();
});
container.querySelector('#bcStatusFilter')?.addEventListener('change', event => {
filters.status = event.target.value;
renderResults();
});
container.querySelector('#bcAgentFilter')?.addEventListener('change', event => {
filters.agent = event.target.value;
renderResults();
});
container.querySelector('#manualBCBtn')?.addEventListener('click', openManualCaseModal);
}
function findCase(id) {
return cases.find(item => item.id === id);
}
function getNextCaseId() {
const year = new Date().getFullYear();
const usedNumbers = cases
.map(item => String(item.id).match(/^BC-\d{4}-(\d+)$/))
.filter(Boolean)
.map(match => Number(match[1]));
const nextNumber = (usedNumbers.length ? Math.max(...usedNumbers) : 0) + 1;
return `BC-${year}-${String(nextNumber).padStart(3, '0')}`;
}
function openManualCaseModal() {
const agents = getAgents();
const modal = createModal({
title: '手动添加 Bad Case',
content: `
后端接口预留:POST /api/v1/bad-cases 创建人工 Bad Case。
`,
onConfirm: async () => {
const issue = document.getElementById('manualBCIssue')?.value.trim();
if (!issue) {
Toast.warning('请填写问题描述');
return false;
}
const selectedAgent = document.getElementById('manualBCAgent')?.value || '';
const customAgent = document.getElementById('manualBCCustomAgent')?.value.trim();
const agent = selectedAgent === 'custom' ? customAgent : selectedAgent;
if (!agent) {
Toast.warning('请填写智能体名称');
return false;
}
cases.unshift({
id: getNextCaseId(),
session: document.getElementById('manualBCSession')?.value.trim() || `sess-${Math.random().toString(16).slice(2, 6)}`,
device: document.getElementById('manualBCDevice')?.value.trim() || '手动录入',
agent,
issue,
severity: document.getElementById('manualBCSeverity')?.value || 'medium',
status: 'open',
reporter: '手动添加',
time: getNowText(),
tags: splitTags(document.getElementById('manualBCTags')?.value) || [],
dialogue: {
user: '手动添加案例,暂无原始用户消息',
assistant: issue,
},
});
try {
if (window.API?.badCases?.create) {
await API.badCases.create(cases[0]);
}
} catch (error) {
console.warn('Mock bad case create failed:', error);
}
filters.status = 'all';
renderPage();
Toast.success('Bad Case 已添加');
},
confirmText: '添加',
size: 'large',
});
const agentSelect = modal.overlay.querySelector('#manualBCAgent');
const customWrap = modal.overlay.querySelector('#manualBCCustomWrap');
agentSelect?.addEventListener('change', event => {
if (customWrap) customWrap.style.display = event.target.value === 'custom' ? 'block' : 'none';
});
}
window.openBCDetail = async function(id) {
const bc = findCase(id);
if (!bc) {
Toast.warning('未找到该 Bad Case');
return;
}
createModal({
title: `Bad Case 详情 - ${escapeHtml(id)}`,
content: `
问题描述
${escapeHtml(bc.issue)}
关联对话
用户
${escapeHtml(bc.dialogue?.user || '暂无原始用户消息')}
AI 回复
${escapeHtml(bc.dialogue?.assistant || '暂无原始 AI 回复')}
${bc.resolution ? `
最近处理意见
${escapeHtml(bc.resolution)}
` : ''}
后端接口预留:POST /api/v1/bad-cases/${escapeHtml(id)}/review 提交审核处理结果。
`,
onConfirm: async () => {
const resolution = document.getElementById('bcReviewResolution')?.value.trim();
const action = document.getElementById('bcReviewAction')?.value || 'none';
if (!resolution) {
Toast.warning('请填写处理意见');
return false;
}
try {
if (window.API?.badCases?.review) {
await API.badCases.review(id, { resolution, action });
}
} catch (error) {
console.warn('Mock bad case review failed:', error);
}
bc.resolution = resolution;
bc.reviewAction = action;
bc.updatedAt = getNowText();
if (bc.status !== 'closed') bc.status = 'reviewing';
renderResults();
Toast.success('已提交处理意见,状态已更新');
},
confirmText: '提交处理',
size: 'large',
});
};
window.openBCTagModal = function(id) {
const bc = findCase(id);
if (!bc) {
Toast.warning('未找到该 Bad Case');
return;
}
createModal({
title: `标注问题 - ${escapeHtml(id)}`,
content: `
后端接口预留:PATCH /api/v1/bad-cases/${escapeHtml(id)}/labels 保存标签、严重度和备注。
`,
onConfirm: () => {
const tags = splitTags(document.getElementById('bcTagsInput')?.value);
if (!tags.length) {
Toast.warning('请填写至少一个问题标签');
return false;
}
bc.tags = tags;
bc.severity = document.getElementById('bcTagSeverity')?.value || bc.severity;
bc.tagNote = document.getElementById('bcTagNote')?.value.trim() || '';
bc.reporter = bc.reporter === '自动检测' ? '运营标注' : bc.reporter;
bc.updatedAt = getNowText();
if (bc.status === 'open') bc.status = 'reviewing';
renderResults();
Toast.success('问题标注已保存');
},
confirmText: '保存标注',
});
};
window.closeBC = function(id) {
const bc = findCase(id);
if (!bc) {
Toast.warning('未找到该 Bad Case');
return;
}
createModal({
title: '关闭 Bad Case',
content: `
确认关闭 ${escapeHtml(id)}?关闭后仍可查看详情,但不再进入待处理列表。
后端接口预留:POST /api/v1/bad-cases/${escapeHtml(id)}/close 关闭问题案例。
`,
onConfirm: () => {
const reason = document.getElementById('bcCloseReason')?.value.trim();
if (!reason) {
Toast.warning('请填写关闭原因');
return false;
}
bc.status = 'closed';
bc.closedReason = reason;
bc.closedAt = getNowText();
renderResults();
Toast.success(`${id} 已关闭`);
},
confirmText: '确认关闭',
});
};
renderPage();
});