// InsightFlow Frontend - Phase 5 (Graph Analysis) const API_BASE = '/api/v1'; let currentProject = null; let currentData = null; let selectedEntity = null; let projectRelations = []; let projectEntities = []; let entityDetailsCache = {}; // Graph Analysis State let graphStats = null; let centralityData = null; let communitiesData = null; let currentPathData = 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(); initAgentPanel(); initEntityCard(); 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(); } // 预加载实体详情 await preloadEntityDetails(); currentData = { transcript_id: 'project_view', project_id: currentProject.id, segments: [], entities: projectEntities, full_text: '', created_at: new Date().toISOString() }; renderGraph(); renderEntityList(); // 更新图分析面板的实体选择器 populateGraphEntitySelects(); } catch (err) { console.error('Load project data failed:', err); } } async function preloadEntityDetails() { // 并行加载所有实体详情 const promises = projectEntities.map(async (ent) => { try { const res = await fetch(`${API_BASE}/entities/${ent.id}/details`); if (res.ok) { entityDetailsCache[ent.id] = await res.json(); } } catch (e) { console.error(`Failed to load entity ${ent.id} details:`, e); } }); await Promise.all(promises); } // ==================== Agent Panel ==================== function initAgentPanel() { const chatInput = document.getElementById('chatInput'); if (chatInput) { chatInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendAgentMessage(); } }); } } function toggleAgentPanel() { const panel = document.getElementById('agentPanel'); const toggle = panel.querySelector('.agent-toggle'); panel.classList.toggle('collapsed'); toggle.textContent = panel.classList.contains('collapsed') ? '‹' : '›'; } function addChatMessage(content, isUser = false, isTyping = false) { const container = document.getElementById('chatMessages'); const msgDiv = document.createElement('div'); msgDiv.className = `chat-message ${isUser ? 'user' : 'assistant'}`; if (isTyping) { msgDiv.innerHTML = `
`; } else { msgDiv.innerHTML = `
${content}
`; } container.appendChild(msgDiv); container.scrollTop = container.scrollHeight; return msgDiv; } function removeTypingIndicator() { const indicator = document.getElementById('typingIndicator'); if (indicator) { indicator.parentElement.remove(); } } async function sendAgentMessage() { const input = document.getElementById('chatInput'); const message = input.value.trim(); if (!message) return; input.value = ''; addChatMessage(message, true); addChatMessage('', false, true); try { // 判断是命令还是问答 const isCommand = message.includes('合并') || message.includes('修改') || message.startsWith('把') || message.startsWith('将'); if (isCommand) { // 执行命令 const res = await fetch(`${API_BASE}/projects/${currentProject.id}/agent/command`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command: message }) }); removeTypingIndicator(); if (res.ok) { const result = await res.json(); let response = ''; if (result.intent === 'merge_entities') { if (result.success) { response = `✅ 已合并 ${result.merged.length} 个实体到 "${result.target}"`; await loadProjectData(); // 刷新数据 } else { response = `❌ 合并失败:${result.error || '未找到匹配的实体'}`; } } else if (result.intent === 'edit_entity') { if (result.success) { response = `✅ 已更新实体 "${result.entity?.name}"`; await loadProjectData(); } else { response = `❌ 编辑失败:${result.error || '未找到实体'}`; } } else if (result.intent === 'answer_question') { response = result.answer; } else { response = result.message || result.explanation || '未识别的指令'; } addChatMessage(response); } else { addChatMessage('❌ 请求失败,请重试'); } } else { // RAG 问答 const res = await fetch(`${API_BASE}/projects/${currentProject.id}/agent/query`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: message, stream: false }) }); removeTypingIndicator(); if (res.ok) { const result = await res.json(); addChatMessage(result.answer); } else { addChatMessage('❌ 获取回答失败,请重试'); } } } catch (err) { removeTypingIndicator(); addChatMessage('❌ 网络错误,请检查连接'); console.error('Agent error:', err); } } async function loadSuggestions() { addChatMessage('正在获取建议...', false, true); try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/agent/suggest`); removeTypingIndicator(); if (res.ok) { const result = await res.json(); const suggestions = result.suggestions || []; if (suggestions.length === 0) { addChatMessage('暂无建议,请先上传一些音频文件。'); return; } let html = '
💡 基于项目数据的建议:
'; suggestions.forEach((s, i) => { html += `
${s.type === 'action' ? '⚡ 操作' : '💡 洞察'}
${s.title}
${s.description}
`; }); const msgDiv = document.createElement('div'); msgDiv.className = 'chat-message assistant'; msgDiv.innerHTML = `
${html}
`; document.getElementById('chatMessages').appendChild(msgDiv); } } catch (err) { removeTypingIndicator(); addChatMessage('❌ 获取建议失败'); } } function applySuggestion(index) { // 可以在这里实现建议的自动应用 addChatMessage('建议功能开发中,敬请期待!'); } // ==================== Transcript Rendering ==================== 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); const details = entityDetailsCache[ent.id]; const confidence = details?.mentions?.[0]?.confidence || 1.0; const lowConfClass = confidence < 0.7 ? 'low-confidence' : ''; 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; } // ==================== Entity Card ==================== function initEntityCard() { const card = document.getElementById('entityCard'); // 鼠标移出卡片时隐藏 card.addEventListener('mouseleave', () => { card.classList.remove('show'); }); } function showEntityCard(event, entityId) { const card = document.getElementById('entityCard'); const details = entityDetailsCache[entityId]; const entity = projectEntities.find(e => e.id === entityId); if (!entity) return; // 更新卡片内容 document.getElementById('cardName').textContent = entity.name; document.getElementById('cardBadge').textContent = entity.type; document.getElementById('cardBadge').className = `entity-type-badge type-${entity.type.toLowerCase()}`; document.getElementById('cardDefinition').textContent = entity.definition || '暂无定义'; const mentionCount = details?.mentions?.length || 0; const relationCount = details?.relations?.length || 0; document.getElementById('cardMentions').textContent = `${mentionCount} 次提及`; document.getElementById('cardRelations').textContent = `${relationCount} 个关系`; // 定位卡片 const rect = event.target.getBoundingClientRect(); card.style.left = `${rect.left}px`; card.style.top = `${rect.bottom + 10}px`; // 确保不超出屏幕 const cardRect = card.getBoundingClientRect(); if (cardRect.right > window.innerWidth) { card.style.left = `${window.innerWidth - cardRect.width - 20}px`; } card.classList.add('show'); } function hideEntityCard() { // 延迟隐藏,允许鼠标移到卡片上 setTimeout(() => { const card = document.getElementById('entityCard'); if (!card.matches(':hover')) { card.classList.remove('show'); } }, 100); } // ==================== Graph Visualization ==================== 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 => ({ id: r.id, source: r.source_id, target: r.target_id, type: r.type, evidence: r.evidence })).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) .style('cursor', 'pointer') .on('click', (e, d) => showProvenance(d)); // 关系标签 const linkLabel = svg.append('g') .selectAll('text') .data(links) .enter().append('text') .attr('font-size', '10px') .attr('fill', '#666') .attr('text-anchor', 'middle') .style('pointer-events', 'none') .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)) .on('mouseenter', (e, d) => showEntityCard(e, d.id)) .on('mouseleave', hideEntityCard); // 节点圆圈 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') .style('pointer-events', 'none'); // 节点类型图标 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) .style('pointer-events', 'none'); 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; } } // ==================== Provenance ==================== async function showProvenance(relation) { const modal = document.getElementById('provenanceModal'); const body = document.getElementById('provenanceBody'); modal.classList.add('show'); body.innerHTML = '

加载中...

'; try { let content = ''; if (relation.id) { // 从API获取溯源信息 const res = await fetch(`${API_BASE}/relations/${relation.id}/provenance`); if (res.ok) { const data = await res.json(); content = `
关系类型
${data.source} → ${data.type} → ${data.target}
来源文档
${data.transcript?.filename || '未知文件'}
证据文本
"${data.evidence || '无证据文本'}"
`; } else { content = '

获取溯源信息失败

'; } } else { // 使用本地数据 content = `
关系类型
${relation.source.name || relation.source} → ${relation.type} → ${relation.target.name || relation.target}
证据文本
"${relation.evidence || '无证据文本'}"
`; } body.innerHTML = content; } catch (err) { body.innerHTML = '

加载失败

'; } } function closeProvenance() { document.getElementById('provenanceModal').classList.remove('show'); } // ==================== 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.onmouseenter = (e) => showEntityCard(e, ent.id); div.onmouseleave = hideEntityCard; div.innerHTML = ` ${ent.type}
${ent.name}
${ent.definition || '暂无定义'}
`; container.appendChild(div); }); } // ==================== Entity Selection ==================== 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); }; // ==================== 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'); }; 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}

`; } } }); } // ==================== Phase 5: Timeline View ==================== async function loadTimeline() { const container = document.getElementById('timelineContainer'); const entityFilter = document.getElementById('timelineEntityFilter'); if (!container) return; container.innerHTML = '

