// InsightFlow Frontend - Phase 3 (Memory & Growth) // Knowledge Growth: Multi-file fusion + Entity Alignment + Document Import const API_BASE = '/api/v1'; let currentProject = null; let currentData = null; let selectedEntity = null; let projectRelations = []; let projectEntities = []; let currentTranscript = null; let projectTranscripts = []; let editMode = false; let contextMenuTarget = null; let currentUploadTab = 'audio'; let knowledgeBaseData = null; // 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(); initContextMenu(); initTextSelection(); 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(); } // Phase 3: Document Upload API async function uploadDocument(file) { const formData = new FormData(); formData.append('file', file); const res = await fetch(`${API_BASE}/projects/${currentProject.id}/upload-document`, { method: 'POST', body: formData }); if (!res.ok) { const error = await res.json(); throw new Error(error.detail || 'Document upload failed'); } return await res.json(); } // Phase 3: Knowledge Base API async function fetchKnowledgeBase() { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/knowledge-base`); if (!res.ok) throw new Error('Failed to fetch knowledge base'); return await res.json(); } // Phase 3: Glossary API async function addGlossaryTerm(term, pronunciation = '') { 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) throw new Error('Failed to add glossary term'); return await res.json(); } async function deleteGlossaryTerm(termId) { const res = await fetch(`${API_BASE}/glossary/${termId}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Failed to delete glossary term'); return await res.json(); } // Phase 2: Entity Edit API async function updateEntity(entityId, data) { const res = await fetch(`${API_BASE}/entities/${entityId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (!res.ok) throw new Error('Failed to update entity'); return await res.json(); } async function deleteEntityApi(entityId) { const res = await fetch(`${API_BASE}/entities/${entityId}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Failed to delete entity'); return await res.json(); } async function mergeEntitiesApi(sourceId, targetId) { const res = await fetch(`${API_BASE}/entities/${sourceId}/merge`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ source_entity_id: sourceId, target_entity_id: targetId }) }); if (!res.ok) throw new Error('Failed to merge entities'); return await res.json(); } async function createEntityApi(data) { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/entities`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (!res.ok) throw new Error('Failed to create entity'); return await res.json(); } // Phase 2: Relation API async function createRelationApi(data) { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/relations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (!res.ok) throw new Error('Failed to create relation'); return await res.json(); } async function deleteRelationApi(relationId) { const res = await fetch(`${API_BASE}/relations/${relationId}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Failed to delete relation'); return await res.json(); } // Phase 2: Transcript API async function getTranscript(transcriptId) { const res = await fetch(`${API_BASE}/transcripts/${transcriptId}`); if (!res.ok) throw new Error('Failed to get transcript'); return await res.json(); } async function updateTranscript(transcriptId, fullText) { const res = await fetch(`${API_BASE}/transcripts/${transcriptId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ full_text: fullText }) }); if (!res.ok) throw new Error('Failed to update transcript'); return await res.json(); } async function loadProjectData() { try { // 并行加载实体、关系和转录列表 const [entitiesRes, relationsRes, transcriptsRes] = await Promise.all([ fetch(`${API_BASE}/projects/${currentProject.id}/entities`), fetch(`${API_BASE}/projects/${currentProject.id}/relations`), fetch(`${API_BASE}/projects/${currentProject.id}/transcripts`) ]); if (entitiesRes.ok) { projectEntities = await entitiesRes.json(); } if (relationsRes.ok) { projectRelations = await relationsRes.json(); } if (transcriptsRes.ok) { projectTranscripts = await transcriptsRes.json(); } // 加载最新的转录 if (projectTranscripts.length > 0) { currentTranscript = await getTranscript(projectTranscripts[0].id); currentData = { transcript_id: currentTranscript.id, project_id: currentProject.id, segments: [{ speaker: '全文', text: currentTranscript.full_text }], entities: projectEntities, full_text: currentTranscript.full_text, created_at: currentTranscript.created_at }; renderTranscript(); } renderGraph(); renderEntityList(); renderTranscriptDropdown(); } catch (err) { console.error('Load project data failed:', err); } } // Phase 3: View Switching window.switchView = function(viewName) { // Update sidebar buttons document.querySelectorAll('.sidebar-btn').forEach(btn => { btn.classList.remove('active'); }); event.target.classList.add('active'); if (viewName === 'workbench') { document.getElementById('workbenchView').style.display = 'flex'; document.getElementById('knowledgeBaseView').classList.remove('show'); } else if (viewName === 'knowledge-base') { document.getElementById('workbenchView').style.display = 'none'; document.getElementById('knowledgeBaseView').classList.add('show'); loadKnowledgeBase(); } }; // Phase 3: Load Knowledge Base async function loadKnowledgeBase() { try { knowledgeBaseData = await fetchKnowledgeBase(); renderKnowledgeBase(); } catch (err) { console.error('Load knowledge base failed:', err); } } // Phase 3: Render Knowledge Base function renderKnowledgeBase() { if (!knowledgeBaseData) return; // Update stats document.getElementById('kbEntityCount').textContent = knowledgeBaseData.stats.entity_count; document.getElementById('kbRelationCount').textContent = knowledgeBaseData.stats.relation_count; document.getElementById('kbTranscriptCount').textContent = knowledgeBaseData.stats.transcript_count; document.getElementById('kbGlossaryCount').textContent = knowledgeBaseData.stats.glossary_count; // Render entities const entityGrid = document.getElementById('kbEntityGrid'); entityGrid.innerHTML = knowledgeBaseData.entities.map(e => `
${e.type}
${e.name}
${e.definition || '暂无定义'}
提及 ${e.mention_count} 次 | 出现在 ${e.appears_in.length} 个文件中
`).join(''); // Render relations const relationsList = document.getElementById('kbRelationsList'); relationsList.innerHTML = knowledgeBaseData.relations.map(r => `
${r.source_name} → ${r.type} → ${r.target_name}
${r.evidence || '无证据'}
`).join(''); // Render glossary const glossaryList = document.getElementById('kbGlossaryList'); glossaryList.innerHTML = knowledgeBaseData.glossary.map(g => `
${g.term} ${g.pronunciation ? ` (${g.pronunciation})` : ''} 出现 ${g.frequency} 次
`).join(''); // Render transcripts const transcriptsList = document.getElementById('kbTranscriptsList'); transcriptsList.innerHTML = knowledgeBaseData.transcripts.map(t => `
${t.type === 'audio' ? '🎵' : '📄'} ${t.filename}
${new Date(t.created_at).toLocaleDateString()}
`).join(''); } // Phase 3: KB Tab Switching window.switchKBTab = function(tabName) { document.querySelectorAll('.kb-nav-item').forEach(item => { item.classList.remove('active'); }); event.target.classList.add('active'); document.querySelectorAll('.kb-section').forEach(section => { section.classList.remove('active'); }); document.getElementById(`kb${tabName.charAt(0).toUpperCase() + tabName.slice(1)}Section`).classList.add('active'); }; // Phase 3: Transcript Dropdown window.toggleTranscriptDropdown = function() { const dropdown = document.getElementById('transcriptDropdown'); dropdown.classList.toggle('show'); }; function renderTranscriptDropdown() { const dropdown = document.getElementById('transcriptDropdown'); if (!dropdown || projectTranscripts.length === 0) return; dropdown.innerHTML = projectTranscripts.map(t => `
${(t.type || 'audio') === 'audio' ? '🎵' : '📄'} ${t.filename}
`).join(''); } window.switchTranscript = async function(transcriptId) { try { currentTranscript = await getTranscript(transcriptId); currentData = { transcript_id: currentTranscript.id, project_id: currentProject.id, segments: [{ speaker: '全文', text: currentTranscript.full_text }], entities: projectEntities, full_text: currentTranscript.full_text, created_at: currentTranscript.created_at }; renderTranscript(); renderTranscriptDropdown(); document.getElementById('transcriptDropdown').classList.remove('show'); } catch (err) { console.error('Switch transcript failed:', err); alert('切换文件失败'); } }; // Phase 2: Transcript Edit Mode window.toggleEditMode = function() { editMode = !editMode; const editBtn = document.getElementById('editBtn'); const saveBtn = document.getElementById('saveBtn'); const content = document.getElementById('transcriptContent'); if (editMode) { editBtn.style.display = 'none'; saveBtn.style.display = 'inline-block'; content.contentEditable = 'true'; content.style.background = '#0f0f0f'; content.style.border = '1px solid #00d4ff'; content.focus(); } else { editBtn.style.display = 'inline-block'; saveBtn.style.display = 'none'; content.contentEditable = 'false'; content.style.background = ''; content.style.border = ''; } }; window.saveTranscript = async function() { if (!currentTranscript) return; const content = document.getElementById('transcriptContent'); const fullText = content.innerText; try { await updateTranscript(currentTranscript.id, fullText); currentTranscript.full_text = fullText; toggleEditMode(); alert('转录文本已保存'); } catch (err) { console.error('Save failed:', err); alert('保存失败: ' + err.message); } }; // Render transcript with entity highlighting function renderTranscript() { const container = document.getElementById('transcriptContent'); if (!container || !currentData) return; container.innerHTML = ''; if (editMode) { container.innerText = currentData.full_text || ''; return; } // 高亮实体 let text = currentData.full_text || ''; const entities = findEntitiesInText(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); text = before + `${name}` + after; }); const div = document.createElement('div'); div.className = 'segment'; div.innerHTML = `
${currentTranscript.filename || '转录文本'}
${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; } // Render D3 graph with relations 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 })).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); // 关系标签 const linkLabel = svg.append('g') .selectAll('text') .data(links) .enter().append('text') .attr('font-size', '10px') .attr('fill', '#666') .attr('text-anchor', 'middle') .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('contextmenu', (e, d) => { e.preventDefault(); showContextMenu(e, d.id); }); // 节点圆圈 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'); // 节点类型图标 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); 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; } } // Render 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.oncontextmenu = (e) => { e.preventDefault(); showContextMenu(e, ent.id); }; div.innerHTML = ` ${ent.type}
${ent.name}
${ent.definition || '暂无定义'}
`; container.appendChild(div); }); } // Select entity - 联动高亮 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.classList.add('selected'); } else { el.classList.remove('selected'); } }); // 高亮图谱中的节点 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.classList.add('selected'); } else { el.classList.remove('selected'); } }); console.log('Selected:', entity.name, entity.definition); }; // Phase 2: Context Menu function initContextMenu() { document.addEventListener('click', () => { hideContextMenu(); }); } function showContextMenu(e, entityId) { contextMenuTarget = entityId; const menu = document.getElementById('contextMenu'); menu.style.left = e.pageX + 'px'; menu.style.top = e.pageY + 'px'; menu.classList.add('show'); } function hideContextMenu() { const menu = document.getElementById('contextMenu'); menu.classList.remove('show'); contextMenuTarget = null; } // Phase 2: Entity Editor Modal window.editEntity = function() { hideContextMenu(); if (!contextMenuTarget && !selectedEntity) return; const entityId = contextMenuTarget || selectedEntity; const entity = projectEntities.find(e => e.id === entityId); if (!entity) return; document.getElementById('entityName').value = entity.name; document.getElementById('entityType').value = entity.type; document.getElementById('entityDefinition').value = entity.definition || ''; document.getElementById('entityAliases').value = (entity.aliases || []).join(', '); // 显示关系编辑器 document.getElementById('relationEditor').style.display = 'block'; renderRelationList(entityId); document.getElementById('entityModal').dataset.entityId = entityId; document.getElementById('entityModal').classList.add('show'); }; function renderRelationList(entityId) { const container = document.getElementById('relationList'); const entityRelations = projectRelations.filter(r => r.source_id === entityId || r.target_id === entityId ); if (entityRelations.length === 0) { container.innerHTML = '

暂无关系

'; return; } container.innerHTML = entityRelations.map(r => { const isSource = r.source_id === entityId; const otherId = isSource ? r.target_id : r.source_id; const other = projectEntities.find(e => e.id === otherId); const otherName = other ? other.name : 'Unknown'; const arrow = isSource ? '→' : '←'; return `
${arrow} ${otherName} (${r.type})
`; }).join(''); } window.hideEntityModal = function() { document.getElementById('entityModal').classList.remove('show'); }; window.saveEntity = async function() { const entityId = document.getElementById('entityModal').dataset.entityId; if (!entityId) return; const data = { name: document.getElementById('entityName').value, type: document.getElementById('entityType').value, definition: document.getElementById('entityDefinition').value, aliases: document.getElementById('entityAliases').value.split(',').map(s => s.trim()).filter(s => s) }; try { await updateEntity(entityId, data); await loadProjectData(); hideEntityModal(); } catch (err) { console.error('Save failed:', err); alert('保存失败: ' + err.message); } }; window.deleteEntity = async function() { const entityId = document.getElementById('entityModal').dataset.entityId; if (!entityId) return; if (!confirm('确定要删除这个实体吗?相关的提及和关系也会被删除。')) return; try { await deleteEntityApi(entityId); await loadProjectData(); hideEntityModal(); } catch (err) { console.error('Delete failed:', err); alert('删除失败: ' + err.message); } }; // Phase 2: Merge Modal window.showMergeModal = function() { hideContextMenu(); if (!contextMenuTarget && !selectedEntity) return; const sourceId = contextMenuTarget || selectedEntity; const source = projectEntities.find(e => e.id === sourceId); if (!source) return; document.getElementById('mergeSource').value = source.name; document.getElementById('mergeModal').dataset.sourceId = sourceId; // 填充目标实体选项(排除自己) const select = document.getElementById('mergeTarget'); select.innerHTML = projectEntities .filter(e => e.id !== sourceId) .map(e => ``) .join(''); document.getElementById('mergeModal').classList.add('show'); }; window.hideMergeModal = function() { document.getElementById('mergeModal').classList.remove('show'); }; window.confirmMerge = async function() { const sourceId = document.getElementById('mergeModal').dataset.sourceId; const targetId = document.getElementById('mergeTarget').value; if (!sourceId || !targetId) return; try { await mergeEntitiesApi(sourceId, targetId); await loadProjectData(); hideMergeModal(); } catch (err) { console.error('Merge failed:', err); alert('合并失败: ' + err.message); } }; // Phase 2: Relation Modal window.showAddRelation = function() { const entityId = document.getElementById('entityModal').dataset.entityId; if (!entityId) return; const entity = projectEntities.find(e => e.id === entityId); document.getElementById('relationModal').dataset.sourceId = entityId; // 填充目标选项 const select = document.getElementById('relationTarget'); select.innerHTML = projectEntities .filter(e => e.id !== entityId) .map(e => ``) .join(''); document.getElementById('relationModal').classList.add('show'); }; window.hideRelationModal = function() { document.getElementById('relationModal').classList.remove('show'); }; window.saveRelation = async function() { const sourceId = document.getElementById('relationModal').dataset.sourceId; const targetId = document.getElementById('relationTarget').value; const type = document.getElementById('relationType').value; const evidence = document.getElementById('relationEvidence').value; if (!sourceId || !targetId) return; try { await createRelationApi({ source_entity_id: sourceId, target_entity_id: targetId, relation_type: type, evidence: evidence }); await loadProjectData(); renderRelationList(sourceId); hideRelationModal(); } catch (err) { console.error('Create relation failed:', err); alert('创建关系失败: ' + err.message); } }; window.deleteRelation = async function(relationId) { if (!confirm('确定要删除这个关系吗?')) return; try { await deleteRelationApi(relationId); await loadProjectData(); const entityId = document.getElementById('entityModal').dataset.entityId; if (entityId) renderRelationList(entityId); } catch (err) { console.error('Delete relation failed:', err); alert('删除关系失败: ' + err.message); } }; // Phase 2: Text Selection - Create Entity function initTextSelection() { document.addEventListener('selectionchange', () => { const selection = window.getSelection(); const text = selection.toString().trim(); if (text.length > 0 && text.length < 50) { showSelectionToolbar(); } else { hideSelectionToolbar(); } }); } function showSelectionToolbar() { document.getElementById('selectionToolbar').classList.add('show'); } window.hideSelectionToolbar = function() { document.getElementById('selectionToolbar').classList.remove('show'); window.getSelection().removeAllRanges(); }; window.createEntityFromSelection = async function() { const selection = window.getSelection(); const text = selection.toString().trim(); if (!text) return; // 获取选中文本在全文中的位置 const container = document.getElementById('transcriptContent'); const fullText = currentTranscript ? currentTranscript.full_text : ''; const startPos = fullText.indexOf(text); try { const result = await createEntityApi({ name: text, type: 'OTHER', definition: '', transcript_id: currentTranscript ? currentTranscript.id : null, start_pos: startPos >= 0 ? startPos : null, end_pos: startPos >= 0 ? startPos + text.length : null }); hideSelectionToolbar(); await loadProjectData(); if (!result.existed) { alert(`已创建实体: ${text}`); } else { alert(`实体 "${text}" 已存在`); } } catch (err) { console.error('Create entity failed:', err); alert('创建实体失败: ' + err.message); } }; // Phase 3: Upload Tab Switching window.switchUploadTab = function(tab) { currentUploadTab = tab; document.querySelectorAll('.upload-tab').forEach(t => t.classList.remove('active')); event.target.classList.add('active'); const hint = document.getElementById('uploadHint'); if (tab === 'audio') { hint.textContent = '支持 MP3, WAV, M4A (最大 500MB)'; } else { hint.textContent = '支持 PDF, DOCX, DOC, TXT, MD'; } }; window.triggerFileSelect = function() { if (currentUploadTab === 'audio') { document.getElementById('fileInput').click(); } else { document.getElementById('docInput').click(); } }; // Show/hide 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'); }; // Phase 3: Glossary Modal window.showAddTermModal = function() { document.getElementById('glossaryModal').classList.add('show'); }; window.hideGlossaryModal = function() { document.getElementById('glossaryModal').classList.remove('show'); document.getElementById('glossaryTerm').value = ''; document.getElementById('glossaryPronunciation').value = ''; }; window.saveGlossaryTerm = async function() { const term = document.getElementById('glossaryTerm').value.trim(); const pronunciation = document.getElementById('glossaryPronunciation').value.trim(); if (!term) { alert('请输入术语'); return; } try { await addGlossaryTerm(term, pronunciation); hideGlossaryModal(); loadKnowledgeBase(); } catch (err) { console.error('Add term failed:', err); alert('添加术语失败: ' + err.message); } }; // Upload handling function initUpload() { // Audio upload const audioInput = document.getElementById('fileInput'); if (audioInput) { audioInput.addEventListener('change', async (e) => { if (!e.target.files.length) return; await handleFileUpload(e.target.files[0], 'audio'); }); } // Document upload const docInput = document.getElementById('docInput'); if (docInput) { docInput.addEventListener('change', async (e) => { if (!e.target.files.length) return; await handleFileUpload(e.target.files[0], 'document'); }); } } async function handleFileUpload(file, type) { const overlay = document.getElementById('uploadOverlay'); overlay.innerHTML = `

正在分析...

${file.name}

${type === 'audio' ? 'ASR转录 + 实体提取中' : '文档解析 + 实体提取中'}

`; try { let result; if (type === 'audio') { result = await uploadAudio(file); } else { result = await uploadDocument(file); } // 更新当前数据 currentData = result; // 重新加载项目数据 await loadProjectData(); // 重置上传界面 overlay.innerHTML = `

上传文件

🎵 音频
📄 文档

支持 MP3, WAV, M4A (最大 500MB)



`; // 重新绑定事件 initUpload(); overlay.classList.remove('show'); } catch (err) { console.error('Upload failed:', err); overlay.innerHTML = `

分析失败

${err.message}

`; } } // Close dropdown when clicking outside document.addEventListener('click', (e) => { const dropdown = document.getElementById('transcriptDropdown'); const selector = document.querySelector('.transcript-selector'); if (dropdown && selector && !selector.contains(e.target)) { dropdown.classList.remove('show'); } });