// InsightFlow Frontend - Phase 4 (Agent Assistant + Provenance) const API_BASE = '/api/v1'; let currentProject = null; let currentData = null; let selectedEntity = null; let projectRelations = []; let projectEntities = []; let entityDetailsCache = {}; // Init document.addEventListener('DOMContentLoaded', () => { const isWorkbench = window.location.pathname.includes('workbench'); if (isWorkbench) { initWorkbench(); } }); // Initialize workbench async function initWorkbench() { const projectId = localStorage.getItem('currentProject'); if (!projectId) { window.location.href = '/'; return; } try { const projects = await fetchProjects(); currentProject = projects.find(p => p.id === projectId); if (!currentProject) { localStorage.removeItem('currentProject'); window.location.href = '/'; return; } const nameEl = document.getElementById('projectName'); if (nameEl) nameEl.textContent = currentProject.name; initUpload(); initAgentPanel(); initEntityCard(); await loadProjectData(); } catch (err) { console.error('Init failed:', err); alert('加载失败,请返回项目列表'); } } // ==================== API Calls ==================== async function fetchProjects() { const res = await fetch(`${API_BASE}/projects`); if (!res.ok) throw new Error('Failed to fetch projects'); return await res.json(); } async function uploadAudio(file) { const formData = new FormData(); formData.append('file', file); const res = await fetch(`${API_BASE}/projects/${currentProject.id}/upload`, { method: 'POST', body: formData }); if (!res.ok) throw new Error('Upload failed'); return await res.json(); } async function loadProjectData() { try { const [entitiesRes, relationsRes] = await Promise.all([ fetch(`${API_BASE}/projects/${currentProject.id}/entities`), fetch(`${API_BASE}/projects/${currentProject.id}/relations`) ]); if (entitiesRes.ok) { projectEntities = await entitiesRes.json(); } if (relationsRes.ok) { projectRelations = await relationsRes.json(); } // 预加载实体详情 await preloadEntityDetails(); currentData = { transcript_id: 'project_view', project_id: currentProject.id, segments: [], entities: projectEntities, full_text: '', created_at: new Date().toISOString() }; renderGraph(); renderEntityList(); } catch (err) { console.error('Load project data failed:', err); } } async function preloadEntityDetails() { // 并行加载所有实体详情 const promises = projectEntities.map(async (ent) => { try { const res = await fetch(`${API_BASE}/entities/${ent.id}/details`); if (res.ok) { entityDetailsCache[ent.id] = await res.json(); } } catch (e) { console.error(`Failed to load entity ${ent.id} details:`, e); } }); await Promise.all(promises); } // ==================== Agent Panel ==================== function initAgentPanel() { const chatInput = document.getElementById('chatInput'); if (chatInput) { chatInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendAgentMessage(); } }); } } function toggleAgentPanel() { const panel = document.getElementById('agentPanel'); const toggle = panel.querySelector('.agent-toggle'); panel.classList.toggle('collapsed'); toggle.textContent = panel.classList.contains('collapsed') ? '‹' : '›'; } function addChatMessage(content, isUser = false, isTyping = false) { const container = document.getElementById('chatMessages'); const msgDiv = document.createElement('div'); msgDiv.className = `chat-message ${isUser ? 'user' : 'assistant'}`; if (isTyping) { msgDiv.innerHTML = `
`; } else { msgDiv.innerHTML = `
${content}
`; } container.appendChild(msgDiv); container.scrollTop = container.scrollHeight; return msgDiv; } function removeTypingIndicator() { const indicator = document.getElementById('typingIndicator'); if (indicator) { indicator.parentElement.remove(); } } async function sendAgentMessage() { const input = document.getElementById('chatInput'); const message = input.value.trim(); if (!message) return; input.value = ''; addChatMessage(message, true); addChatMessage('', false, true); try { // 判断是命令还是问答 const isCommand = message.includes('合并') || message.includes('修改') || message.startsWith('把') || message.startsWith('将'); if (isCommand) { // 执行命令 const res = await fetch(`${API_BASE}/projects/${currentProject.id}/agent/command`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command: message }) }); removeTypingIndicator(); if (res.ok) { const result = await res.json(); let response = ''; if (result.intent === 'merge_entities') { if (result.success) { response = `✅ 已合并 ${result.merged.length} 个实体到 "${result.target}"`; await loadProjectData(); // 刷新数据 } else { response = `❌ 合并失败:${result.error || '未找到匹配的实体'}`; } } else if (result.intent === 'edit_entity') { if (result.success) { response = `✅ 已更新实体 "${result.entity?.name}"`; await loadProjectData(); } else { response = `❌ 编辑失败:${result.error || '未找到实体'}`; } } else if (result.intent === 'answer_question') { response = result.answer; } else { response = result.message || result.explanation || '未识别的指令'; } addChatMessage(response); } else { addChatMessage('❌ 请求失败,请重试'); } } else { // RAG 问答 const res = await fetch(`${API_BASE}/projects/${currentProject.id}/agent/query`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: message, stream: false }) }); removeTypingIndicator(); if (res.ok) { const result = await res.json(); addChatMessage(result.answer); } else { addChatMessage('❌ 获取回答失败,请重试'); } } } catch (err) { removeTypingIndicator(); addChatMessage('❌ 网络错误,请检查连接'); console.error('Agent error:', err); } } async function loadSuggestions() { addChatMessage('正在获取建议...', false, true); try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/agent/suggest`); removeTypingIndicator(); if (res.ok) { const result = await res.json(); const suggestions = result.suggestions || []; if (suggestions.length === 0) { addChatMessage('暂无建议,请先上传一些音频文件。'); return; } let html = '
💡 基于项目数据的建议:
'; suggestions.forEach((s, i) => { html += `
${s.type === 'action' ? '⚡ 操作' : '💡 洞察'}
${s.title}
${s.description}
`; }); const msgDiv = document.createElement('div'); msgDiv.className = 'chat-message assistant'; msgDiv.innerHTML = `
${html}
`; document.getElementById('chatMessages').appendChild(msgDiv); } } catch (err) { removeTypingIndicator(); addChatMessage('❌ 获取建议失败'); } } function applySuggestion(index) { // 可以在这里实现建议的自动应用 addChatMessage('建议功能开发中,敬请期待!'); } // ==================== Transcript Rendering ==================== function renderTranscript() { const container = document.getElementById('transcriptContent'); if (!container || !currentData || !currentData.segments) return; container.innerHTML = ''; currentData.segments.forEach((seg, idx) => { const div = document.createElement('div'); div.className = 'segment'; div.dataset.index = idx; let text = seg.text; const entities = findEntitiesInText(seg.text); entities.sort((a, b) => b.start - a.start); entities.forEach(ent => { const before = text.slice(0, ent.start); const name = text.slice(ent.start, ent.end); const after = text.slice(ent.end); const details = entityDetailsCache[ent.id]; const confidence = details?.mentions?.[0]?.confidence || 1.0; const lowConfClass = confidence < 0.7 ? 'low-confidence' : ''; text = before + `${name}` + after; }); div.innerHTML = `
${seg.speaker}
${text}
`; container.appendChild(div); }); } function findEntitiesInText(text) { if (!projectEntities || projectEntities.length === 0) return []; const found = []; projectEntities.forEach(ent => { const name = ent.name; let pos = 0; while ((pos = text.indexOf(name, pos)) !== -1) { found.push({ id: ent.id, name: ent.name, start: pos, end: pos + name.length }); pos += 1; } if (ent.aliases && ent.aliases.length > 0) { ent.aliases.forEach(alias => { let aliasPos = 0; while ((aliasPos = text.indexOf(alias, aliasPos)) !== -1) { found.push({ id: ent.id, name: alias, start: aliasPos, end: aliasPos + alias.length }); aliasPos += 1; } }); } }); return found; } // ==================== Entity Card ==================== function initEntityCard() { const card = document.getElementById('entityCard'); // 鼠标移出卡片时隐藏 card.addEventListener('mouseleave', () => { card.classList.remove('show'); }); } function showEntityCard(event, entityId) { const card = document.getElementById('entityCard'); const details = entityDetailsCache[entityId]; const entity = projectEntities.find(e => e.id === entityId); if (!entity) return; // 更新卡片内容 document.getElementById('cardName').textContent = entity.name; document.getElementById('cardBadge').textContent = entity.type; document.getElementById('cardBadge').className = `entity-type-badge type-${entity.type.toLowerCase()}`; document.getElementById('cardDefinition').textContent = entity.definition || '暂无定义'; const mentionCount = details?.mentions?.length || 0; const relationCount = details?.relations?.length || 0; document.getElementById('cardMentions').textContent = `${mentionCount} 次提及`; document.getElementById('cardRelations').textContent = `${relationCount} 个关系`; // 定位卡片 const rect = event.target.getBoundingClientRect(); card.style.left = `${rect.left}px`; card.style.top = `${rect.bottom + 10}px`; // 确保不超出屏幕 const cardRect = card.getBoundingClientRect(); if (cardRect.right > window.innerWidth) { card.style.left = `${window.innerWidth - cardRect.width - 20}px`; } card.classList.add('show'); } function hideEntityCard() { // 延迟隐藏,允许鼠标移到卡片上 setTimeout(() => { const card = document.getElementById('entityCard'); if (!card.matches(':hover')) { card.classList.remove('show'); } }, 100); } // ==================== Graph Visualization ==================== function renderGraph() { const svg = d3.select('#graph-svg'); svg.selectAll('*').remove(); if (!projectEntities || projectEntities.length === 0) { svg.append('text') .attr('x', '50%') .attr('y', '50%') .attr('text-anchor', 'middle') .attr('fill', '#666') .text('暂无实体数据,请上传音频'); return; } const container = svg.node().parentElement; const width = container.clientWidth; const height = container.clientHeight - 200; svg.attr('width', width).attr('height', height); const nodes = projectEntities.map(e => ({ id: e.id, name: e.name, type: e.type, definition: e.definition, ...e })); const links = projectRelations.map(r => ({ id: r.id, source: r.source_id, target: r.target_id, type: r.type, evidence: r.evidence })).filter(r => r.source && r.target); if (links.length === 0 && nodes.length > 1) { for (let i = 0; i < Math.min(nodes.length - 1, 5); i++) { links.push({ source: nodes[0].id, target: nodes[i + 1].id, type: 'related' }); } } const colorMap = { 'PROJECT': '#7b2cbf', 'TECH': '#00d4ff', 'PERSON': '#ff6b6b', 'ORG': '#4ecdc4', 'OTHER': '#666' }; const simulation = d3.forceSimulation(nodes) .force('link', d3.forceLink(links).id(d => d.id).distance(120)) .force('charge', d3.forceManyBody().strength(-400)) .force('center', d3.forceCenter(width / 2, height / 2)) .force('collision', d3.forceCollide().radius(50)); // 关系连线 const link = svg.append('g') .selectAll('line') .data(links) .enter().append('line') .attr('stroke', '#444') .attr('stroke-width', 1.5) .attr('stroke-opacity', 0.6) .style('cursor', 'pointer') .on('click', (e, d) => showProvenance(d)); // 关系标签 const linkLabel = svg.append('g') .selectAll('text') .data(links) .enter().append('text') .attr('font-size', '10px') .attr('fill', '#666') .attr('text-anchor', 'middle') .style('pointer-events', 'none') .text(d => d.type); // 节点组 const node = svg.append('g') .selectAll('g') .data(nodes) .enter().append('g') .attr('class', 'node') .call(d3.drag() .on('start', dragstarted) .on('drag', dragged) .on('end', dragended)) .on('click', (e, d) => window.selectEntity(d.id)) .on('mouseenter', (e, d) => showEntityCard(e, d.id)) .on('mouseleave', hideEntityCard); // 节点圆圈 node.append('circle') .attr('r', 35) .attr('fill', d => colorMap[d.type] || '#666') .attr('stroke', '#fff') .attr('stroke-width', 2) .attr('class', 'node-circle'); // 节点文字 node.append('text') .text(d => d.name.length > 6 ? d.name.slice(0, 5) + '...' : d.name) .attr('text-anchor', 'middle') .attr('dy', 5) .attr('fill', '#fff') .attr('font-size', '11px') .attr('font-weight', '500') .style('pointer-events', 'none'); // 节点类型图标 node.append('text') .attr('dy', -45) .attr('text-anchor', 'middle') .attr('fill', d => colorMap[d.type] || '#666') .attr('font-size', '10px') .text(d => d.type) .style('pointer-events', 'none'); simulation.on('tick', () => { link .attr('x1', d => d.source.x) .attr('y1', d => d.source.y) .attr('x2', d => d.target.x) .attr('y2', d => d.target.y); linkLabel .attr('x', d => (d.source.x + d.target.x) / 2) .attr('y', d => (d.source.y + d.target.y) / 2); node.attr('transform', d => `translate(${d.x},${d.y})`); }); function dragstarted(e, d) { if (!e.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(e, d) { d.fx = e.x; d.fy = e.y; } function dragended(e, d) { if (!e.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; } } // ==================== Provenance ==================== async function showProvenance(relation) { const modal = document.getElementById('provenanceModal'); const body = document.getElementById('provenanceBody'); modal.classList.add('show'); body.innerHTML = '

加载中...

'; try { let content = ''; if (relation.id) { // 从API获取溯源信息 const res = await fetch(`${API_BASE}/relations/${relation.id}/provenance`); if (res.ok) { const data = await res.json(); content = `
关系类型
${data.source} → ${data.type} → ${data.target}
来源文档
${data.transcript?.filename || '未知文件'}
证据文本
"${data.evidence || '无证据文本'}"
`; } else { content = '

获取溯源信息失败

'; } } else { // 使用本地数据 content = `
关系类型
${relation.source.name || relation.source} → ${relation.type} → ${relation.target.name || relation.target}
证据文本
"${relation.evidence || '无证据文本'}"
`; } body.innerHTML = content; } catch (err) { body.innerHTML = '

加载失败

'; } } function closeProvenance() { document.getElementById('provenanceModal').classList.remove('show'); } // ==================== Entity List ==================== function renderEntityList() { const container = document.getElementById('entityList'); if (!container) return; container.innerHTML = '

项目实体

'; if (!projectEntities || projectEntities.length === 0) { container.innerHTML += '

暂无实体,请上传音频文件

'; return; } projectEntities.forEach(ent => { const div = document.createElement('div'); div.className = 'entity-item'; div.dataset.id = ent.id; div.onclick = () => window.selectEntity(ent.id); div.onmouseenter = (e) => showEntityCard(e, ent.id); div.onmouseleave = hideEntityCard; div.innerHTML = ` ${ent.type}
${ent.name}
${ent.definition || '暂无定义'}
`; container.appendChild(div); }); } // ==================== Entity Selection ==================== window.selectEntity = function(entityId) { selectedEntity = entityId; const entity = projectEntities.find(e => e.id === entityId); if (!entity) return; // 高亮文本中的实体 document.querySelectorAll('.entity').forEach(el => { if (el.dataset.id === entityId) { el.style.background = '#ff6b6b'; el.style.color = '#fff'; } else { el.style.background = ''; el.style.color = ''; } }); // 高亮图谱中的节点 d3.selectAll('.node-circle') .attr('stroke', d => d.id === entityId ? '#ff6b6b' : '#fff') .attr('stroke-width', d => d.id === entityId ? 4 : 2) .attr('r', d => d.id === entityId ? 40 : 35); // 高亮实体列表 document.querySelectorAll('.entity-item').forEach(el => { if (el.dataset.id === entityId) { el.style.background = '#2a2a2a'; el.style.borderLeft = '3px solid #ff6b6b'; } else { el.style.background = ''; el.style.borderLeft = ''; } }); console.log('Selected:', entity.name, entity.definition); }; // ==================== Upload ==================== window.showUpload = function() { const el = document.getElementById('uploadOverlay'); if (el) el.classList.add('show'); }; window.hideUpload = function() { const el = document.getElementById('uploadOverlay'); if (el) el.classList.remove('show'); }; function initUpload() { const input = document.getElementById('fileInput'); const overlay = document.getElementById('uploadOverlay'); if (!input) return; input.addEventListener('change', async (e) => { if (!e.target.files.length) return; const file = e.target.files[0]; if (overlay) { overlay.innerHTML = `

正在分析...

${file.name}

ASR转录 + 实体提取中

`; } try { const result = await uploadAudio(file); currentData = result; await loadProjectData(); if (result.segments && result.segments.length > 0) { renderTranscript(); } if (overlay) overlay.classList.remove('show'); } catch (err) { console.error('Upload failed:', err); if (overlay) { overlay.innerHTML = `

分析失败

${err.message}

`; } } }); } // ==================== Phase 5: Timeline View ==================== async function loadTimeline() { const container = document.getElementById('timelineContainer'); const entityFilter = document.getElementById('timelineEntityFilter'); if (!container) return; container.innerHTML = '

加载时间线数据...

'; try { // 更新实体筛选器选项 if (entityFilter && projectEntities.length > 0) { const currentValue = entityFilter.value; entityFilter.innerHTML = ''; projectEntities.forEach(ent => { const option = document.createElement('option'); option.value = ent.id; option.textContent = ent.name; entityFilter.appendChild(option); }); entityFilter.value = currentValue; } // 构建查询参数 const params = new URLSearchParams(); if (entityFilter && entityFilter.value) { params.append('entity_id', entityFilter.value); } // 获取时间线数据 const res = await fetch(`${API_BASE}/projects/${currentProject.id}/timeline?${params}`); if (!res.ok) throw new Error('Failed to load timeline'); const data = await res.json(); const events = data.events || []; // 更新统计 const mentions = events.filter(e => e.type === 'mention').length; const relations = events.filter(e => e.type === 'relation').length; document.getElementById('timelineTotalEvents').textContent = events.length; document.getElementById('timelineMentions').textContent = mentions; document.getElementById('timelineRelations').textContent = relations; // 渲染时间线 renderTimeline(events); } catch (err) { console.error('Load timeline failed:', err); container.innerHTML = '

加载失败,请重试

'; } } function renderTimeline(events) { const container = document.getElementById('timelineContainer'); if (events.length === 0) { container.innerHTML = `

暂无时间线数据

请先上传音频或文档文件

`; return; } // 按日期分组 const grouped = groupEventsByDate(events); let html = '
'; Object.entries(grouped).forEach(([date, dayEvents]) => { const dateLabel = formatDateLabel(date); html += `
${dateLabel}
`; dayEvents.forEach(event => { html += renderTimelineEvent(event); }); html += '
'; }); container.innerHTML = html; } function groupEventsByDate(events) { const grouped = {}; events.forEach(event => { const date = event.event_date.split('T')[0]; if (!grouped[date]) { grouped[date] = []; } grouped[date].push(event); }); return grouped; } function formatDateLabel(dateStr) { const date = new Date(dateStr); const today = new Date(); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); if (dateStr === today.toISOString().split('T')[0]) { return '今天'; } else if (dateStr === yesterday.toISOString().split('T')[0]) { return '昨天'; } else { return `${date.getMonth() + 1}月${date.getDate()}日`; } } function renderTimelineEvent(event) { if (event.type === 'mention') { return `
提及 ${event.entity_name} ${event.entity_type || 'OTHER'}
"${event.text_snippet || ''}"
📄 ${event.source?.filename || '未知文件'} ${event.confidence ? `置信度: ${(event.confidence * 100).toFixed(0)}%` : ''}
`; } else if (event.type === 'relation') { return `
关系 ${event.source_entity} → ${event.target_entity}
关系类型: ${event.relation_type}
${event.evidence ? `
"${event.evidence}"
` : ''}
📄 ${event.source?.filename || '未知文件'}
`; } return ''; } // ==================== View Switching ==================== window.switchView = function(viewName) { // 更新侧边栏按钮状态 document.querySelectorAll('.sidebar-btn').forEach(btn => { btn.classList.remove('active'); }); // 隐藏所有视图 document.getElementById('workbenchView').style.display = 'none'; document.getElementById('knowledgeBaseView').classList.remove('show'); document.getElementById('timelineView').classList.remove('show'); // 显示选中的视图 if (viewName === 'workbench') { document.getElementById('workbenchView').style.display = 'flex'; document.querySelector('.sidebar-btn:nth-child(1)').classList.add('active'); } else if (viewName === 'knowledge-base') { document.getElementById('knowledgeBaseView').classList.add('show'); document.querySelector('.sidebar-btn:nth-child(2)').classList.add('active'); loadKnowledgeBase(); } else if (viewName === 'timeline') { document.getElementById('timelineView').classList.add('show'); document.querySelector('.sidebar-btn:nth-child(3)').classList.add('active'); loadTimeline(); } }; window.switchKBTab = function(tabName) { // 更新导航项状态 document.querySelectorAll('.kb-nav-item').forEach(item => { item.classList.remove('active'); }); // 隐藏所有部分 document.querySelectorAll('.kb-section').forEach(section => { section.classList.remove('active'); }); // 显示选中的部分 const tabMap = { 'entities': { nav: 0, section: 'kbEntitiesSection' }, 'relations': { nav: 1, section: 'kbRelationsSection' }, 'glossary': { nav: 2, section: 'kbGlossarySection' }, 'transcripts': { nav: 3, section: 'kbTranscriptsSection' } }; const mapping = tabMap[tabName]; if (mapping) { document.querySelectorAll('.kb-nav-item')[mapping.nav].classList.add('active'); document.getElementById(mapping.section).classList.add('active'); } }; async function loadKnowledgeBase() { if (!currentProject) return; try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/knowledge-base`); if (!res.ok) throw new Error('Failed to load knowledge base'); const data = await res.json(); // 更新统计 document.getElementById('kbEntityCount').textContent = data.stats.entity_count; document.getElementById('kbRelationCount').textContent = data.stats.relation_count; document.getElementById('kbTranscriptCount').textContent = data.stats.transcript_count; document.getElementById('kbGlossaryCount').textContent = data.stats.glossary_count; // 渲染实体网格 const entityGrid = document.getElementById('kbEntityGrid'); entityGrid.innerHTML = ''; data.entities.forEach(ent => { const card = document.createElement('div'); card.className = 'kb-entity-card'; card.onclick = () => { switchView('workbench'); setTimeout(() => selectEntity(ent.id), 100); }; card.innerHTML = `
${ent.type} ${ent.name}
${ent.definition || '暂无定义'}
📍 ${ent.mention_count || 0} 次提及 · ${ent.appears_in?.length || 0} 个文件
`; entityGrid.appendChild(card); }); // 渲染关系列表 const relationsList = document.getElementById('kbRelationsList'); relationsList.innerHTML = ''; data.relations.forEach(rel => { const item = document.createElement('div'); item.className = 'kb-glossary-item'; item.innerHTML = `
${rel.source_name} → ${rel.type} → ${rel.target_name} ${rel.evidence ? `
"${rel.evidence.substring(0, 100)}..."
` : ''}
`; relationsList.appendChild(item); }); // 渲染术语表 const glossaryList = document.getElementById('kbGlossaryList'); glossaryList.innerHTML = ''; data.glossary.forEach(term => { const item = document.createElement('div'); item.className = 'kb-glossary-item'; item.innerHTML = `
${term.term} ${term.pronunciation ? `(${term.pronunciation})` : ''} 出现 ${term.frequency} 次
`; glossaryList.appendChild(item); }); // 渲染文件列表 const transcriptsList = document.getElementById('kbTranscriptsList'); transcriptsList.innerHTML = ''; data.transcripts.forEach(t => { const item = document.createElement('div'); item.className = 'kb-transcript-item'; item.innerHTML = `
${t.type === 'audio' ? '🎵' : '📄'} ${t.filename}
${new Date(t.created_at).toLocaleString()}
`; transcriptsList.appendChild(item); }); } catch (err) { console.error('Load knowledge base failed:', err); } } // ==================== Glossary Functions ==================== window.showAddTermModal = function() { document.getElementById('glossaryModal').classList.add('show'); }; window.hideGlossaryModal = function() { document.getElementById('glossaryModal').classList.remove('show'); }; window.saveGlossaryTerm = async function() { const term = document.getElementById('glossaryTerm').value.trim(); const pronunciation = document.getElementById('glossaryPronunciation').value.trim(); if (!term) return; try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/glossary`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ term, pronunciation }) }); if (res.ok) { hideGlossaryModal(); document.getElementById('glossaryTerm').value = ''; document.getElementById('glossaryPronunciation').value = ''; loadKnowledgeBase(); } } catch (err) { console.error('Save glossary term failed:', err); } }; window.deleteGlossaryTerm = async function(termId) { if (!confirm('确定要删除这个术语吗?')) return; try { const res = await fetch(`${API_BASE}/glossary/${termId}`, { method: 'DELETE' }); if (res.ok) { loadKnowledgeBase(); } } catch (err) { console.error('Delete glossary term failed:', err); } };