// InsightFlow Frontend - Production Version const API_BASE = '/api/v1'; let currentProject = null; let currentData = null; let selectedEntity = null; let projectRelations = []; let projectEntities = []; // 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(); 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(); } 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); } } // Render transcript with entity highlighting 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); 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; } // 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 => ({ 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)); // 节点圆圈 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.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.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); }; // 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'); }; // Upload handling 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}

`; } } }); }