From b28af9a611871a9a425a552442fb754e8424e0b8 Mon Sep 17 00:00:00 2001 From: OpenClaw Bot Date: Tue, 17 Feb 2026 13:38:03 +0800 Subject: [PATCH] feat: Phase 2 workbench - dual-view editor with D3 graph --- frontend/app.js | 268 ++++++++++++++++++++++++++++++++++++++++++-- frontend/index.html | 237 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 488 insertions(+), 17 deletions(-) diff --git a/frontend/app.js b/frontend/app.js index f187b72..a8663b6 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -1,10 +1,262 @@ +// InsightFlow Phase 2 - Workbench const API_BASE = '/api/v1'; -async function upload(file) { - const formData = new FormData(); - formData.append('file', file); - const res = await fetch(API_BASE + '/upload', { - method: 'POST', - body: formData - }); - return await res.json(); + +let currentData = null; +let selectedEntity = null; + +// Init +document.addEventListener('DOMContentLoaded', () => { + initUpload(); + loadSampleData(); // For demo +}); + +// 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 的进度..." + }; + + renderTranscript(); + renderGraph(); + renderEntityList(); + document.getElementById('uploadOverlay').classList.add('hidden'); +} + +// Render transcript with entity highlighting +function renderTranscript() { + const container = document.getElementById('transcriptContent'); + container.innerHTML = ''; + + currentData.segments.forEach((seg, idx) => { + const div = document.createElement('div'); + 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; + }); + + div.innerHTML = ` +
${seg.speaker}
+
${text}
+ `; + + container.appendChild(div); + }); +} + +// Find entities within a segment +function findEntitiesInSegment(seg, segIndex) { + // Simple calculation - in real app, need proper offset tracking + 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 force-directed graph +function renderGraph() { + const svg = d3.select('#graph-svg'); + svg.selectAll('*').remove(); + + const width = svg.node().parentElement.clientWidth; + const height = svg.node().parentElement.clientHeight - 200; // Minus entity list + + svg.attr('width', width).attr('height', height); + + // Prepare nodes and links + const nodes = currentData.entities.map(e => ({ + id: e.id, + name: e.name, + type: e.type, + ...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', + 'PERSON': '#ff6b6b', + 'ORG': '#4ecdc4', + '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) + .enter().append('line') + .attr('stroke', '#333') + .attr('stroke-width', 1); + + // Draw nodes + 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) => 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') + .attr('dy', 5) + .attr('fill', '#fff') + .attr('font-size', '11px'); + + // Update positions + 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'); + container.innerHTML = '

识别实体

'; + + currentData.entities.forEach(ent => { + const div = document.createElement('div'); + div.className = 'entity-item'; + div.onclick = () => selectEntity(ent.id); + + div.innerHTML = ` + ${ent.type} +
+
${ent.name}
+
${ent.definition || '暂无定义'}
+
+ `; + + container.appendChild(div); + }); +} + +// Select entity (highlight in both views) +function selectEntity(entityId) { + selectedEntity = entityId; + const entity = 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() { + const input = document.getElementById('fileInput'); + const overlay = document.getElementById('uploadOverlay'); + + input.addEventListener('change', async (e) => { + if (!e.target.files.length) return; + + overlay.innerHTML = '

分析中...

'; + + // TODO: Upload and analyze + setTimeout(() => { + overlay.classList.add('hidden'); + }, 2000); + }); } diff --git a/frontend/index.html b/frontend/index.html index f6570ee..f81ce46 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,18 +1,237 @@ - + - InsightFlow MVP + + InsightFlow - Phase 2 Workbench + -

InsightFlow

-

Phase 1 MVP - 音频转录与实体提取

-
拖拽音频文件上传
+
+

InsightFlow Workbench

+ Phase 2 - 双视图联动 +
+ +
+ +
+
+ 📄 转录文本 + 点击实体高亮 | 划词新建 +
+
+ +
+
+ + +
+
+ 🔗 知识图谱 + 拖拽节点 | 点击查看关系 +
+ +
+ +
+
+
+ + +
+ + +
+
+

上传音频开始分析

+

支持 MP3, WAV, M4A

+ + +
+
+ +