加载时间线数据...

'; try { // 更新实体筛选器选项 if (entityFilter && projectEntities.length > 0) { const currentValue = entityFilter.value; entityFilter.innerHTML = ''; projectEntities.forEach(ent => { const option = document.createElement('option'); option.value = ent.id; option.textContent = ent.name; entityFilter.appendChild(option); }); entityFilter.value = currentValue; } // 构建查询参数 const params = new URLSearchParams(); if (entityFilter && entityFilter.value) { params.append('entity_id', entityFilter.value); } // 获取时间线数据 const res = await fetch(`${API_BASE}/projects/${currentProject.id}/timeline?${params}`); if (!res.ok) throw new Error('Failed to load timeline'); const data = await res.json(); const events = data.events || []; // 更新统计 const mentions = events.filter(e => e.type === 'mention').length; const relations = events.filter(e => e.type === 'relation').length; document.getElementById('timelineTotalEvents').textContent = events.length; document.getElementById('timelineMentions').textContent = mentions; document.getElementById('timelineRelations').textContent = relations; // 渲染时间线 renderTimeline(events); } catch (err) { console.error('Load timeline failed:', err); container.innerHTML = '

加载失败,请重试

'; } } function renderTimeline(events) { const container = document.getElementById('timelineContainer'); if (events.length === 0) { container.innerHTML = `

暂无时间线数据

请先上传音频或文档文件

`; return; } // 按日期分组 const grouped = groupEventsByDate(events); let html = '
'; Object.entries(grouped).forEach(([date, dayEvents]) => { const dateLabel = formatDateLabel(date); html += `
${dateLabel}
`; dayEvents.forEach(event => { html += renderTimelineEvent(event); }); html += '
'; }); container.innerHTML = html; } function groupEventsByDate(events) { const grouped = {}; events.forEach(event => { const date = event.event_date.split('T')[0]; if (!grouped[date]) { grouped[date] = []; } grouped[date].push(event); }); return grouped; } function formatDateLabel(dateStr) { const date = new Date(dateStr); const today = new Date(); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); if (dateStr === today.toISOString().split('T')[0]) { return '今天'; } else if (dateStr === yesterday.toISOString().split('T')[0]) { return '昨天'; } else { return `${date.getMonth() + 1}月${date.getDate()}日`; } } function renderTimelineEvent(event) { if (event.type === 'mention') { return `
提及 ${event.entity_name} ${event.entity_type || 'OTHER'}
"${event.text_snippet || ''}"
📄 ${event.source?.filename || '未知文件'} ${event.confidence ? `置信度: ${(event.confidence * 100).toFixed(0)}%` : ''}
`; } else if (event.type === 'relation') { return `
关系 ${event.source_entity} → ${event.target_entity}
关系类型: ${event.relation_type}
${event.evidence ? `
"${event.evidence}"
` : ''}
📄 ${event.source?.filename || '未知文件'}
`; } return ''; } // ==================== View Switching ==================== window.switchView = function(viewName) { // 更新侧边栏按钮状态 document.querySelectorAll('.sidebar-btn').forEach(btn => { btn.classList.remove('active'); }); // 隐藏所有视图 document.getElementById('workbenchView').style.display = 'none'; document.getElementById('knowledgeBaseView').classList.remove('show'); document.getElementById('timelineView').classList.remove('show'); document.getElementById('reasoningView').classList.remove('active'); document.getElementById('graphAnalysisView').classList.remove('active'); // 显示选中的视图 if (viewName === 'workbench') { document.getElementById('workbenchView').style.display = 'flex'; document.querySelector('.sidebar-btn:nth-child(1)').classList.add('active'); } else if (viewName === 'knowledge-base') { document.getElementById('knowledgeBaseView').classList.add('show'); document.querySelector('.sidebar-btn:nth-child(2)').classList.add('active'); loadKnowledgeBase(); } else if (viewName === 'timeline') { document.getElementById('timelineView').classList.add('show'); document.querySelector('.sidebar-btn:nth-child(3)').classList.add('active'); loadTimeline(); } else if (viewName === 'reasoning') { document.getElementById('reasoningView').classList.add('active'); document.querySelector('.sidebar-btn:nth-child(4)').classList.add('active'); } else if (viewName === 'graph-analysis') { document.getElementById('graphAnalysisView').classList.add('active'); document.querySelector('.sidebar-btn:nth-child(5)').classList.add('active'); initGraphAnalysis(); } }; window.switchKBTab = function(tabName) { // 更新导航项状态 document.querySelectorAll('.kb-nav-item').forEach(item => { item.classList.remove('active'); }); // 隐藏所有部分 document.querySelectorAll('.kb-section').forEach(section => { section.classList.remove('active'); }); // 显示选中的部分 const tabMap = { 'entities': { nav: 0, section: 'kbEntitiesSection' }, 'relations': { nav: 1, section: 'kbRelationsSection' }, 'glossary': { nav: 2, section: 'kbGlossarySection' }, 'transcripts': { nav: 3, section: 'kbTranscriptsSection' } }; const mapping = tabMap[tabName]; if (mapping) { document.querySelectorAll('.kb-nav-item')[mapping.nav].classList.add('active'); document.getElementById(mapping.section).classList.add('active'); } }; async function loadKnowledgeBase() { if (!currentProject) return; try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/knowledge-base`); if (!res.ok) throw new Error('Failed to load knowledge base'); const data = await res.json(); // 更新统计 document.getElementById('kbEntityCount').textContent = data.stats.entity_count; document.getElementById('kbRelationCount').textContent = data.stats.relation_count; document.getElementById('kbTranscriptCount').textContent = data.stats.transcript_count; document.getElementById('kbGlossaryCount').textContent = data.stats.glossary_count; // 渲染实体网格 const entityGrid = document.getElementById('kbEntityGrid'); entityGrid.innerHTML = ''; data.entities.forEach(ent => { const card = document.createElement('div'); card.className = 'kb-entity-card'; card.onclick = () => { switchView('workbench'); setTimeout(() => selectEntity(ent.id), 100); }; // 渲染属性预览 let attrsHtml = ''; if (ent.attributes && ent.attributes.length > 0) { attrsHtml = `
${ent.attributes.slice(0, 3).map(a => ` ${a.name}: ${Array.isArray(a.value) ? a.value.join(', ') : a.value} `).join('')} ${ent.attributes.length > 3 ? `+${ent.attributes.length - 3}` : ''}
`; } card.innerHTML = `
${ent.type} ${ent.name}
${ent.definition || '暂无定义'}
📍 ${ent.mention_count || 0} 次提及 · ${ent.appears_in?.length || 0} 个文件
${attrsHtml} `; entityGrid.appendChild(card); }); // 渲染关系列表 const relationsList = document.getElementById('kbRelationsList'); relationsList.innerHTML = ''; data.relations.forEach(rel => { const item = document.createElement('div'); item.className = 'kb-glossary-item'; item.innerHTML = `
${rel.source_name} → ${rel.type} → ${rel.target_name} ${rel.evidence ? `
"${rel.evidence.substring(0, 100)}..."
` : ''}
`; relationsList.appendChild(item); }); // 渲染术语表 const glossaryList = document.getElementById('kbGlossaryList'); glossaryList.innerHTML = ''; data.glossary.forEach(term => { const item = document.createElement('div'); item.className = 'kb-glossary-item'; item.innerHTML = `
${term.term} ${term.pronunciation ? `(${term.pronunciation})` : ''} 出现 ${term.frequency} 次
`; glossaryList.appendChild(item); }); // 渲染文件列表 const transcriptsList = document.getElementById('kbTranscriptsList'); transcriptsList.innerHTML = ''; data.transcripts.forEach(t => { const item = document.createElement('div'); item.className = 'kb-transcript-item'; item.innerHTML = `
${t.type === 'audio' ? '🎵' : '📄'} ${t.filename}
${new Date(t.created_at).toLocaleString()}
`; transcriptsList.appendChild(item); }); } catch (err) { console.error('Load knowledge base failed:', err); } } // ==================== Glossary Functions ==================== window.showAddTermModal = function() { document.getElementById('glossaryModal').classList.add('show'); }; window.hideGlossaryModal = function() { document.getElementById('glossaryModal').classList.remove('show'); }; window.saveGlossaryTerm = async function() { const term = document.getElementById('glossaryTerm').value.trim(); const pronunciation = document.getElementById('glossaryPronunciation').value.trim(); if (!term) return; try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/glossary`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ term, pronunciation }) }); if (res.ok) { hideGlossaryModal(); document.getElementById('glossaryTerm').value = ''; document.getElementById('glossaryPronunciation').value = ''; loadKnowledgeBase(); } } catch (err) { console.error('Save glossary term failed:', err); } }; window.deleteGlossaryTerm = async function(termId) { if (!confirm('确定要删除这个术语吗?')) return; try { const res = await fetch(`${API_BASE}/glossary/${termId}`, { method: 'DELETE' }); if (res.ok) { loadKnowledgeBase(); } } catch (err) { console.error('Delete glossary term failed:', err); } }; // ==================== Phase 5: Knowledge Reasoning ==================== window.submitReasoningQuery = async function() { const input = document.getElementById('reasoningInput'); const depth = document.getElementById('reasoningDepth').value; const query = input.value.trim(); if (!query) return; const resultsDiv = document.getElementById('reasoningResults'); // 显示加载状态 resultsDiv.innerHTML = `

