Files
insightflow/frontend/app.js

263 lines
8.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// InsightFlow Phase 2 - Workbench
const API_BASE = '/api/v1';
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 + `<span class="entity" data-id="${ent.id}" onclick="selectEntity('${ent.id}')">${name}</span>` + after;
});
div.innerHTML = `
<div class="speaker">${seg.speaker}</div>
<div class="segment-text">${text}</div>
`;
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 = '<h3 style="margin-bottom:12px;color:#888;font-size:0.9rem;">识别实体</h3>';
currentData.entities.forEach(ent => {
const div = document.createElement('div');
div.className = 'entity-item';
div.onclick = () => selectEntity(ent.id);
div.innerHTML = `
<span class="entity-type-badge type-${ent.type.toLowerCase()}">${ent.type}</span>
<div>
<div style="font-weight:500;">${ent.name}</div>
<div style="font-size:0.8rem;color:#666;">${ent.definition || '暂无定义'}</div>
</div>
`;
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 = '<div style="text-align:center;"><h2>分析中...</h2></div>';
// TODO: Upload and analyze
setTimeout(() => {
overlay.classList.add('hidden');
}, 2000);
});
}