请上传音频文件开始分析
项目实体
+暂无实体数据
diff --git a/frontend/app.js b/frontend/app.js index a8663b6..54dfac2 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -1,57 +1,126 @@ -// InsightFlow Phase 2 - Workbench +// InsightFlow Frontend - Production Version const API_BASE = '/api/v1'; +let currentProject = null; let currentData = null; let selectedEntity = null; // Init document.addEventListener('DOMContentLoaded', () => { - initUpload(); - loadSampleData(); // For demo + initApp(); }); -// Sample data for demo -function loadSampleData() { - currentData = { - transcript_id: "demo_001", - segments: [ - { - start: 0, - end: 5, - text: "我们今天讨论 Project Alpha 的进度,需要协调后端团队和前端团队。", - speaker: "张三" - }, - { - start: 5, - end: 12, - text: "K8s 集群已经部署完成,但是 Redis 缓存出现了一些问题。", - speaker: "李四" - }, - { - start: 12, - end: 18, - text: "建议下周进行 Code Review,确保代码质量符合标准。", - speaker: "王五" - } - ], - entities: [ - { id: 'ent_1', name: 'Project Alpha', type: 'PROJECT', start: 8, end: 21, definition: '当前核心开发项目' }, - { id: 'ent_2', name: 'K8s', type: 'TECH', start: 0, end: 3, definition: 'Kubernetes容器编排平台' }, - { id: 'ent_3', name: 'Redis', type: 'TECH', start: 32, end: 37, definition: '内存数据库缓存系统' }, - { id: 'ent_4', name: 'Code Review', type: 'OTHER', start: 10, end: 21, definition: '代码审查流程' } - ], - full_text: "我们今天讨论 Project Alpha 的进度..." +// Initialize app - load projects +async function initApp() { + try { + const projects = await fetchProjects(); + if (projects.length === 0) { + // Create default project + await createProject('默认项目', '自动创建的默认项目'); + location.reload(); + return; + } + + currentProject = projects[0]; + renderProjectSelector(projects); + initUpload(); + + // Load existing entities for this project + 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 createProject(name, description) { + const res = await fetch(`${API_BASE}/projects`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name, description }) + }); + if (!res.ok) throw new Error('Failed to create project'); + 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(); + + // Convert to currentData format + 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 project selector +function renderProjectSelector(projects) { + // Simple project selector in header + const header = document.querySelector('.header'); + const selector = document.createElement('select'); + selector.style.cssText = 'background:#222;color:#fff;border:1px solid #333;padding:4px 12px;border-radius:4px;margin-left:20px;'; + + projects.forEach(p => { + const opt = document.createElement('option'); + opt.value = p.id; + opt.textContent = p.name; + if (p.id === currentProject.id) opt.selected = true; + selector.appendChild(opt); + }); + + selector.onchange = (e) => { + currentProject = projects.find(p => p.id === e.target.value); + loadProjectEntities(); }; - renderTranscript(); - renderGraph(); - renderEntityList(); - document.getElementById('uploadOverlay').classList.add('hidden'); + header.appendChild(selector); } // 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) => { @@ -59,18 +128,16 @@ function renderTranscript() { div.className = 'segment'; div.dataset.index = idx; - // Highlight entities in text let text = seg.text; const entities = findEntitiesInSegment(seg, idx); - // Sort by position (descending) to replace from end 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; + text = before + `${name}` + after; }); div.innerHTML = ` @@ -84,7 +151,8 @@ function renderTranscript() { // Find entities within a segment function findEntitiesInSegment(seg, segIndex) { - // Simple calculation - in real app, need proper offset tracking + if (!currentData || !currentData.entities) return []; + let offset = 0; for (let i = 0; i < segIndex; i++) { offset += currentData.segments[i].text.length + 1; @@ -104,12 +172,21 @@ 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; // Minus entity list + const height = svg.node().parentElement.clientHeight - 200; svg.attr('width', width).attr('height', height); - // Prepare nodes and links const nodes = currentData.entities.map(e => ({ id: e.id, name: e.name, @@ -117,13 +194,11 @@ function renderGraph() { ...e })); - // Create some sample links (in real app, extract from relationships) const links = []; for (let i = 0; i < nodes.length - 1; i++) { links.push({ source: nodes[i].id, target: nodes[i + 1].id }); } - // Color scale const colorMap = { 'PROJECT': '#7b2cbf', 'TECH': '#00d4ff', @@ -132,14 +207,12 @@ function renderGraph() { 'OTHER': '#666' }; - // Force simulation 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)); - // Draw links const link = svg.append('g') .selectAll('line') .data(links) @@ -147,7 +220,6 @@ function renderGraph() { .attr('stroke', '#333') .attr('stroke-width', 1); - // Draw nodes const node = svg.append('g') .selectAll('g') .data(nodes) @@ -157,16 +229,14 @@ function renderGraph() { .on('start', dragstarted) .on('drag', dragged) .on('end', dragended)) - .on('click', (e, d) => selectEntity(d.id)); + .on('click', (e, d) => window.selectEntity(d.id)); - // Node circles node.append('circle') .attr('r', 30) .attr('fill', d => colorMap[d.type] || '#666') .attr('stroke', '#fff') .attr('stroke-width', 2); - // Node labels node.append('text') .text(d => d.name.length > 8 ? d.name.slice(0, 6) + '...' : d.name) .attr('text-anchor', 'middle') @@ -174,7 +244,6 @@ function renderGraph() { .attr('fill', '#fff') .attr('font-size', '11px'); - // Update positions simulation.on('tick', () => { link .attr('x1', d => d.source.x) @@ -206,12 +275,19 @@ function renderGraph() { // Render entity list function renderEntityList() { const container = document.getElementById('entityList'); - container.innerHTML = '
暂无实体,请上传音频文件
'; + return; + } currentData.entities.forEach(ent => { const div = document.createElement('div'); div.className = 'entity-item'; - div.onclick = () => selectEntity(ent.id); + div.onclick = () => window.selectEntity(ent.id); div.innerHTML = ` ${ent.type} @@ -225,24 +301,22 @@ function renderEntityList() { }); } -// Select entity (highlight in both views) -function selectEntity(entityId) { +// Select entity +window.selectEntity = function(entityId) { selectedEntity = entityId; - const entity = currentData.entities.find(e => e.id === entityId); + const entity = currentData && currentData.entities.find(e => e.id === entityId); if (!entity) return; - // Highlight in transcript document.querySelectorAll('.entity').forEach(el => { el.style.background = el.dataset.id === entityId ? '#ff6b6b' : ''; }); - // Highlight in graph 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); -} +}; // Upload handling function initUpload() { @@ -252,11 +326,33 @@ function initUpload() { input.addEventListener('change', async (e) => { if (!e.target.files.length) return; - overlay.innerHTML = '${file.name}
+${err.message}
+ +请上传音频文件开始分析
暂无实体数据