正在进行知识推理...

`; try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/reasoning/query`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query, reasoning_depth: depth }) }); if (!res.ok) throw new Error('Reasoning failed'); const data = await res.json(); renderReasoningResult(data); } catch (err) { console.error('Reasoning query failed:', err); resultsDiv.innerHTML = `

推理失败,请稍后重试

`; } }; function renderReasoningResult(data) { const resultsDiv = document.getElementById('reasoningResults'); const typeLabels = { 'causal': '🔍 因果推理', 'comparative': '⚖️ 对比推理', 'temporal': '⏱️ 时序推理', 'associative': '🔗 关联推理', 'summary': '📝 总结推理' }; const typeLabel = typeLabels[data.reasoning_type] || '🤔 智能分析'; const confidencePercent = Math.round(data.confidence * 100); let evidenceHtml = ''; if (data.evidence && data.evidence.length > 0) { evidenceHtml = `

📋 支撑证据

${data.evidence.map(e => `
${e.text || e}
`).join('')}
`; } let gapsHtml = ''; if (data.knowledge_gaps && data.knowledge_gaps.length > 0) { gapsHtml = `

⚠️ 知识缺口

`; } resultsDiv.innerHTML = `
${typeLabel}
置信度: ${confidencePercent}%
${data.answer.replace(/\n/g, '
')}
${evidenceHtml} ${gapsHtml}
`; } window.clearReasoningResult = function() { document.getElementById('reasoningResults').innerHTML = ''; document.getElementById('reasoningInput').value = ''; document.getElementById('inferencePathsSection').style.display = 'none'; }; window.generateSummary = async function(summaryType) { const resultsDiv = document.getElementById('reasoningResults'); // 显示加载状态 resultsDiv.innerHTML = `

正在生成项目总结...

`; try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/reasoning/summary`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ summary_type: summaryType }) }); if (!res.ok) throw new Error('Summary failed'); const data = await res.json(); renderSummaryResult(data); } catch (err) { console.error('Summary generation failed:', err); resultsDiv.innerHTML = `

总结生成失败,请稍后重试

`; } }; function renderSummaryResult(data) { const resultsDiv = document.getElementById('reasoningResults'); const typeLabels = { 'comprehensive': '📋 全面总结', 'executive': '💼 高管摘要', 'technical': '⚙️ 技术总结', 'risk': '⚠️ 风险分析' }; const typeLabel = typeLabels[data.summary_type] || '📝 项目总结'; let keyPointsHtml = ''; if (data.key_points && data.key_points.length > 0) { keyPointsHtml = `

📌 关键要点

`; } let risksHtml = ''; if (data.risks && data.risks.length > 0) { risksHtml = `

⚠️ 风险与问题

`; } let recommendationsHtml = ''; if (data.recommendations && data.recommendations.length > 0) { recommendationsHtml = `

💡 建议

${data.recommendations.map(r => `
${r}
`).join('')}
`; } resultsDiv.innerHTML = `
${typeLabel}
置信度: ${Math.round(data.confidence * 100)}%
${data.overview ? data.overview.replace(/\n/g, '
') : ''}
${keyPointsHtml} ${risksHtml} ${recommendationsHtml}
`; } window.findInferencePath = async function(startEntity, endEntity) { const pathsSection = document.getElementById('inferencePathsSection'); const pathsList = document.getElementById('inferencePathsList'); pathsSection.style.display = 'block'; pathsList.innerHTML = '

正在搜索关联路径...

'; try { const res = await fetch( `${API_BASE}/projects/${currentProject.id}/reasoning/inference-path?start_entity=${encodeURIComponent(startEntity)}&end_entity=${encodeURIComponent(endEntity)}` ); if (!res.ok) throw new Error('Path finding failed'); const data = await res.json(); renderInferencePaths(data); } catch (err) { console.error('Path finding failed:', err); pathsList.innerHTML = '

路径搜索失败

'; } }; // Phase 5: Entity Attributes Management let currentEntityIdForAttributes = null; let currentAttributes = []; let currentTemplates = []; // Show entity attributes modal window.showEntityAttributes = async function(entityId) { if (entityId) { currentEntityIdForAttributes = entityId; } else if (selectedEntity) { currentEntityIdForAttributes = selectedEntity; } else { alert('请先选择一个实体'); return; } const modal = document.getElementById('attributesModal'); modal.classList.add('show'); // Reset form document.getElementById('attributesAddForm').style.display = 'none'; document.getElementById('toggleAddAttrBtn').style.display = 'inline-block'; document.getElementById('saveAttrBtn').style.display = 'none'; await loadEntityAttributes(); }; window.hideAttributesModal = function() { document.getElementById('attributesModal').classList.remove('show'); currentEntityIdForAttributes = null; }; async function loadEntityAttributes() { if (!currentEntityIdForAttributes) return; try { const res = await fetch(`${API_BASE}/entities/${currentEntityIdForAttributes}/attributes`); if (!res.ok) throw new Error('Failed to load attributes'); const data = await res.json(); currentAttributes = data.attributes || []; renderAttributesList(); } catch (err) { console.error('Load attributes failed:', err); document.getElementById('attributesList').innerHTML = '

加载失败

'; } } function renderAttributesList() { const container = document.getElementById('attributesList'); if (currentAttributes.length === 0) { container.innerHTML = '

暂无属性,点击"添加属性"创建

'; return; } container.innerHTML = currentAttributes.map(attr => { let valueDisplay = attr.value; if (attr.type === 'multiselect' && Array.isArray(attr.value)) { valueDisplay = attr.value.join(', '); } return `
${attr.name} ${attr.type}
${valueDisplay || '-'}
`; }).join(''); } window.toggleAddAttributeForm = function() { const form = document.getElementById('attributesAddForm'); const toggleBtn = document.getElementById('toggleAddAttrBtn'); const saveBtn = document.getElementById('saveAttrBtn'); if (form.style.display === 'none') { form.style.display = 'block'; toggleBtn.style.display = 'none'; saveBtn.style.display = 'inline-block'; } else { form.style.display = 'none'; toggleBtn.style.display = 'inline-block'; saveBtn.style.display = 'none'; } }; window.onAttrTypeChange = function() { const type = document.getElementById('attrType').value; const optionsGroup = document.getElementById('attrOptionsGroup'); const valueContainer = document.getElementById('attrValueContainer'); if (type === 'select' || type === 'multiselect') { optionsGroup.style.display = 'block'; } else { optionsGroup.style.display = 'none'; } // Update value input based on type if (type === 'date') { valueContainer.innerHTML = ''; } else if (type === 'number') { valueContainer.innerHTML = ''; } else { valueContainer.innerHTML = ''; } }; window.saveAttribute = async function() { if (!currentEntityIdForAttributes) return; const name = document.getElementById('attrName').value.trim(); const type = document.getElementById('attrType').value; let value = document.getElementById('attrValue').value; const changeReason = document.getElementById('attrChangeReason').value.trim(); if (!name) { alert('请输入属性名称'); return; } // Handle options for select/multiselect let options = null; if (type === 'select' || type === 'multiselect') { const optionsStr = document.getElementById('attrOptions').value.trim(); if (optionsStr) { options = optionsStr.split(',').map(o => o.trim()).filter(o => o); } // Handle multiselect value if (type === 'multiselect' && value) { value = value.split(',').map(v => v.trim()).filter(v => v); } } // Handle number type if (type === 'number' && value) { value = parseFloat(value); } try { const res = await fetch(`${API_BASE}/entities/${currentEntityIdForAttributes}/attributes`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, type, value, options, change_reason: changeReason }) }); if (!res.ok) throw new Error('Failed to save attribute'); // Reset form document.getElementById('attrName').value = ''; document.getElementById('attrValue').value = ''; document.getElementById('attrOptions').value = ''; document.getElementById('attrChangeReason').value = ''; // Reload attributes await loadEntityAttributes(); // Hide form toggleAddAttributeForm(); } catch (err) { console.error('Save attribute failed:', err); alert('保存失败,请重试'); } }; window.deleteAttribute = async function(attributeId) { if (!confirm('确定要删除这个属性吗?')) return; try { const res = await fetch(`${API_BASE}/entities/${currentEntityIdForAttributes}/attributes/${attributeId}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Failed to delete attribute'); await loadEntityAttributes(); } catch (err) { console.error('Delete attribute failed:', err); alert('删除失败'); } }; // Attribute History window.showAttributeHistory = async function(attributeName) { if (!currentEntityIdForAttributes) return; const modal = document.getElementById('attrHistoryModal'); modal.classList.add('show'); try { const res = await fetch(`${API_BASE}/entities/${currentEntityIdForAttributes}/attributes/history?attribute_name=${encodeURIComponent(attributeName)}`); if (!res.ok) throw new Error('Failed to load history'); const data = await res.json(); renderAttributeHistory(data.history, attributeName); } catch (err) { console.error('Load history failed:', err); document.getElementById('attrHistoryContent').innerHTML = '

加载失败

'; } }; window.hideAttrHistoryModal = function() { document.getElementById('attrHistoryModal').classList.remove('show'); }; function renderAttributeHistory(history, attributeName) { const container = document.getElementById('attrHistoryContent'); if (history.length === 0) { container.innerHTML = `

属性 "${attributeName}" 暂无变更历史

`; return; } container.innerHTML = history.map(h => { const date = new Date(h.changed_at).toLocaleString(); return `
${h.changed_by || '系统'} ${date}
${h.old_value || '(无)'} ${h.new_value || '(无)'}
${h.change_reason ? `
原因: ${h.change_reason}
` : ''}
`; }).join(''); } // Attribute Templates Management window.showAttributeTemplates = async function() { const modal = document.getElementById('attrTemplatesModal'); modal.classList.add('show'); document.getElementById('templateForm').style.display = 'none'; document.getElementById('toggleTemplateBtn').style.display = 'inline-block'; document.getElementById('saveTemplateBtn').style.display = 'none'; await loadAttributeTemplates(); }; window.hideAttrTemplatesModal = function() { document.getElementById('attrTemplatesModal').classList.remove('show'); }; async function loadAttributeTemplates() { if (!currentProject) return; try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/attribute-templates`); if (!res.ok) throw new Error('Failed to load templates'); currentTemplates = await res.json(); renderTemplatesList(); } catch (err) { console.error('Load templates failed:', err); document.getElementById('templatesList').innerHTML = '

加载失败

'; } } function renderTemplatesList() { const container = document.getElementById('templatesList'); if (currentTemplates.length === 0) { container.innerHTML = '

暂无模板,点击"新建模板"创建

'; return; } container.innerHTML = currentTemplates.map(t => { const optionsStr = t.options ? `选项: ${t.options.join(', ')}` : ''; return `
${t.name} ${t.type} ${t.is_required ? '*' : ''}
${t.description || ''} ${optionsStr}
`; }).join(''); } window.toggleTemplateForm = function() { const form = document.getElementById('templateForm'); const toggleBtn = document.getElementById('toggleTemplateBtn'); const saveBtn = document.getElementById('saveTemplateBtn'); if (form.style.display === 'none') { form.style.display = 'block'; toggleBtn.style.display = 'none'; saveBtn.style.display = 'inline-block'; } else { form.style.display = 'none'; toggleBtn.style.display = 'inline-block'; saveBtn.style.display = 'none'; } }; window.onTemplateTypeChange = function() { const type = document.getElementById('templateType').value; const optionsGroup = document.getElementById('templateOptionsGroup'); if (type === 'select' || type === 'multiselect') { optionsGroup.style.display = 'block'; } else { optionsGroup.style.display = 'none'; } }; window.saveTemplate = async function() { if (!currentProject) return; const name = document.getElementById('templateName').value.trim(); const type = document.getElementById('templateType').value; const description = document.getElementById('templateDesc').value.trim(); const isRequired = document.getElementById('templateRequired').checked; const defaultValue = document.getElementById('templateDefault').value.trim(); if (!name) { alert('请输入模板名称'); return; } let options = null; if (type === 'select' || type === 'multiselect') { const optionsStr = document.getElementById('templateOptions').value.trim(); if (optionsStr) { options = optionsStr.split(',').map(o => o.trim()).filter(o => o); } } try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/attribute-templates`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, type, description, options, is_required: isRequired, default_value: defaultValue || null }) }); if (!res.ok) throw new Error('Failed to save template'); // Reset form document.getElementById('templateName').value = ''; document.getElementById('templateDesc').value = ''; document.getElementById('templateOptions').value = ''; document.getElementById('templateDefault').value = ''; document.getElementById('templateRequired').checked = false; await loadAttributeTemplates(); toggleTemplateForm(); } catch (err) { console.error('Save template failed:', err); alert('保存失败'); } }; window.deleteTemplate = async function(templateId) { if (!confirm('确定要删除这个模板吗?')) return; try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/attribute-templates/${templateId}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Failed to delete template'); await loadAttributeTemplates(); } catch (err) { console.error('Delete template failed:', err); alert('删除失败'); } }; // Search entities by attributes window.searchByAttributes = async function() { if (!currentProject) return; const filterName = document.getElementById('attrFilterName').value; const filterValue = document.getElementById('attrFilterValue').value; const filterOp = document.getElementById('attrFilterOp').value; if (!filterName || !filterValue) { alert('请输入筛选条件'); return; } try { const filters = JSON.stringify([{ name: filterName, value: filterValue, operator: filterOp }]); const res = await fetch(`${API_BASE}/projects/${currentProject.id}/entities/search-by-attributes?filters=${encodeURIComponent(filters)}`); if (!res.ok) throw new Error('Search failed'); const entities = await res.json(); // Update entity grid const grid = document.getElementById('kbEntityGrid'); if (entities.length === 0) { grid.innerHTML = '

