// InsightFlow Frontend - Production Version const API_BASE = '/api/v1'; let currentProject = null; let currentData = null; let selectedEntity = 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(); await loadProjectEntities(); } 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 loadProjectEntities() { try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/entities`); if (!res.ok) return; const entities = await res.json(); currentData = { transcript_id: 'project_view', project_id: currentProject.id, segments: [], entities: entities.map(e => ({ id: e.id, name: e.name, type: e.type, definition: e.definition || '' })), full_text: '', created_at: new Date().toISOString() }; renderGraph(); renderEntityList(); } catch (err) { console.error('Load entities failed:', err); } } // Render transcript 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 = findEntitiesInSegment(seg, idx); 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 findEntitiesInSegment(seg, segIndex) { if (!currentData || !currentData.entities) return []; let offset = 0; for (let i = 0; i < segIndex; i++) { offset += currentData.segments[i].text.length + 1; } return currentData.entities.filter(ent => { return ent.start >= offset && ent.end <= offset + seg.text.length; }).map(ent => ({ ...ent, start: ent.start - offset, end: ent.end - offset })); } // Render D3 graph function renderGraph() { const svg = d3.select('#graph-svg'); svg.selectAll('*').remove(); if (!currentData || !currentData.entities || currentData.entities.length === 0) { svg.append('text') .attr('x', '50%') .attr('y', '50%') .attr('text-anchor', 'middle') .attr('fill', '#666') .text('暂无实体数据,请上传音频'); return; } const width = svg.node().parentElement.clientWidth; const height = svg.node().parentElement.clientHeight - 200; svg.attr('width', width).attr('height', height); const nodes = currentData.entities.map(e => ({ id: e.id, name: e.name, type: e.type, ...e })); const links = []; for (let i = 0; i < nodes.length - 1; i++) { links.push({ source: nodes[i].id, target: nodes[i + 1].id }); } 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(100)) .force('charge', d3.forceManyBody().strength(-300)) .force('center', d3.forceCenter(width / 2, height / 2)) .force('collision', d3.forceCollide().radius(40)); const link = svg.append('g') .selectAll('line') .data(links) .enter().append('line') .attr('stroke', '#333') .attr('stroke-width', 1); 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', 30) .attr('fill', d => colorMap[d.type] || '#666') .attr('stroke', '#fff') .attr('stroke-width', 2); node.append('text') .text(d => d.name.length > 8 ? d.name.slice(0, 6) + '...' : d.name) .attr('text-anchor', 'middle') .attr('dy', 5) .attr('fill', '#fff') .attr('font-size', '11px'); 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); 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 (!currentData || !currentData.entities || currentData.entities.length === 0) { container.innerHTML += '

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

'; return; } currentData.entities.forEach(ent => { const div = document.createElement('div'); div.className = 'entity-item'; 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 = currentData && currentData.entities.find(e => e.id === entityId); if (!entity) return; document.querySelectorAll('.entity').forEach(el => { el.style.background = el.dataset.id === entityId ? '#ff6b6b' : ''; }); d3.selectAll('.node circle') .attr('stroke', d => d.id === entityId ? '#ff6b6b' : '#fff') .attr('stroke-width', d => d.id === entityId ? 4 : 2); console.log('Selected:', entity.name); }; // 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}

`; } try { const result = await uploadAudio(file); currentData = result; renderTranscript(); renderGraph(); renderEntityList(); if (overlay) overlay.classList.remove('show'); } catch (err) { console.error('Upload failed:', err); if (overlay) { overlay.innerHTML = `

分析失败

${err.message}

`; } } }); }