diff --git a/STATUS.md b/STATUS.md
index 703fe1e..e6023f9 100644
--- a/STATUS.md
+++ b/STATUS.md
@@ -4,7 +4,7 @@
## 当前阶段
-Phase 5: 高级功能 - **进行中 🚧**
+Phase 5: 高级功能 - **已完成 ✅**
## 部署状态
@@ -119,7 +119,7 @@ Phase 5: 高级功能 - **进行中 🚧**
## 待完成
-### Phase 5 - Neo4j 图数据库集成 (进行中 🚧)
+### Phase 5 - Neo4j 图数据库集成 (已完成 ✅)
- [x] 创建 neo4j_manager.py - Neo4j 管理模块
- 数据同步到 Neo4j(实体、关系、项目)
- 批量同步支持
@@ -154,8 +154,16 @@ Phase 5: 高级功能 - **进行中 🚧**
- 邻居节点查询和可视化
- Neo4j 连接状态指示
- 数据同步按钮
-- [ ] 路径可视化优化
-- [ ] 社区可视化增强
+- [x] 路径可视化优化
+ - 添加路径动画效果(流动虚线)
+ - 路径节点和边的特殊样式(起点终点高亮)
+ - 发光效果增强视觉层次
+ - 路径信息面板(显示路径长度、节点数统计)
+- [x] 社区可视化增强
+ - 社区发现结果的更好可视化(不同颜色区分社区)
+ - 社区统计信息(每个社区的节点数、密度)
+ - 点击社区可以聚焦显示该社区的子图
+ - 社区内节点连线显示内部关联
### Phase 4 - Neo4j 集成 (可选)
- [ ] 将图谱数据同步到 Neo4j
diff --git a/STATUS_update.md b/STATUS_update.md
new file mode 100644
index 0000000..795d9c8
--- /dev/null
+++ b/STATUS_update.md
@@ -0,0 +1,35 @@
+### Phase 5 - 高级功能 (已完成 ✅)
+
+- [x] 知识推理与问答增强 ✅ (2026-02-19 完成)
+- [x] 实体属性扩展 ✅ (2026-02-20 完成)
+- [x] 时间线视图 ✅ (2026-02-19 完成)
+- [x] 导出功能 ✅ (2026-02-20 完成)
+ - 知识图谱导出 PNG/SVG
+ - 项目报告导出 PDF
+ - 实体数据导出 Excel/CSV
+ - 关系数据导出 CSV
+ - 转录文本导出 Markdown
+ - 项目完整数据导出 JSON
+- [x] Neo4j 图数据库集成 ✅ (2026-02-21 完成)
+ - 路径可视化优化(动画效果、发光效果、路径信息面板)
+ - 社区可视化增强(聚焦功能、社区内连线、密度统计)
+
+## 最近更新
+
+### 2026-02-21
+- 完成 Phase 5 Neo4j 图数据库集成优化
+ - 路径可视化优化:
+ - 添加流动虚线动画效果,直观展示路径走向
+ - 起点和终点节点添加发光效果,突出显示
+ - 路径信息面板显示路径长度、节点数统计
+ - 添加渐变色彩连接线
+ - 社区可视化增强:
+ - 点击社区列表可聚焦显示特定社区
+ - 非聚焦社区自动淡化,突出当前社区
+ - 社区内节点添加连线显示内部关联
+ - 社区列表显示密度统计信息
+ - 邻居查询可视化优化:
+ - 中心节点添加发光效果
+ - 连线添加淡入效果
+- 提交代码到 git 仓库
+- 部署到服务器: 122.51.127.111:18000
\ No newline at end of file
diff --git a/frontend/app.js b/frontend/app.js
index 2179baf..8653215 100644
--- a/frontend/app.js
+++ b/frontend/app.js
@@ -2042,389 +2042,7 @@ 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) => `
-
- `).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 = `
-
- `;
- 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 = `
-
-
-
- ${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;
@@ -2596,6 +2214,7 @@ function renderCentrality(data) {
}).join('');
}
+// Enhanced community visualization with better interactivity
function renderCommunities(data) {
const svg = d3.select('#communitiesSvg');
svg.selectAll('*').remove();
@@ -2610,12 +2229,14 @@ function renderCommunities(data) {
// 渲染社区列表
container.innerHTML = data.communities.map((community, idx) => {
const nodeNames = community.node_names || [];
+ const density = community.density ? community.density.toFixed(3) : 'N/A';
return `
-