未找到匹配的实体

'; return; } grid.innerHTML = entities.map(ent => `
${ent.type} ${ent.name}
${ent.definition || '暂无定义'}
`).join(''); } catch (err) { console.error('Search by attributes failed:', err); alert('搜索失败'); } }; // ==================== Export Functions ==================== // Show export panel window.showExportPanel = function() { const modal = document.getElementById('exportPanelModal'); if (modal) { modal.style.display = 'flex'; // Show transcript export section if a transcript is selected const transcriptSection = document.getElementById('transcriptExportSection'); if (transcriptSection && currentData && currentData.transcript_id !== 'project_view') { transcriptSection.style.display = 'block'; } else if (transcriptSection) { transcriptSection.style.display = 'none'; } } }; // Hide export panel window.hideExportPanel = function() { const modal = document.getElementById('exportPanelModal'); if (modal) { modal.style.display = 'none'; } }; // Helper function to download file function downloadFile(url, filename) { const link = document.createElement('a'); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); } // Export knowledge graph as SVG window.exportGraph = async function(format) { if (!currentProject) return; try { const endpoint = format === 'svg' ? 'graph-svg' : 'graph-png'; const mimeType = format === 'svg' ? 'image/svg+xml' : 'image/png'; const ext = format === 'svg' ? 'svg' : 'png'; const res = await fetch(`${API_BASE}/projects/${currentProject.id}/export/${endpoint}`); if (!res.ok) throw new Error(`Export ${format} failed`); const blob = await res.blob(); const url = URL.createObjectURL(blob); downloadFile(url, `insightflow-graph-${currentProject.id}.${ext}`); URL.revokeObjectURL(url); showNotification(`图谱已导出为 ${format.toUpperCase()}`, 'success'); } catch (err) { console.error(`Export ${format} failed:`, err); alert(`导出失败: ${err.message}`); } }; // Export entities window.exportEntities = async function(format) { if (!currentProject) return; try { const endpoint = format === 'excel' ? 'entities-excel' : 'entities-csv'; const mimeType = format === 'excel' ? 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' : 'text/csv'; const ext = format === 'excel' ? 'xlsx' : 'csv'; const res = await fetch(`${API_BASE}/projects/${currentProject.id}/export/${endpoint}`); if (!res.ok) throw new Error(`Export ${format} failed`); const blob = await res.blob(); const url = URL.createObjectURL(blob); downloadFile(url, `insightflow-entities-${currentProject.id}.${ext}`); URL.revokeObjectURL(url); showNotification(`实体数据已导出为 ${format.toUpperCase()}`, 'success'); } catch (err) { console.error(`Export ${format} failed:`, err); alert(`导出失败: ${err.message}`); } }; // Export relations window.exportRelations = async function(format) { if (!currentProject) return; try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/export/relations-csv`); if (!res.ok) throw new Error('Export relations failed'); const blob = await res.blob(); const url = URL.createObjectURL(blob); downloadFile(url, `insightflow-relations-${currentProject.id}.csv`); URL.revokeObjectURL(url); showNotification('关系数据已导出为 CSV', 'success'); } catch (err) { console.error('Export relations failed:', err); alert(`导出失败: ${err.message}`); } }; // Export project report as PDF window.exportReport = async function(format) { if (!currentProject) return; try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/export/report-pdf`); if (!res.ok) throw new Error('Export PDF failed'); const blob = await res.blob(); const url = URL.createObjectURL(blob); downloadFile(url, `insightflow-report-${currentProject.id}.pdf`); URL.revokeObjectURL(url); showNotification('项目报告已导出为 PDF', 'success'); } catch (err) { console.error('Export PDF failed:', err); alert(`导出失败: ${err.message}`); } }; // Export project as JSON window.exportProject = async function(format) { if (!currentProject) return; try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/export/project-json`); if (!res.ok) throw new Error('Export JSON failed'); const blob = await res.blob(); const url = URL.createObjectURL(blob); downloadFile(url, `insightflow-project-${currentProject.id}.json`); URL.revokeObjectURL(url); showNotification('项目数据已导出为 JSON', 'success'); } catch (err) { console.error('Export JSON failed:', err); alert(`导出失败: ${err.message}`); } }; // Export transcript as Markdown window.exportTranscript = async function(format) { if (!currentProject || !currentData || currentData.transcript_id === 'project_view') { alert('请先选择一个转录文件'); return; } try { const res = await fetch(`${API_BASE}/transcripts/${currentData.transcript_id}/export/markdown`); if (!res.ok) throw new Error('Export Markdown failed'); const blob = await res.blob(); const url = URL.createObjectURL(blob); downloadFile(url, `insightflow-transcript-${currentData.transcript_id}.md`); URL.revokeObjectURL(url); showNotification('转录文本已导出为 Markdown', 'success'); } catch (err) { console.error('Export Markdown failed:', err); alert(`导出失败: ${err.message}`); } }; // Show notification function showNotification(message, type = 'info') { // Create notification element const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; top: 20px; right: 20px; background: ${type === 'success' ? 'rgba(0, 212, 255, 0.9)' : '#333'}; color: ${type === 'success' ? '#000' : '#fff'}; padding: 12px 20px; border-radius: 8px; z-index: 10000; font-size: 0.9rem; animation: slideIn 0.3s ease; `; notification.textContent = message; document.body.appendChild(notification); // Remove after 3 seconds setTimeout(() => { notification.style.animation = 'slideOut 0.3s ease'; setTimeout(() => { document.body.removeChild(notification); }, 300); }, 3000); } // Add animation styles const style = document.createElement('style'); style.textContent = ` @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } } `; document.head.appendChild(style); // ==================== Graph Analysis Functions ==================== // Load graph statistics window.loadGraphStats = async function() { if (!currentProject) return; try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/graph/stats`); if (!res.ok) throw new Error('Failed to load graph stats'); const stats = await res.json(); document.getElementById('statNodeCount').textContent = stats.node_count || 0; document.getElementById('statEdgeCount').textContent = stats.edge_count || 0; document.getElementById('statDensity').textContent = (stats.density || 0).toFixed(3); document.getElementById('statComponents').textContent = stats.connected_components || 0; } catch (err) { console.error('Load graph stats failed:', err); } }; // Sync project to Neo4j window.syncToNeo4j = async function() { if (!currentProject) return; try { showNotification('正在同步到 Neo4j...', 'info'); const res = await fetch(`${API_BASE}/neo4j/sync`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project_id: currentProject.id }) }); if (!res.ok) throw new Error('Sync failed'); const result = await res.json(); showNotification(`同步完成: ${result.nodes_synced} 节点, ${result.relationships_synced} 关系`, 'success'); // Refresh stats after sync await loadGraphStats(); } catch (err) { console.error('Sync to Neo4j failed:', err); alert('同步失败: ' + err.message); } }; // Load centrality analysis window.loadCentralityAnalysis = async function() { if (!currentProject) return; const metric = document.getElementById('centralityMetric')?.value || 'degree'; try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/graph/centrality?metric=${metric}`); if (!res.ok) throw new Error('Failed to load centrality analysis'); const data = await res.json(); const list = document.getElementById('centralityList'); if (!data.rankings || data.rankings.length === 0) { list.innerHTML = '

