675 lines
27 KiB
JavaScript
675 lines
27 KiB
JavaScript
/**
|
||
* 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, '"')
|
||
.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 `
|
||
<div class="kpi-grid" style="grid-template-columns:repeat(4,minmax(0,1fr));margin-bottom:20px">
|
||
<div class="kpi-card">
|
||
<div class="kpi-icon"><i data-lucide="alert-triangle"></i></div>
|
||
<div class="kpi-label">待处理</div>
|
||
<div class="kpi-value" style="color:var(--danger)">${stats.open}</div>
|
||
<div class="kpi-change down">其中严重 ${stats.criticalOpen} 个</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-icon"><i data-lucide="search"></i></div>
|
||
<div class="kpi-label">审核中</div>
|
||
<div class="kpi-value" style="color:var(--warning)">${stats.reviewing}</div>
|
||
<div class="kpi-change">等待复核或修复验证</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-icon"><i data-lucide="check-circle"></i></div>
|
||
<div class="kpi-label">已关闭</div>
|
||
<div class="kpi-value" style="color:var(--success)">${stats.closed}</div>
|
||
<div class="kpi-change up">当前前端 mock 统计</div>
|
||
</div>
|
||
<div class="kpi-card">
|
||
<div class="kpi-icon"><i data-lucide="shield-alert"></i></div>
|
||
<div class="kpi-label">内容安全触发</div>
|
||
<div class="kpi-value" style="color:var(--danger)">${stats.security}</div>
|
||
<div class="kpi-change down">未关闭 ${stats.securityOpen} 个</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderActions(item) {
|
||
return `
|
||
<div style="display:flex;gap:4px;align-items:center">
|
||
<button class="btn btn-ghost btn-sm" data-tooltip="查看详情" onclick="openBCDetail('${item.id}')"><i data-lucide="eye"></i></button>
|
||
${item.status !== 'closed' ? `
|
||
<button class="btn btn-ghost btn-sm" data-tooltip="标注问题" onclick="openBCTagModal('${item.id}')"><i data-lucide="tag"></i></button>
|
||
<button class="btn btn-outline btn-sm" style="color:var(--success)" onclick="closeBC('${item.id}')">关闭</button>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
function renderRows(rows) {
|
||
if (!rows.length) {
|
||
return `
|
||
<tr>
|
||
<td colspan="9">
|
||
<div style="padding:42px 20px;text-align:center;color:var(--text-muted)">
|
||
<i data-lucide="inbox" style="width:32px;height:32px;margin-bottom:10px"></i>
|
||
<div style="font-size:14px;color:var(--text-secondary)">暂无匹配的 Bad Case</div>
|
||
<div style="font-size:12px;margin-top:4px">可以调整筛选条件,或手动添加新的问题案例。</div>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}
|
||
|
||
return rows.map(item => `
|
||
<tr>
|
||
<td style="font-family:monospace;font-size:12px;color:var(--primary)">${escapeHtml(item.id)}</td>
|
||
<td style="max-width:300px">
|
||
<div style="font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="${escapeHtml(item.issue)}">${escapeHtml(item.issue)}</div>
|
||
<div style="font-size:11px;color:var(--text-muted)">Session: ${escapeHtml(item.session)} · ${escapeHtml(item.device)}</div>
|
||
</td>
|
||
<td style="font-size:12px">${escapeHtml(item.agent)}</td>
|
||
<td><span class="badge ${sevMap[item.severity]?.badge || 'badge-default'}">${sevMap[item.severity]?.label || '-'}</span></td>
|
||
<td><span class="badge ${statMap[item.status]?.badge || 'badge-default'}">${statMap[item.status]?.label || '-'}</span></td>
|
||
<td>
|
||
<div style="display:flex;gap:4px;flex-wrap:wrap">
|
||
${(item.tags || []).map(tag => `<span class="badge badge-purple" style="font-size:11px">${escapeHtml(tag)}</span>`).join('') || '<span style="font-size:12px;color:var(--text-muted)">未标注</span>'}
|
||
</div>
|
||
</td>
|
||
<td style="font-size:12px">${escapeHtml(item.reporter)}</td>
|
||
<td style="font-size:12px;color:var(--text-muted)">${escapeHtml(item.time)}</td>
|
||
<td>${renderActions(item)}</td>
|
||
</tr>
|
||
`).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 = `
|
||
<div class="card" style="padding:0;overflow:hidden">
|
||
<div class="table-wrapper">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>ID</th>
|
||
<th>问题描述</th>
|
||
<th>智能体</th>
|
||
<th>严重度</th>
|
||
<th>状态</th>
|
||
<th>标签</th>
|
||
<th>报告人</th>
|
||
<th>时间</th>
|
||
<th>操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>${renderRows(rows)}</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
lucide.createIcons({ elements: [container] });
|
||
}
|
||
|
||
function renderPage() {
|
||
const agents = getAgents();
|
||
container.innerHTML = `
|
||
<div class="page-header">
|
||
<div>
|
||
<div class="page-title">Bad Case 管理</div>
|
||
<div class="page-desc">收集、标注和修复 AI 回复质量问题,持续优化智能体表现</div>
|
||
</div>
|
||
<div class="page-actions">
|
||
<button class="btn btn-primary" id="manualBCBtn">
|
||
<i data-lucide="plus"></i> 手动添加
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="bcStats"></div>
|
||
|
||
<div class="filter-bar">
|
||
<div class="search-input">
|
||
<i data-lucide="search"></i>
|
||
<input type="text" id="bcSearchInput" value="${escapeHtml(filters.keyword)}" placeholder="搜索 Bad Case 内容 / ID / Session / 设备...">
|
||
</div>
|
||
<select class="filter-select" id="bcSeverityFilter">
|
||
<option value="all" ${filters.severity === 'all' ? 'selected' : ''}>全部严重度</option>
|
||
<option value="critical" ${filters.severity === 'critical' ? 'selected' : ''}>严重</option>
|
||
<option value="high" ${filters.severity === 'high' ? 'selected' : ''}>高</option>
|
||
<option value="medium" ${filters.severity === 'medium' ? 'selected' : ''}>中</option>
|
||
<option value="low" ${filters.severity === 'low' ? 'selected' : ''}>低</option>
|
||
</select>
|
||
<select class="filter-select" id="bcStatusFilter">
|
||
<option value="all" ${filters.status === 'all' ? 'selected' : ''}>全部状态</option>
|
||
<option value="open" ${filters.status === 'open' ? 'selected' : ''}>待处理</option>
|
||
<option value="reviewing" ${filters.status === 'reviewing' ? 'selected' : ''}>审核中</option>
|
||
<option value="closed" ${filters.status === 'closed' ? 'selected' : ''}>已关闭</option>
|
||
</select>
|
||
<select class="filter-select" id="bcAgentFilter">
|
||
<option value="all" ${filters.agent === 'all' ? 'selected' : ''}>全部智能体</option>
|
||
${agents.map(agent => `<option value="${escapeHtml(agent)}" ${filters.agent === agent ? 'selected' : ''}>${escapeHtml(agent)}</option>`).join('')}
|
||
</select>
|
||
<span id="bcFilterCount" style="margin-left:auto;font-size:12px;color:var(--text-muted)"></span>
|
||
</div>
|
||
|
||
<div id="bcTableCard"></div>
|
||
`;
|
||
|
||
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: `
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">关联 Session ID</label>
|
||
<input type="text" class="form-control" id="manualBCSession" placeholder="sess-xxxxxx">
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">关联设备</label>
|
||
<input type="text" class="form-control" id="manualBCDevice" placeholder="DEV-001">
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">智能体</label>
|
||
<select class="form-control" id="manualBCAgent">
|
||
${agents.map(agent => `<option value="${escapeHtml(agent)}">${escapeHtml(agent)}</option>`).join('')}
|
||
<option value="custom">自定义智能体</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group" id="manualBCCustomWrap" style="display:none">
|
||
<label class="form-label">自定义智能体名称</label>
|
||
<input type="text" class="form-control" id="manualBCCustomAgent" placeholder="请输入智能体名称">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">问题描述<span class="required">*</span></label>
|
||
<textarea class="form-control" rows="4" id="manualBCIssue" placeholder="描述 AI 回复的具体问题、触发场景和期望结果..."></textarea>
|
||
</div>
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">严重度</label>
|
||
<select class="form-control" id="manualBCSeverity">
|
||
<option value="low">低</option>
|
||
<option value="medium" selected>中</option>
|
||
<option value="high">高</option>
|
||
<option value="critical">严重</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">问题标签</label>
|
||
<input type="text" class="form-control" id="manualBCTags" placeholder="内容安全, ASR质量...">
|
||
</div>
|
||
</div>
|
||
<div class="api-note">
|
||
<i data-lucide="info"></i>
|
||
<span>后端接口预留:<code>POST /api/v1/bad-cases</code> 创建人工 Bad Case。</span>
|
||
</div>`,
|
||
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: `
|
||
<div style="display:grid;gap:14px">
|
||
<div>
|
||
<div class="form-label">问题描述</div>
|
||
<div style="padding:12px;background:#fff2f0;border:1px solid #ffd8d3;border-radius:var(--radius-sm);font-size:13px;line-height:1.7">${escapeHtml(bc.issue)}</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div>
|
||
<div class="form-label">严重度</div>
|
||
<span class="badge ${sevMap[bc.severity]?.badge || 'badge-default'}">${sevMap[bc.severity]?.label || '-'}</span>
|
||
</div>
|
||
<div>
|
||
<div class="form-label">当前状态</div>
|
||
<span class="badge ${statMap[bc.status]?.badge || 'badge-default'}">${statMap[bc.status]?.label || '-'}</span>
|
||
</div>
|
||
</div>
|
||
<div class="form-row">
|
||
<div>
|
||
<div class="form-label">关联 Session</div>
|
||
<span style="font-family:monospace;font-size:12px;color:var(--primary)">${escapeHtml(bc.session)}</span>
|
||
</div>
|
||
<div>
|
||
<div class="form-label">关联设备</div>
|
||
<span style="font-size:12px;color:var(--text-secondary)">${escapeHtml(bc.device)}</span>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div class="form-label">关联对话</div>
|
||
<div style="display:grid;gap:8px;padding:12px;background:#f7f8fc;border-radius:var(--radius-sm)">
|
||
<div style="font-size:12px;color:var(--text-muted)">用户</div>
|
||
<div style="font-size:13px;color:var(--text-primary);line-height:1.6">${escapeHtml(bc.dialogue?.user || '暂无原始用户消息')}</div>
|
||
<div style="font-size:12px;color:var(--text-muted);margin-top:4px">AI 回复</div>
|
||
<div style="font-size:13px;color:var(--text-primary);line-height:1.6">${escapeHtml(bc.dialogue?.assistant || '暂无原始 AI 回复')}</div>
|
||
</div>
|
||
</div>
|
||
${bc.resolution ? `
|
||
<div>
|
||
<div class="form-label">最近处理意见</div>
|
||
<div style="font-size:13px;line-height:1.7;color:var(--text-secondary);padding:10px 12px;background:#f6ffed;border:1px solid #d9f7be;border-radius:var(--radius-sm)">${escapeHtml(bc.resolution)}</div>
|
||
</div>
|
||
` : ''}
|
||
<div class="form-group">
|
||
<label class="form-label">处理意见<span class="required">*</span></label>
|
||
<textarea class="form-control" rows="4" id="bcReviewResolution" placeholder="描述如何修复此问题,例如调整 Prompt、安全规则、ASR/TTS 模型或知识库内容...">${escapeHtml(bc.resolution || '')}</textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">关联修复动作</label>
|
||
<select class="form-control" id="bcReviewAction">
|
||
<option value="none">不关联</option>
|
||
<option value="prompt">修改系统 Prompt / 安全策略</option>
|
||
<option value="model">调整模型配置</option>
|
||
<option value="knowledge">修正知识库内容</option>
|
||
<option value="workflow">调整工作流或工具调用</option>
|
||
</select>
|
||
</div>
|
||
<div class="api-note">
|
||
<i data-lucide="info"></i>
|
||
<span>后端接口预留:<code>POST /api/v1/bad-cases/${escapeHtml(id)}/review</code> 提交审核处理结果。</span>
|
||
</div>
|
||
</div>`,
|
||
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: `
|
||
<div class="form-row">
|
||
<div class="form-group">
|
||
<label class="form-label">严重度</label>
|
||
<select class="form-control" id="bcTagSeverity">
|
||
<option value="critical" ${bc.severity === 'critical' ? 'selected' : ''}>严重</option>
|
||
<option value="high" ${bc.severity === 'high' ? 'selected' : ''}>高</option>
|
||
<option value="medium" ${bc.severity === 'medium' ? 'selected' : ''}>中</option>
|
||
<option value="low" ${bc.severity === 'low' ? 'selected' : ''}>低</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">问题标签<span class="required">*</span></label>
|
||
<input type="text" class="form-control" id="bcTagsInput" value="${escapeHtml((bc.tags || []).join(', '))}" placeholder="例:内容安全, ASR质量">
|
||
</div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label class="form-label">标注备注</label>
|
||
<textarea class="form-control" rows="3" id="bcTagNote" placeholder="补充问题原因或建议修复方向">${escapeHtml(bc.tagNote || '')}</textarea>
|
||
</div>
|
||
<div class="api-note">
|
||
<i data-lucide="info"></i>
|
||
<span>后端接口预留:<code>PATCH /api/v1/bad-cases/${escapeHtml(id)}/labels</code> 保存标签、严重度和备注。</span>
|
||
</div>`,
|
||
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: `
|
||
<p style="font-size:14px;margin-bottom:10px">确认关闭 <strong>${escapeHtml(id)}</strong>?关闭后仍可查看详情,但不再进入待处理列表。</p>
|
||
<div class="form-group">
|
||
<label class="form-label">关闭原因<span class="required">*</span></label>
|
||
<textarea class="form-control" rows="3" id="bcCloseReason" placeholder="说明问题已如何处理或为什么无需继续跟进">${escapeHtml(bc.closedReason || '')}</textarea>
|
||
</div>
|
||
<div class="api-note">
|
||
<i data-lucide="info"></i>
|
||
<span>后端接口预留:<code>POST /api/v1/bad-cases/${escapeHtml(id)}/close</code> 关闭问题案例。</span>
|
||
</div>`,
|
||
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();
|
||
});
|