暂无数据

'; return; } list.innerHTML = data.rankings.slice(0, 10).map((r, i) => `
${i + 1}
${r.entity_name}
得分: ${r.score.toFixed(3)}
`).join(''); } catch (err) { console.error('Load centrality analysis failed:', err); document.getElementById('centralityList').innerHTML = '

加载失败

'; } }; // Load communities window.loadCommunities = async function() { if (!currentProject) return; try { const res = await fetch(`${API_BASE}/projects/${currentProject.id}/graph/communities`); if (!res.ok) throw new Error('Failed to load communities'); const data = await res.json(); const list = document.getElementById('communitiesList'); if (!data.communities || data.communities.length === 0) { list.innerHTML = '

未发现社区

'; return; } list.innerHTML = data.communities.map((c, i) => `
社区 ${i + 1} ${c.size} 节点
密度: ${(c.density || 0).toFixed(3)}
${c.nodes.slice(0, 5).map(n => `${n.name}`).join('')} ${c.nodes.length > 5 ? `+${c.nodes.length - 5}` : ''}
`).join(''); } catch (err) { console.error('Load communities failed:', err); document.getElementById('communitiesList').innerHTML = '

加载失败

'; } }; // Find shortest path window.findShortestPath = async function() { if (!currentProject) return; const startId = document.getElementById('pathStartEntity')?.value; const endId = document.getElementById('pathEndEntity')?.value; if (!startId || !endId) { alert('请选择起点和终点实体'); return; } if (startId === endId) { alert('起点和终点不能相同'); return; } try { const res = await fetch(`${API_BASE}/graph/shortest-path`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ entity_id_1: startId, entity_id_2: endId, max_depth: 10 }) }); if (!res.ok) throw new Error('Failed to find path'); const data = await res.json(); const resultsDiv = document.getElementById('graphAnalysisResults'); if (!data.path || data.path.length === 0) { resultsDiv.innerHTML = '
未找到路径
'; resultsDiv.classList.add('show'); return; } // Build path display const pathHtml = data.path.map((node, i) => { const arrow = i < data.path.length - 1 ? '' : ''; return `${node.name}${arrow}`; }).join(''); resultsDiv.innerHTML = `
最短路径 (${data.path.length} 个节点, 长度: ${data.path_length})
${pathHtml}
`; resultsDiv.classList.add('show'); // Visualize path in graph visualizePath(data.path); } catch (err) { console.error('Find shortest path failed:', err); alert('查找路径失败: ' + err.message); } }; // Find neighbors window.findNeighbors = async function() { if (!currentProject) return; const entityId = document.getElementById('neighborEntity')?.value; if (!entityId) { alert('请选择实体'); return; } try { const res = await fetch(`${API_BASE}/entities/${entityId}/neighbors?limit=50`); if (!res.ok) throw new Error('Failed to find neighbors'); const data = await res.json(); const resultsDiv = document.getElementById('graphAnalysisResults'); if (!data.neighbors || data.neighbors.length === 0) { resultsDiv.innerHTML = '
未找到邻居节点
'; resultsDiv.classList.add('show'); return; } const entityName = projectEntities.find(e => e.id === entityId)?.name || entityId; resultsDiv.innerHTML = `
${entityName} 的邻居节点 (${data.neighbors.length})
${data.neighbors.map(n => `${n.entity_name} (${n.relation_type})`).join('')}
`; resultsDiv.classList.add('show'); } catch (err) { console.error('Find neighbors failed:', err); alert('查找邻居失败: ' + err.message); } }; // Populate entity selects for graph analysis function populateGraphEntitySelects() { const selects = [ document.getElementById('pathStartEntity'), document.getElementById('pathEndEntity'), document.getElementById('neighborEntity') ]; selects.forEach(select => { if (!select) return; const currentValue = select.value; select.innerHTML = ''; projectEntities.forEach(ent => { const option = document.createElement('option'); option.value = ent.id; option.textContent = ent.name; select.appendChild(option); }); if (currentValue) { select.value = currentValue; } }); } // Visualize path in graph function visualizePath(path) { const svg = d3.select('#graphAnalysisSvg'); svg.selectAll('*').remove(); if (!path || path.length === 0) return; const width = svg.node().parentElement.clientWidth; const height = svg.node().parentElement.clientHeight || 400; svg.attr('width', width).attr('height', height); // Create nodes and links const nodes = path.map((n, i) => ({ id: n.id, name: n.name, type: n.type || 'OTHER', index: i })); 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 colorScale = { 'PROJECT': '#7b2cbf', 'TECH': '#00d4ff', 'PERSON': '#ff6b6b', 'ORG': '#4ecdc4', 'OTHER': '#666' }; // Create 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)); // Draw links const link = svg.append('g') .selectAll('line') .data(links) .enter().append('line') .attr('stroke', '#00d4ff') .attr('stroke-width', 2) .attr('marker-end', 'url(#arrow)'); // Add arrow marker svg.append('defs').append('marker') .attr('id', 'arrow') .attr('viewBox', '0 -5 10 10') .attr('refX', 25) .attr('refY', 0) .attr('markerWidth', 6) .attr('markerHeight', 6) .attr('orient', 'auto') .append('path') .attr('d', 'M0,-5L10,0L0,5') .attr('fill', '#00d4ff'); // Draw nodes const node = svg.append('g') .selectAll('g') .data(nodes) .enter().append('g') .call(d3.drag() .on('start', dragstarted) .on('drag', dragged) .on('end', dragended)); node.append('circle') .attr('r', 20) .attr('fill', d => colorScale[d.type] || '#666') .attr('stroke', '#fff') .attr('stroke-width', 2); node.append('text') .attr('dy', 35) .attr('text-anchor', 'middle') .attr('fill', '#e0e0e0') .attr('font-size', '12px') .text(d => d.name); // 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(event, d) { if (!event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(event, d) { d.fx = event.x; d.fy = event.y; } function dragended(event, d) { if (!event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; } } // Reset graph visualization window.resetGraphViz = function() { const svg = d3.select('#graphAnalysisSvg'); svg.selectAll('*').remove(); document.getElementById('graphAnalysisResults').classList.remove('show'); }; // Highlight entity in graph function highlightEntityInGraph(entityId) { // This would highlight the entity in the main graph view // For now, just switch to workbench and select the entity switchView('workbench'); setTimeout(() => selectEntity(entityId), 100); } // Initialize graph analysis view function initGraphAnalysis() { populateGraphEntitySelects(); loadGraphStats(); } // ==================== Phase 5: Graph Analysis ==================== function initGraphAnalysis() { if (!currentProject) return; // 填充实体选择器 populateEntitySelectors(); // 加载图统计 loadGraphStats(); // 检查 Neo4j 状态 checkNeo4jStatus(); } function populateEntitySelectors() { const selectors = [ document.getElementById('pathStartEntity'), document.getElementById('pathEndEntity'), document.getElementById('neighborEntity') ]; selectors.forEach(selector => { if (!selector) return; const currentValue = selector.value; selector.innerHTML = ''; projectEntities.forEach(ent => { const option = document.createElement('option'); option.value = ent.id; option.textContent = `${ent.name} (${ent.type})`; selector.appendChild(option); }); selector.value = currentValue; }); } async function checkNeo4jStatus() { try { const res = await fetch(`${API_BASE}/neo4j/status`); if (res.ok) { const data = await res.json(); updateNeo4jStatusUI(data.connected); } } catch (err) { console.error('Check Neo4j status failed:', err); updateNeo4jStatusUI(false); } } function updateNeo4jStatusUI(connected) { // 可以在头部添加状态指示器 const header = document.querySelector('.graph-analysis-header'); let statusEl = document.getElementById('neo4jStatus'); if (!statusEl) { statusEl = document.createElement('div'); statusEl.id = 'neo4jStatus'; statusEl.className = 'neo4j-status'; header.appendChild(statusEl); } statusEl.className = `neo4j-status ${connected ? 'connected' : 'disconnected'}`; statusEl.innerHTML = ` Neo4j ${connected ? '已连接' : '未连接'} `; } async function syncToNeo4j() { if (!currentProject) return; const btn = event.target; const originalText = btn.textContent; btn.textContent = '🔄 同步中...'; btn.disabled = true; try { const res = await fetch(`${API_BASE}/neo4j/sync`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project_id: currentProject.id }) }); if (!res.ok) throw new Error('Sync failed'); const data = await res.json(); showNotification(`同步成功!${data.nodes_synced} 个节点, ${data.edges_synced} 条边`, 'success'); // 刷新统计 await loadGraphStats(); checkNeo4jStatus(); } catch (err) { console.error('Sync to Neo4j failed:', err); showNotification('同步失败,请检查 Neo4j 连接', 'error'); } finally { btn.textContent = originalText; btn.disabled = false; } } async function loadGraphStats() { if (!currentProject) return; try { // 加载图统计 const statsRes = await fetch(`${API_BASE}/projects/${currentProject.id}/graph/stats`); if (statsRes.ok) { graphStats = await statsRes.json(); renderGraphStats(graphStats); } // 加载中心性分析 const centralityRes = await fetch(`${API_BASE}/projects/${currentProject.id}/graph/centrality`); if (centralityRes.ok) { centralityData = await centralityRes.json(); renderCentrality(centralityData); } // 加载社区发现 const communitiesRes = await fetch(`${API_BASE}/projects/${currentProject.id}/graph/communities`); if (communitiesRes.ok) { communitiesData = await communitiesRes.json(); renderCommunities(communitiesData); } } catch (err) { console.error('Load graph stats failed:', err); } } function renderGraphStats(stats) { document.getElementById('statNodeCount').textContent = stats.node_count || 0; document.getElementById('statEdgeCount').textContent = stats.edge_count || 0; document.getElementById('statDensity').textContent = (stats.density || 0).toFixed(3); document.getElementById('statComponents').textContent = stats.component_count || 0; } function renderCentrality(data) { const container = document.getElementById('centralityList'); if (!data.centrality || data.centrality.length === 0) { container.innerHTML = '

暂无中心性数据

'; return; } // 按度中心性排序 const sorted = [...data.centrality].sort((a, b) => b.degree - a.degree); container.innerHTML = sorted.map((item, index) => { const rank = index + 1; const isTop3 = rank <= 3; const entity = projectEntities.find(e => e.id === item.entity_id); return `
${rank}
${item.entity_name}
${item.entity_type}${entity ? ` · ${entity.definition?.substring(0, 30) || ''}` : ''}
${item.degree}
连接数
`; }).join(''); } function renderCommunities(data) { const svg = d3.select('#communitiesSvg'); svg.selectAll('*').remove(); const container = document.getElementById('communitiesList'); if (!data.communities || data.communities.length === 0) { container.innerHTML = '

暂无社区数据

'; return; } // 渲染社区列表 container.innerHTML = data.communities.map((community, idx) => { const nodeNames = community.node_names || []; return `
社区 ${idx + 1} ${community.size} 个节点
${nodeNames.slice(0, 8).map(name => ` ${name} `).join('')} ${nodeNames.length > 8 ? `+${nodeNames.length - 8}` : ''}
`; }).join(''); // 渲染社区可视化 renderCommunitiesViz(data.communities); } function renderCommunitiesViz(communities) { const svg = d3.select('#communitiesSvg'); const container = svg.node().parentElement; const width = container.clientWidth; const height = container.clientHeight || 400; svg.attr('width', width).attr('height', height); // 颜色方案 const colors = [ '#00d4ff', '#7b2cbf', '#ff6b6b', '#4ecdc4', '#ffe66d', '#a8e6cf', '#ff8b94', '#c7ceea' ]; // 准备节点数据 let allNodes = []; communities.forEach((comm, idx) => { const nodes = (comm.node_names || []).map((name, i) => ({ id: `${idx}-${i}`, name: name, community: idx, color: colors[idx % colors.length] })); allNodes = allNodes.concat(nodes); }); if (allNodes.length === 0) return; // 使用力导向布局 const simulation = d3.forceSimulation(allNodes) .force('charge', d3.forceManyBody().strength(-100)) .force('center', d3.forceCenter(width / 2, height / 2)) .force('collision', d3.forceCollide().radius(30)) .force('x', d3.forceX(width / 2).strength(0.05)) .force('y', d3.forceY(height / 2).strength(0.05)); // 绘制节点 const node = svg.selectAll('.community-node') .data(allNodes) .enter().append('g') .attr('class', 'community-node') .call(d3.drag() .on('start', dragstarted) .on('drag', dragged) .on('end', dragended)); node.append('circle') .attr('r', 20) .attr('fill', d => d.color) .attr('stroke', '#fff') .attr('stroke-width', 2) .attr('opacity', 0.8); node.append('text') .text(d => d.name.length > 4 ? d.name.slice(0, 3) + '...' : d.name) .attr('text-anchor', 'middle') .attr('dy', 5) .attr('fill', '#fff') .attr('font-size', '10px') .style('pointer-events', 'none'); simulation.on('tick', () => { node.attr('transform', d => `translate(${d.x},${d.y})`); }); function dragstarted(event, d) { if (!event.active) simulation.alphaTarget(0.3).restart(); d.fx = d.x; d.fy = d.y; } function dragged(event, d) { d.fx = event.x; d.fy = event.y; } function dragended(event, d) { if (!event.active) simulation.alphaTarget(0); d.fx = null; d.fy = null; } } window.switchGraphTab = function(tabName) { // 更新标签状态 document.querySelectorAll('.graph-analysis-tab').forEach(tab => { tab.classList.remove('active'); }); event.target.classList.add('active'); // 切换面板 document.querySelectorAll('.graph-viz-panel').forEach(panel => { panel.classList.remove('active'); }); if (tabName === 'centrality') { document.getElementById('centralityPanel').classList.add('active'); } else if (tabName === 'communities') { document.getElementById('communitiesPanel').classList.add('active'); } }; async function findShortestPath() { const startId = document.getElementById('pathStartEntity').value; const endId = document.getElementById('pathEndEntity').value; if (!startId || !endId) { alert('请选择起点和终点实体'); return; } if (startId === endId) { alert('起点和终点不能相同'); return; } // 切换到路径面板 document.querySelectorAll('.graph-viz-panel').forEach(panel => { panel.classList.remove('active'); }); document.getElementById('pathPanel').classList.add('active'); // 显示加载状态 document.getElementById('pathViz').innerHTML = `
正在查找最短路径...
`; try { const res = await fetch(`${API_BASE}/graph/shortest-path`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ start_entity_id: startId, end_entity_id: endId }) }); if (!res.ok) throw new Error('Path finding failed'); const data = await res.json(); currentPathData = data; renderPath(data); } catch (err) { console.error('Find shortest path failed:', err); document.getElementById('pathViz').innerHTML = `

路径查找失败

请确保数据已同步到 Neo4j

`; } } function renderPath(data) { const startEntity = projectEntities.find(e => e.id === data.start_entity_id); const endEntity = projectEntities.find(e => e.id === data.end_entity_id); document.getElementById('pathDescription').textContent = `${startEntity?.name || '起点'} → ${endEntity?.name || '终点'} (${data.path_length} 步)`; // 渲染路径可视化 const svg = d3.select('#pathSvg'); svg.selectAll('*').remove(); const container = svg.node().parentElement; const width = container.clientWidth; const height = container.clientHeight || 300; svg.attr('width', width).attr('height', height); if (!data.path || data.path.length === 0) { document.getElementById('pathViz').innerHTML = `
🔍

未找到路径

`; document.getElementById('pathInfo').innerHTML = ''; return; } // 准备节点和边 const nodes = data.path.map((nodeId, idx) => ({ id: nodeId, name: projectEntities.find(e => e.id === nodeId)?.name || nodeId, x: (width / (data.path.length + 1)) * (idx + 1), y: height / 2 })); const links = []; for (let i = 0; i < nodes.length - 1; i++) { links.push({ source: nodes[i], target: nodes[i + 1] }); } // 绘制连线 svg.selectAll('.path-link') .data(links) .enter().append('line') .attr('class', 'path-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) .attr('stroke', '#00d4ff') .attr('stroke-width', 3) .attr('stroke-opacity', 0.6); // 绘制箭头 links.forEach((link, i) => { const angle = Math.atan2(link.target.y - link.source.y, link.target.x - link.source.x); const arrowSize = 10; const arrowX = link.target.x - 25 * Math.cos(angle); const arrowY = link.target.y - 25 * Math.sin(angle); svg.append('polygon') .attr('points', `0,-${arrowSize/2} ${arrowSize},0 0,${arrowSize/2}`) .attr('transform', `translate(${arrowX},${arrowY}) rotate(${angle * 180 / Math.PI})`) .attr('fill', '#00d4ff'); }); // 绘制节点 const node = svg.selectAll('.path-node') .data(nodes) .enter().append('g') .attr('class', 'path-node') .attr('transform', d => `translate(${d.x},${d.y})`); node.append('circle') .attr('r', d => d.id === data.start_entity_id || d.id === data.end_entity_id ? 30 : 25) .attr('fill', d => { if (d.id === data.start_entity_id) return '#00d4ff'; if (d.id === data.end_entity_id) return '#7b2cbf'; return '#333'; }) .attr('stroke', '#fff') .attr('stroke-width', 2); 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', d => (d.id === data.start_entity_id || d.id === data.end_entity_id) ? '#000' : '#fff') .attr('font-size', '11px') .style('pointer-events', 'none'); // 渲染路径信息 renderPathInfo(data); } function renderPathInfo(data) { const container = document.getElementById('pathInfo'); let html = ''; data.path.forEach((nodeId, idx) => { const entity = projectEntities.find(e => e.id === nodeId); const isStart = idx === 0; const isEnd = idx === data.path.length - 1; html += `
${idx + 1}
${entity?.name || nodeId}
${!isStart ? `
← 通过关系连接
` : ''}
${isStart ? '起点' : ''} ${isEnd ? '终点' : ''}
`; }); container.innerHTML = html; } async function findNeighbors() { const entityId = document.getElementById('neighborEntity').value; const depth = parseInt(document.getElementById('neighborDepth').value) || 1; if (!entityId) { alert('请选择实体'); return; } // 切换到路径面板显示邻居 document.querySelectorAll('.graph-viz-panel').forEach(panel => { panel.classList.remove('active'); }); document.getElementById('pathPanel').classList.add('active'); const entity = projectEntities.find(e => e.id === entityId); document.getElementById('pathDescription').textContent = `${entity?.name || '实体'} 的 ${depth} 度邻居`; // 显示加载状态 document.getElementById('pathViz').innerHTML = `
正在查找邻居节点...
`; document.getElementById('pathInfo').innerHTML = ''; try { const res = await fetch(`${API_BASE}/entities/${entityId}/neighbors?depth=${depth}`); if (!res.ok) throw new Error('Neighbors query failed'); const data = await res.json(); renderNeighbors(data, entity); } catch (err) { console.error('Find neighbors failed:', err); document.getElementById('pathViz').innerHTML = `

邻居查询失败

请确保数据已同步到 Neo4j

`; } } function renderNeighbors(data, centerEntity) { const svg = d3.select('#pathSvg'); svg.selectAll('*').remove(); const container = svg.node().parentElement; const width = container.clientWidth; const height = container.clientHeight || 300; svg.attr('width', width).attr('height', height); const neighbors = data.neighbors || []; if (neighbors.length === 0) { document.getElementById('pathViz').innerHTML = `
🔍

未找到邻居节点

`; return; } // 中心节点 const centerNode = { id: centerEntity.id, name: centerEntity.name, x: width / 2, y: height / 2, isCenter: true }; // 邻居节点 - 环形布局 const radius = Math.min(width, height) / 3; const neighborNodes = neighbors.map((n, idx) => ({ id: n.entity_id, name: n.entity_name, x: width / 2 + radius * Math.cos((2 * Math.PI * idx) / neighbors.length - Math.PI / 2), y: height / 2 + radius * Math.sin((2 * Math.PI * idx) / neighbors.length - Math.PI / 2), relationType: n.relation_type })); const allNodes = [centerNode, ...neighborNodes]; // 绘制连线 neighborNodes.forEach(neighbor => { svg.append('line') .attr('x1', centerNode.x) .attr('y1', centerNode.y) .attr('x2', neighbor.x) .attr('y2', neighbor.y) .attr('stroke', '#00d4ff') .attr('stroke-width', 2) .attr('stroke-opacity', 0.4); }); // 绘制节点 const node = svg.selectAll('.neighbor-node') .data(allNodes) .enter().append('g') .attr('class', 'neighbor-node') .attr('transform', d => `translate(${d.x},${d.y})`); node.append('circle') .attr('r', d => d.isCenter ? 35 : 25) .attr('fill', d => d.isCenter ? '#00d4ff' : '#333') .attr('stroke', '#fff') .attr('stroke-width', 2); 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', d => d.isCenter ? '#000' : '#fff') .attr('font-size', d => d.isCenter ? '12px' : '10px') .style('pointer-events', 'none'); // 渲染邻居信息 let html = `

找到 ${neighbors.length} 个邻居节点:

`; neighbors.forEach((n, idx) => { html += `
${idx + 1}
${n.entity_name}
关系: ${n.relation_type}
`; }); document.getElementById('pathInfo').innerHTML = html; } // Show notification helper function showNotification(message, type = 'info') { const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; top: 20px; right: 20px; background: ${type === 'success' ? 'rgba(0, 212, 255, 0.9)' : type === 'error' ? 'rgba(255, 107, 107, 0.9)' : '#333'}; color: ${type === 'success' || type === 'error' ? '#000' : '#fff'}; padding: 12px 20px; border-radius: 8px; z-index: 10000; font-size: 0.9rem; animation: slideIn 0.3s ease; `; notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.style.animation = 'slideOut 0.3s ease'; setTimeout(() => { if (notification.parentNode) { document.body.removeChild(notification); } }, 300); }, 3000); }