马一丁

Beautify the front-end page display of GraphRAG

@@ -1486,47 +1486,50 @@ @@ -1486,47 +1486,50 @@
1486 color: #666; 1486 color: #666;
1487 font-size: 14px; 1487 font-size: 14px;
1488 } 1488 }
1489 -  
1490 - /* 知识图谱迷你面板 */  
1491 - .graph-mini-panel { 1489 + /* 知识图谱面板(与左侧内容同宽) */
  1490 + .graph-panel {
  1491 + width: 100%;
1492 border: 2px solid #000000; 1492 border: 2px solid #000000;
1493 - background-color: #f7f7f7;  
1494 - padding: 10px 12px;  
1495 - width: 280px;  
1496 - align-self: flex-start; 1493 + background-color: #ffffff;
1497 display: none; 1494 display: none;
1498 - box-shadow: 4px 4px 0 #0000001a; 1495 + flex-direction: column;
  1496 + gap: 8px;
1499 } 1497 }
1500 1498
1501 - .graph-mini-panel.collapsed .graph-mini-body { 1499 + .graph-panel.collapsed .graph-panel-body {
1502 display: none; 1500 display: none;
1503 } 1501 }
1504 1502
1505 - .graph-mini-header { 1503 + .graph-panel-header {
1506 display: flex; 1504 display: flex;
1507 - align-items: center;  
1508 justify-content: space-between; 1505 justify-content: space-between;
1509 - gap: 8px;  
1510 - margin-bottom: 8px; 1506 + align-items: center;
  1507 + padding: 12px 14px;
  1508 + border-bottom: 2px solid #000000;
  1509 + background-color: #f8f9fa;
  1510 + gap: 10px;
1511 } 1511 }
1512 1512
1513 - .graph-mini-title { 1513 + .graph-panel-title {
1514 font-weight: bold; 1514 font-weight: bold;
1515 - font-size: 14px; 1515 + font-size: 15px;
1516 } 1516 }
1517 1517
1518 - .graph-mini-subtitle { 1518 + .graph-panel-subtitle {
1519 font-size: 12px; 1519 font-size: 12px;
1520 - color: #555555;  
1521 - line-height: 1.2; 1520 + color: #555;
  1521 + margin-top: 4px;
  1522 + line-height: 1.3;
1522 } 1523 }
1523 1524
1524 - .graph-mini-actions { 1525 + .graph-panel-actions {
1525 display: flex; 1526 display: flex;
1526 - gap: 6px; 1527 + align-items: center;
  1528 + gap: 8px;
  1529 + flex-wrap: wrap;
1527 } 1530 }
1528 1531
1529 - .graph-mini-button { 1532 + .graph-panel-button {
1530 border: 1px solid #000000; 1533 border: 1px solid #000000;
1531 background-color: #ffffff; 1534 background-color: #ffffff;
1532 padding: 6px 10px; 1535 padding: 6px 10px;
@@ -1536,40 +1539,143 @@ @@ -1536,40 +1539,143 @@
1536 transition: background-color 0.2s ease, color 0.2s ease; 1539 transition: background-color 0.2s ease, color 0.2s ease;
1537 } 1540 }
1538 1541
1539 - .graph-mini-button:hover { 1542 + .graph-panel-button:hover {
1540 background-color: #000000; 1543 background-color: #000000;
1541 color: #ffffff; 1544 color: #ffffff;
1542 } 1545 }
1543 1546
1544 - .graph-mini-body { 1547 + .graph-status-chip {
  1548 + display: inline-flex;
  1549 + align-items: center;
  1550 + gap: 6px;
  1551 + padding: 6px 10px;
  1552 + border: 1px solid #000000;
  1553 + font-size: 12px;
  1554 + font-weight: bold;
  1555 + }
  1556 +
  1557 + .graph-status-chip.idle { background-color: #fff7ed; color: #b45309; }
  1558 + .graph-status-chip.loading { background-color: #fef3c7; color: #92400e; }
  1559 + .graph-status-chip.ready { background-color: #e8f5e9; color: #2f855a; }
  1560 + .graph-status-chip.error { background-color: #fef2f2; color: #b91c1c; }
  1561 +
  1562 + .graph-panel-body {
  1563 + padding: 10px 12px 14px 12px;
  1564 + display: flex;
  1565 + flex-direction: column;
  1566 + gap: 10px;
  1567 + }
  1568 +
  1569 + .graph-panel-toolbar {
  1570 + display: flex;
  1571 + flex-wrap: wrap;
  1572 + gap: 10px;
  1573 + align-items: center;
  1574 + justify-content: space-between;
  1575 + }
  1576 +
  1577 + .graph-toolbar-left {
  1578 + display: flex;
  1579 + flex-wrap: wrap;
  1580 + align-items: center;
  1581 + gap: 10px;
  1582 + }
  1583 +
  1584 + .graph-stats {
  1585 + display: flex;
  1586 + gap: 10px;
  1587 + font-size: 12px;
  1588 + color: #333;
  1589 + }
  1590 +
  1591 + .graph-stats .stat-label {
  1592 + font-weight: bold;
  1593 + margin-right: 4px;
  1594 + }
  1595 +
  1596 + .graph-filter-group {
  1597 + display: flex;
  1598 + gap: 8px;
  1599 + flex-wrap: wrap;
  1600 + }
  1601 +
  1602 + .graph-filter-item {
  1603 + display: inline-flex;
  1604 + align-items: center;
  1605 + gap: 6px;
  1606 + padding: 6px 8px;
  1607 + border: 1px solid #000000;
  1608 + background-color: #ffffff;
  1609 + cursor: pointer;
  1610 + font-size: 12px;
  1611 + user-select: none;
  1612 + }
  1613 +
  1614 + .graph-filter-item input {
  1615 + accent-color: #000000;
  1616 + }
  1617 +
  1618 + .graph-filter-count {
  1619 + color: #666666;
  1620 + font-size: 11px;
  1621 + }
  1622 +
  1623 + .graph-search {
  1624 + display: flex;
  1625 + gap: 8px;
  1626 + align-items: center;
  1627 + }
  1628 +
  1629 + .graph-search input {
  1630 + padding: 8px 10px;
  1631 + border: 1px solid #000000;
  1632 + min-width: 200px;
  1633 + }
  1634 +
  1635 + .graph-panel-canvas {
1545 position: relative; 1636 position: relative;
1546 - height: 240px;  
1547 - width: 240px; 1637 + height: 360px;
  1638 + width: 100%;
1548 border: 1px dashed #000000; 1639 border: 1px dashed #000000;
1549 background-color: #ffffff; 1640 background-color: #ffffff;
1550 - overflow: hidden;  
1551 } 1641 }
1552 1642
1553 - .graph-mini-placeholder { 1643 + .graph-panel-placeholder {
1554 position: absolute; 1644 position: absolute;
1555 inset: 0; 1645 inset: 0;
1556 display: flex; 1646 display: flex;
  1647 + flex-direction: column;
1557 align-items: center; 1648 align-items: center;
1558 justify-content: center; 1649 justify-content: center;
  1650 + gap: 6px;
1559 text-align: center; 1651 text-align: center;
  1652 + padding: 10px;
  1653 + font-size: 13px;
  1654 + color: #555;
  1655 + background-color: #ffffff;
  1656 + z-index: 2;
  1657 + }
  1658 +
  1659 + .graph-panel-placeholder.error {
  1660 + color: #b42318;
  1661 + }
  1662 +
  1663 + .graph-panel-detail {
  1664 + border: 1px solid #e5e5e5;
1560 padding: 8px; 1665 padding: 8px;
  1666 + background-color: #fafafa;
1561 font-size: 12px; 1667 font-size: 12px;
1562 - color: #666666; 1668 + line-height: 1.5;
1563 } 1669 }
1564 1670
1565 - .graph-mini-placeholder.error {  
1566 - color: #b42318; 1671 + .graph-panel-detail .detail-title {
  1672 + font-weight: bold;
  1673 + margin-bottom: 4px;
1567 } 1674 }
1568 1675
1569 - .graph-mini-canvas {  
1570 - height: 100%;  
1571 - width: 100%;  
1572 - display: none; 1676 + .graph-panel-detail .detail-meta {
  1677 + color: #555;
  1678 + margin-bottom: 6px;
1573 } 1679 }
1574 </style> 1680 </style>
1575 1681
@@ -1736,9 +1842,12 @@ @@ -1736,9 +1842,12 @@
1736 let configDirty = false; 1842 let configDirty = false;
1737 let graphragEnabled = false; 1843 let graphragEnabled = false;
1738 let graphragSettingLoaded = false; 1844 let graphragSettingLoaded = false;
1739 - let graphMiniNetwork = null;  
1740 - let graphMiniPreferredTaskId = null;  
1741 - let graphMiniLoading = false; 1845 + let graphPanelNetwork = null;
  1846 + let graphPanelData = { nodes: [], edges: [] };
  1847 + let graphPanelFilters = new Set(['topic', 'engine', 'section', 'search_query', 'source']);
  1848 + let graphPanelTaskId = null;
  1849 + let graphPanelState = 'idle';
  1850 + let graphPanelLoading = false;
1742 let configAutoRefreshTimer = null; 1851 let configAutoRefreshTimer = null;
1743 let systemStarted = false; 1852 let systemStarted = false;
1744 let systemStarting = false; 1853 let systemStarting = false;
@@ -4882,21 +4991,58 @@ function getConsoleContainer() { @@ -4882,21 +4991,58 @@ function getConsoleContainer() {
4882 <!-- 任务进度区域 --> 4991 <!-- 任务进度区域 -->
4883 <div id="taskProgressArea"></div> 4992 <div id="taskProgressArea"></div>
4884 4993
4885 - <!-- 知识图谱迷你预览(GraphRAG 开启时显示) -->  
4886 - <div class="graph-mini-panel" id="graphMiniPanel">  
4887 - <div class="graph-mini-header"> 4994 + <!-- 知识图谱面板(GraphRAG 开启时显示) -->
  4995 + <div class="graph-panel" id="graphPanel">
  4996 + <div class="graph-panel-header">
4888 <div> 4997 <div>
4889 - <div class="graph-mini-title">知识图谱</div>  
4890 - <div class="graph-mini-subtitle">GraphRAG 生成概览</div> 4998 + <div class="graph-panel-title">知识图谱</div>
  4999 + <div class="graph-panel-subtitle">GraphRAG 节点关系可视化</div>
4891 </div> 5000 </div>
4892 - <div class="graph-mini-actions">  
4893 - <button class="graph-mini-button" id="graphMiniRefresh" title="刷新知识图谱">刷新</button>  
4894 - <button class="graph-mini-button" id="graphMiniToggle" title="折叠/展开">折叠</button> 5001 + <div class="graph-panel-actions">
  5002 + <span class="graph-status-chip idle" id="graphStatusChip">未生成</span>
  5003 + <button class="graph-panel-button" id="graphFullBtn" title="在新标签页查看">全屏</button>
  5004 + <button class="graph-panel-button" id="graphRefreshBtn" title="刷新知识图谱">刷新</button>
  5005 + <button class="graph-panel-button" id="graphCollapseBtn" title="折叠/展开">收起</button>
4895 </div> 5006 </div>
4896 </div> 5007 </div>
4897 - <div class="graph-mini-body" id="graphMiniBody">  
4898 - <div class="graph-mini-placeholder" id="graphMiniPlaceholder">等待图谱生成...</div>  
4899 - <div class="graph-mini-canvas" id="graphMiniCanvas"></div> 5008 + <div class="graph-panel-body" id="graphPanelBody">
  5009 + <div class="graph-panel-toolbar">
  5010 + <div class="graph-toolbar-left">
  5011 + <div class="graph-stats">
  5012 + <span><span class="stat-label">节点</span><span id="graphNodeCount">0</span></span>
  5013 + <span><span class="stat-label">关系</span><span id="graphEdgeCount">0</span></span>
  5014 + </div>
  5015 + <div class="graph-filter-group" id="graphFilterGroup">
  5016 + <label class="graph-filter-item">
  5017 + <input type="checkbox" data-type="topic" checked> 主题 <span class="graph-filter-count" data-type-count="topic">(0)</span>
  5018 + </label>
  5019 + <label class="graph-filter-item">
  5020 + <input type="checkbox" data-type="engine" checked> 引擎 <span class="graph-filter-count" data-type-count="engine">(0)</span>
  5021 + </label>
  5022 + <label class="graph-filter-item">
  5023 + <input type="checkbox" data-type="section" checked> 报告段落 <span class="graph-filter-count" data-type-count="section">(0)</span>
  5024 + </label>
  5025 + <label class="graph-filter-item">
  5026 + <input type="checkbox" data-type="search_query" checked> 搜索词 <span class="graph-filter-count" data-type-count="search_query">(0)</span>
  5027 + </label>
  5028 + <label class="graph-filter-item">
  5029 + <input type="checkbox" data-type="source" checked> 数据来源 <span class="graph-filter-count" data-type-count="source">(0)</span>
  5030 + </label>
  5031 + </div>
  5032 + </div>
  5033 + <div class="graph-search">
  5034 + <input type="text" id="graphSearchInput" placeholder="搜索节点...">
  5035 + <button class="graph-panel-button" id="graphFitBtn">适应</button>
  5036 + </div>
  5037 + </div>
  5038 + <div class="graph-panel-canvas" id="graphPanelCanvas">
  5039 + <div class="graph-panel-placeholder" id="graphPanelPlaceholder">等待图谱生成...</div>
  5040 + </div>
  5041 + <div class="graph-panel-detail" id="graphPanelDetail" style="display: none;">
  5042 + <div class="detail-title" id="graphDetailTitle"></div>
  5043 + <div class="detail-meta" id="graphDetailMeta"></div>
  5044 + <div class="detail-props" id="graphDetailProps"></div>
  5045 + </div>
4900 </div> 5046 </div>
4901 </div> 5047 </div>
4902 5048
@@ -4910,7 +5056,7 @@ function getConsoleContainer() { @@ -4910,7 +5056,7 @@ function getConsoleContainer() {
4910 5056
4911 reportContent.innerHTML = interfaceHTML; 5057 reportContent.innerHTML = interfaceHTML;
4912 initializeReportControls(); 5058 initializeReportControls();
4913 - initializeGraphMiniPanel(statusData); 5059 + initializeGraphPanel(statusData);
4914 resetReportStreamOutput('等待新的Report任务启动...'); 5060 resetReportStreamOutput('等待新的Report任务启动...');
4915 updateReportStreamStatus('idle'); 5061 updateReportStreamStatus('idle');
4916 5062
@@ -4942,8 +5088,8 @@ function getConsoleContainer() { @@ -4942,8 +5088,8 @@ function getConsoleContainer() {
4942 } 5088 }
4943 } 5089 }
4944 5090
4945 - async function initializeGraphMiniPanel(statusData) {  
4946 - const panel = document.getElementById('graphMiniPanel'); 5091 + async function initializeGraphPanel(statusData) {
  5092 + const panel = document.getElementById('graphPanel');
4947 if (!panel) return; 5093 if (!panel) return;
4948 5094
4949 const enabled = await ensureGraphragSetting(); 5095 const enabled = await ensureGraphragSetting();
@@ -4952,115 +5098,223 @@ function getConsoleContainer() { @@ -4952,115 +5098,223 @@ function getConsoleContainer() {
4952 return; 5098 return;
4953 } 5099 }
4954 5100
4955 - panel.style.display = 'block';  
4956 - bindGraphMiniEvents(); 5101 + panel.style.display = 'flex';
  5102 + bindGraphPanelEvents();
4957 5103
4958 - const currentTaskId = statusData && statusData.current_task  
4959 - ? statusData.current_task.task_id  
4960 - : (lastCompletedReportTask ? lastCompletedReportTask.task_id : null);  
4961 - const currentTaskStatus = statusData && statusData.current_task  
4962 - ? statusData.current_task.status  
4963 - : '';  
4964 - if (currentTaskId) {  
4965 - graphMiniPreferredTaskId = currentTaskId;  
4966 - } 5104 + const currentTask = statusData && statusData.current_task ? statusData.current_task : null;
  5105 + graphPanelTaskId = currentTask?.task_id || (lastCompletedReportTask ? lastCompletedReportTask.task_id : null);
4967 5106
4968 - if (currentTaskStatus === 'running') {  
4969 - setGraphMiniWaiting(currentTaskId); 5107 + if (currentTask && currentTask.status === 'running') {
  5108 + setGraphPanelAwaiting(graphPanelTaskId);
4970 return; 5109 return;
4971 } 5110 }
4972 5111
4973 - if (panel.classList.contains('collapsed')) {  
4974 - setGraphMiniPlaceholder('展开以查看知识图谱');  
4975 - return; 5112 + refreshGraphPanel(graphPanelTaskId, true);
  5113 + }
  5114 +
  5115 + function bindGraphPanelEvents() {
  5116 + const refreshBtn = document.getElementById('graphRefreshBtn');
  5117 + const collapseBtn = document.getElementById('graphCollapseBtn');
  5118 + const fullBtn = document.getElementById('graphFullBtn');
  5119 + const fitBtn = document.getElementById('graphFitBtn');
  5120 + const searchInput = document.getElementById('graphSearchInput');
  5121 + const filterGroup = document.getElementById('graphFilterGroup');
  5122 +
  5123 + if (refreshBtn && !refreshBtn.dataset.bound) {
  5124 + refreshBtn.dataset.bound = 'true';
  5125 + refreshBtn.addEventListener('click', () => refreshGraphPanel(graphPanelTaskId, true));
  5126 + }
  5127 +
  5128 + if (collapseBtn && !collapseBtn.dataset.bound) {
  5129 + collapseBtn.dataset.bound = 'true';
  5130 + collapseBtn.addEventListener('click', () => {
  5131 + const panel = document.getElementById('graphPanel');
  5132 + if (!panel) return;
  5133 + const collapsed = panel.classList.toggle('collapsed');
  5134 + collapseBtn.textContent = collapsed ? '展开' : '收起';
  5135 + if (!collapsed) {
  5136 + refreshGraphPanel(graphPanelTaskId, false);
  5137 + } else {
  5138 + setGraphPanelPlaceholder('已折叠,展开后可查看知识图谱');
  5139 + }
  5140 + });
4976 } 5141 }
4977 5142
4978 - refreshGraphMini(graphMiniPreferredTaskId, true);  
4979 - } 5143 + if (fullBtn && !fullBtn.dataset.bound) {
  5144 + fullBtn.dataset.bound = 'true';
  5145 + fullBtn.addEventListener('click', () => {
  5146 + const target = graphPanelTaskId || (lastCompletedReportTask ? lastCompletedReportTask.task_id : null);
  5147 + const url = target ? `/graph-viewer/${target}` : '/graph-viewer';
  5148 + window.open(url, '_blank');
  5149 + });
  5150 + }
4980 5151
4981 - function bindGraphMiniEvents() {  
4982 - const toggleBtn = document.getElementById('graphMiniToggle');  
4983 - const refreshBtn = document.getElementById('graphMiniRefresh'); 5152 + if (fitBtn && !fitBtn.dataset.bound) {
  5153 + fitBtn.dataset.bound = 'true';
  5154 + fitBtn.addEventListener('click', () => {
  5155 + if (graphPanelNetwork) {
  5156 + graphPanelNetwork.fit({ animation: { duration: 300, easing: 'easeInOutQuad' } });
  5157 + }
  5158 + });
  5159 + }
4984 5160
4985 - if (toggleBtn && !toggleBtn.dataset.bound) {  
4986 - toggleBtn.dataset.bound = 'true';  
4987 - toggleBtn.addEventListener('click', toggleGraphMiniPanel); 5161 + if (searchInput && !searchInput.dataset.bound) {
  5162 + searchInput.dataset.bound = 'true';
  5163 + searchInput.addEventListener('keydown', (e) => {
  5164 + if (e.key === 'Enter') {
  5165 + focusGraphByKeyword(searchInput.value);
  5166 + }
  5167 + });
  5168 + searchInput.addEventListener('input', () => {
  5169 + if (!searchInput.value) {
  5170 + clearGraphSelection();
  5171 + }
  5172 + });
4988 } 5173 }
4989 5174
4990 - if (refreshBtn && !refreshBtn.dataset.bound) {  
4991 - refreshBtn.dataset.bound = 'true';  
4992 - refreshBtn.addEventListener('click', () => refreshGraphMini(graphMiniPreferredTaskId, true)); 5175 + if (filterGroup && !filterGroup.dataset.bound) {
  5176 + filterGroup.dataset.bound = 'true';
  5177 + filterGroup.addEventListener('change', (event) => {
  5178 + const checkbox = event.target.closest('input[type="checkbox"][data-type]');
  5179 + if (!checkbox) return;
  5180 + const type = (checkbox.dataset.type || '').toLowerCase();
  5181 + if (checkbox.checked) {
  5182 + graphPanelFilters.add(type);
  5183 + } else {
  5184 + graphPanelFilters.delete(type);
  5185 + }
  5186 + renderGraphPanel(graphPanelData, false);
  5187 + });
4993 } 5188 }
4994 } 5189 }
4995 5190
4996 - function toggleGraphMiniPanel() {  
4997 - const panel = document.getElementById('graphMiniPanel');  
4998 - const toggleBtn = document.getElementById('graphMiniToggle');  
4999 - if (!panel) return;  
5000 - const collapsed = panel.classList.toggle('collapsed');  
5001 - if (toggleBtn) {  
5002 - toggleBtn.textContent = collapsed ? '展开' : '折叠';  
5003 - }  
5004 - if (!collapsed) {  
5005 - refreshGraphMini(graphMiniPreferredTaskId, true);  
5006 - } else {  
5007 - setGraphMiniPlaceholder('已折叠,展开以查看知识图谱'); 5191 + function setGraphPanelState(state, message = '') {
  5192 + graphPanelState = state;
  5193 + const chip = document.getElementById('graphStatusChip');
  5194 + if (!chip) return;
  5195 + chip.classList.remove('idle', 'loading', 'ready', 'error');
  5196 + chip.classList.add(state);
  5197 + const textMap = {
  5198 + idle: '未生成',
  5199 + loading: '正在生成',
  5200 + ready: '已生成',
  5201 + error: '加载失败'
  5202 + };
  5203 + chip.textContent = textMap[state] || state;
  5204 + if (message) {
  5205 + setGraphPanelPlaceholder(message, state === 'error' ? 'error' : '');
5008 } 5206 }
5009 } 5207 }
5010 5208
5011 - function setGraphMiniPlaceholder(message, type = '') {  
5012 - const placeholder = document.getElementById('graphMiniPlaceholder');  
5013 - const canvas = document.getElementById('graphMiniCanvas');  
5014 - if (!placeholder || !canvas) return; 5209 + function setGraphPanelPlaceholder(message, type = '') {
  5210 + const placeholder = document.getElementById('graphPanelPlaceholder');
  5211 + if (!placeholder) return;
5015 placeholder.textContent = message || ''; 5212 placeholder.textContent = message || '';
5016 - if (type === 'error') {  
5017 - placeholder.classList.add('error');  
5018 - } else {  
5019 - placeholder.classList.remove('error');  
5020 - }  
5021 - placeholder.style.display = 'flex';  
5022 - canvas.style.display = 'none'; 5213 + placeholder.classList.toggle('error', type === 'error');
  5214 + placeholder.style.display = message ? 'flex' : 'none';
5023 } 5215 }
5024 5216
5025 - async function fetchGraphData(taskId = null) {  
5026 - const url = taskId ? `/api/graph/${taskId}` : '/api/graph/latest';  
5027 - try {  
5028 - const response = await fetch(url, { cache: 'no-store' });  
5029 - const data = await response.json();  
5030 - if (!response.ok || !data.success || !data.graph) {  
5031 - return null; 5217 + function updateGraphStats(allData, filteredNodes = null, filteredEdges = null) {
  5218 + const nodeCountEl = document.getElementById('graphNodeCount');
  5219 + const edgeCountEl = document.getElementById('graphEdgeCount');
  5220 + if (nodeCountEl) nodeCountEl.textContent = (filteredNodes || allData.nodes || []).length;
  5221 + if (edgeCountEl) edgeCountEl.textContent = (filteredEdges || allData.edges || []).length;
  5222 +
  5223 + const typeCounts = {};
  5224 + (allData.nodes || []).forEach(node => {
  5225 + const key = (node.group || node.type || 'other').toLowerCase();
  5226 + typeCounts[key] = (typeCounts[key] || 0) + 1;
  5227 + });
  5228 +
  5229 + document.querySelectorAll('.graph-filter-count').forEach(el => {
  5230 + const t = el.dataset.typeCount;
  5231 + el.textContent = `(${typeCounts[t] || 0})`;
  5232 + });
  5233 + }
  5234 +
  5235 + function renderGraphPanel(graph, resetPlaceholder = true) {
  5236 + const panel = document.getElementById('graphPanel');
  5237 + const canvasWrapper = document.getElementById('graphPanelCanvas');
  5238 + if (!panel || !canvasWrapper) return;
  5239 +
  5240 + graphPanelData = graph || { nodes: [], edges: [] };
  5241 + const allowed = graphPanelFilters;
  5242 + const nodes = (graphPanelData.nodes || []).filter(node => allowed.has((node.group || node.type || '').toLowerCase()));
  5243 + const nodeIdSet = new Set(nodes.map(n => n.id));
  5244 + const edges = (graphPanelData.edges || []).filter(edge => nodeIdSet.has(edge.from) && nodeIdSet.has(edge.to));
  5245 +
  5246 + updateGraphStats(graphPanelData, nodes, edges);
  5247 +
  5248 + const placeholder = document.getElementById('graphPanelPlaceholder');
  5249 + if (!nodes.length) {
  5250 + setGraphPanelState('ready', '未找到匹配的节点,请调整筛选或稍后再试');
  5251 + if (placeholder) placeholder.style.display = 'flex';
  5252 + if (graphPanelNetwork) {
  5253 + graphPanelNetwork.destroy();
  5254 + graphPanelNetwork = null;
5032 } 5255 }
5033 - return data;  
5034 - } catch (error) {  
5035 - console.warn('获取知识图谱失败:', error);  
5036 - return null; 5256 + return;
  5257 + }
  5258 +
  5259 + if (resetPlaceholder && placeholder) {
  5260 + placeholder.style.display = 'none';
5037 } 5261 }
5038 - }  
5039 5262
5040 - function renderGraphMini(graph) {  
5041 - const canvas = document.getElementById('graphMiniCanvas');  
5042 - const placeholder = document.getElementById('graphMiniPlaceholder');  
5043 - if (!canvas || !placeholder) return;  
5044 if (!(window.vis && window.vis.Network)) { 5263 if (!(window.vis && window.vis.Network)) {
5045 - setGraphMiniPlaceholder('图谱组件未加载,请检查网络后重试', 'error'); 5264 + updateGraphStats({ nodes: [], edges: [] });
  5265 + setGraphPanelState('error', '图谱组件未加载,请检查网络后重试');
5046 return; 5266 return;
5047 } 5267 }
5048 5268
5049 - const nodes = new vis.DataSet((graph.nodes || []).map(node => ({  
5050 - id: node.id,  
5051 - label: node.label || node.id,  
5052 - group: node.group || node.type,  
5053 - title: node.title || '',  
5054 - size: 12  
5055 - }))); 5269 + const container = canvasWrapper;
  5270 + let canvas = document.getElementById('graphPanelCanvasInner');
  5271 + if (!canvas) {
  5272 + canvas = document.createElement('div');
  5273 + canvas.id = 'graphPanelCanvasInner';
  5274 + canvas.style.height = '100%';
  5275 + canvas.style.width = '100%';
  5276 + container.appendChild(canvas);
  5277 + } else {
  5278 + canvas.innerHTML = '';
  5279 + }
  5280 + setGraphPanelPlaceholder('');
5056 5281
5057 - const edges = new vis.DataSet((graph.edges || []).map(edge => ({ 5282 + const colorMap = {
  5283 + topic: '#ef4444',
  5284 + engine: '#f59e0b',
  5285 + section: '#10b981',
  5286 + search_query: '#3b82f6',
  5287 + source: '#8b5cf6'
  5288 + };
  5289 +
  5290 + const nodeData = new vis.DataSet(nodes.map(node => {
  5291 + const nodeType = (node.group || node.type || '').toLowerCase();
  5292 + return {
  5293 + id: node.id,
  5294 + label: node.label || node.id,
  5295 + group: node.group || node.type,
  5296 + title: node.title || '',
  5297 + properties: node.properties || {},
  5298 + size: 14,
  5299 + color: {
  5300 + background: colorMap[nodeType] || '#ffffff',
  5301 + border: '#000000',
  5302 + highlight: {
  5303 + background: colorMap[nodeType] || '#e0e0e0',
  5304 + border: '#000000'
  5305 + }
  5306 + }
  5307 + };
  5308 + }));
  5309 +
  5310 + const edgeData = new vis.DataSet(edges.map(edge => ({
5058 from: edge.from, 5311 from: edge.from,
5059 to: edge.to, 5312 to: edge.to,
5060 label: edge.label || '', 5313 label: edge.label || '',
5061 arrows: 'to', 5314 arrows: 'to',
5062 - color: '#444444',  
5063 - font: { align: 'top', size: 10 } 5315 + font: { align: 'top', size: 10 },
  5316 + color: '#555555',
  5317 + smooth: true
5064 }))); 5318 })));
5065 5319
5066 const options = { 5320 const options = {
@@ -5071,83 +5325,154 @@ function getConsoleContainer() { @@ -5071,83 +5325,154 @@ function getConsoleContainer() {
5071 shape: 'dot', 5325 shape: 'dot',
5072 borderWidth: 1, 5326 borderWidth: 1,
5073 font: { size: 12 }, 5327 font: { size: 12 },
5074 - scaling: { min: 8, max: 18 } 5328 + scaling: { min: 8, max: 20 }
5075 }, 5329 },
5076 edges: { 5330 edges: {
5077 - smooth: true,  
5078 color: '#999999', 5331 color: '#999999',
5079 width: 1 5332 width: 1
5080 }, 5333 },
5081 physics: { 5334 physics: {
5082 stabilization: true, 5335 stabilization: true,
5083 - barnesHut: { avoidOverlap: 0.5, springLength: 80, springConstant: 0.02 } 5336 + barnesHut: { avoidOverlap: 0.3, springLength: 90, springConstant: 0.02 }
5084 } 5337 }
5085 }; 5338 };
5086 5339
5087 - if (graphMiniNetwork) {  
5088 - graphMiniNetwork.destroy(); 5340 + if (graphPanelNetwork) {
  5341 + graphPanelNetwork.destroy();
5089 } 5342 }
  5343 + graphPanelNetwork = new vis.Network(canvas, { nodes: nodeData, edges: edgeData }, options);
5090 5344
5091 - placeholder.style.display = 'none';  
5092 - canvas.style.display = 'block';  
5093 - graphMiniNetwork = new vis.Network(canvas, { nodes, edges }, options);  
5094 const fitOnce = () => { 5345 const fitOnce = () => {
5095 - graphMiniNetwork.fit({ animation: { duration: 300, easing: 'easeInOutQuad' } });  
5096 - graphMiniNetwork.off('stabilizationIterationsDone', fitOnce); 5346 + graphPanelNetwork.fit({ animation: { duration: 300, easing: 'easeInOutQuad' } });
  5347 + graphPanelNetwork.off('stabilizationIterationsDone', fitOnce);
5097 }; 5348 };
5098 - graphMiniNetwork.on('stabilizationIterationsDone', fitOnce); 5349 + graphPanelNetwork.on('stabilizationIterationsDone', fitOnce);
  5350 +
  5351 + graphPanelNetwork.on('selectNode', params => {
  5352 + const selectedId = params.nodes[0];
  5353 + const selectedNode = nodes.find(n => n.id === selectedId);
  5354 + showGraphDetail(selectedNode);
  5355 + });
  5356 + graphPanelNetwork.on('deselectNode', hideGraphDetail);
  5357 + }
  5358 +
  5359 + function showGraphDetail(node) {
  5360 + const detail = document.getElementById('graphPanelDetail');
  5361 + const title = document.getElementById('graphDetailTitle');
  5362 + const meta = document.getElementById('graphDetailMeta');
  5363 + const props = document.getElementById('graphDetailProps');
  5364 + if (!detail || !title || !meta || !props) return;
  5365 + if (!node) {
  5366 + hideGraphDetail();
  5367 + return;
  5368 + }
  5369 + detail.style.display = 'block';
  5370 + title.textContent = node.label || node.id;
  5371 + meta.textContent = `类型: ${(node.group || node.type || '未知')}`;
  5372 + const properties = node.properties || {};
  5373 + const lines = Object.entries(properties).map(([k, v]) => `<div><strong>${k}:</strong> ${v}</div>`);
  5374 + props.innerHTML = lines.length ? lines.join('') : '暂无更多属性';
  5375 + }
  5376 +
  5377 + function hideGraphDetail() {
  5378 + const detail = document.getElementById('graphPanelDetail');
  5379 + if (detail) {
  5380 + detail.style.display = 'none';
  5381 + }
5099 } 5382 }
5100 5383
5101 - async function refreshGraphMini(taskId = null, allowFallback = true) {  
5102 - const panel = document.getElementById('graphMiniPanel'); 5384 + function focusGraphByKeyword(keyword) {
  5385 + if (!graphPanelNetwork || !keyword) return;
  5386 + const lower = keyword.toLowerCase();
  5387 + const nodes = graphPanelNetwork.body.data.nodes.get();
  5388 + const matched = nodes.filter(n => (n.label || '').toLowerCase().includes(lower));
  5389 + if (matched.length) {
  5390 + const ids = matched.map(n => n.id);
  5391 + graphPanelNetwork.selectNodes(ids);
  5392 + graphPanelNetwork.focus(ids[0], { scale: 1, animation: { duration: 300, easing: 'easeInOutQuad' } });
  5393 + showGraphDetail(matched[0]);
  5394 + }
  5395 + }
  5396 +
  5397 + function clearGraphSelection() {
  5398 + if (graphPanelNetwork) {
  5399 + graphPanelNetwork.unselectAll();
  5400 + }
  5401 + hideGraphDetail();
  5402 + }
  5403 +
  5404 + async function fetchGraphData(taskId = null) {
  5405 + const url = taskId ? `/api/graph/${taskId}` : '/api/graph/latest';
  5406 + try {
  5407 + const response = await fetch(url, { cache: 'no-store' });
  5408 + const data = await response.json();
  5409 + if (!response.ok || !data.success || !data.graph) {
  5410 + return null;
  5411 + }
  5412 + return data;
  5413 + } catch (error) {
  5414 + console.warn('获取知识图谱失败:', error);
  5415 + return null;
  5416 + }
  5417 + }
  5418 +
  5419 + async function refreshGraphPanel(taskId = null, allowFallback = true) {
  5420 + const panel = document.getElementById('graphPanel');
5103 if (!panel) return; 5421 if (!panel) return;
5104 const enabled = await ensureGraphragSetting(); 5422 const enabled = await ensureGraphragSetting();
5105 if (!enabled) { 5423 if (!enabled) {
5106 panel.style.display = 'none'; 5424 panel.style.display = 'none';
5107 return; 5425 return;
5108 } 5426 }
  5427 + panel.style.display = 'flex';
  5428 + bindGraphPanelEvents();
  5429 +
5109 if (panel.classList.contains('collapsed')) { 5430 if (panel.classList.contains('collapsed')) {
5110 return; 5431 return;
5111 } 5432 }
5112 5433
5113 - bindGraphMiniEvents();  
5114 - panel.style.display = 'block';  
5115 -  
5116 if (taskId) { 5434 if (taskId) {
5117 - graphMiniPreferredTaskId = taskId; 5435 + graphPanelTaskId = taskId;
5118 } 5436 }
5119 5437
5120 - if (graphMiniLoading) {  
5121 - return;  
5122 - } 5438 + if (graphPanelLoading) return;
  5439 + graphPanelLoading = true;
  5440 + setGraphPanelState('loading', '正在加载知识图谱...');
5123 5441
5124 - const targetTaskId = taskId || graphMiniPreferredTaskId || (lastCompletedReportTask ? lastCompletedReportTask.task_id : null);  
5125 - setGraphMiniPlaceholder('加载知识图谱...');  
5126 - graphMiniLoading = true;  
5127 try { 5442 try {
  5443 + const targetTaskId = graphPanelTaskId || (lastCompletedReportTask ? lastCompletedReportTask.task_id : null);
5128 let data = await fetchGraphData(targetTaskId); 5444 let data = await fetchGraphData(targetTaskId);
5129 if (!data && allowFallback && targetTaskId) { 5445 if (!data && allowFallback && targetTaskId) {
5130 data = await fetchGraphData(null); 5446 data = await fetchGraphData(null);
5131 } 5447 }
5132 if (data && data.graph) { 5448 if (data && data.graph) {
5133 - renderGraphMini(data.graph); 5449 + graphPanelTaskId = targetTaskId || data.report_id || graphPanelTaskId;
  5450 + renderGraphPanel(data.graph);
  5451 + setGraphPanelState('ready');
5134 } else { 5452 } else {
5135 - setGraphMiniPlaceholder('暂未找到知识图谱,请稍后点击刷新重试', 'error'); 5453 + updateGraphStats({ nodes: [], edges: [] });
  5454 + setGraphPanelState('idle', '暂未找到知识图谱,请生成报告后刷新');
5136 } 5455 }
  5456 + } catch (error) {
  5457 + console.warn('刷新知识图谱失败:', error);
  5458 + updateGraphStats({ nodes: [], edges: [] });
  5459 + setGraphPanelState('error', '加载知识图谱失败,请稍后重试');
5137 } finally { 5460 } finally {
5138 - graphMiniLoading = false; 5461 + graphPanelLoading = false;
5139 } 5462 }
5140 } 5463 }
5141 5464
5142 - function setGraphMiniWaiting(taskId) {  
5143 - graphMiniPreferredTaskId = taskId || graphMiniPreferredTaskId; 5465 + function setGraphPanelAwaiting(taskId) {
  5466 + graphPanelTaskId = taskId || graphPanelTaskId;
5144 ensureGraphragSetting().then(enabled => { 5467 ensureGraphragSetting().then(enabled => {
5145 if (!enabled) return; 5468 if (!enabled) return;
5146 - const panel = document.getElementById('graphMiniPanel'); 5469 + const panel = document.getElementById('graphPanel');
5147 if (!panel) return; 5470 if (!panel) return;
5148 - panel.style.display = 'block';  
5149 - bindGraphMiniEvents();  
5150 - setGraphMiniPlaceholder('等待图谱生成...'); 5471 + panel.style.display = 'flex';
  5472 + bindGraphPanelEvents();
  5473 + updateGraphStats({ nodes: [], edges: [] });
  5474 + setGraphPanelState('loading', '报告生成中,知识图谱生成后自动刷新');
  5475 + setGraphPanelPlaceholder('正在生成知识图谱...');
5151 }); 5476 });
5152 } 5477 }
5153 5478
@@ -5548,7 +5873,7 @@ function getConsoleContainer() { @@ -5548,7 +5873,7 @@ function getConsoleContainer() {
5548 if (data.success) { 5873 if (data.success) {
5549 reportTaskId = data.task_id; 5874 reportTaskId = data.task_id;
5550 showMessage('报告生成已启动', 'success'); 5875 showMessage('报告生成已启动', 'success');
5551 - setGraphMiniWaiting(reportTaskId); 5876 + setGraphPanelAwaiting(reportTaskId);
5552 5877
5553 // 更新任务状态显示 5878 // 更新任务状态显示
5554 updateTaskProgressStatus({ 5879 updateTaskProgressStatus({
@@ -5638,8 +5963,8 @@ function getConsoleContainer() { @@ -5638,8 +5963,8 @@ function getConsoleContainer() {
5638 if (data.task.status === 'completed') { 5963 if (data.task.status === 'completed') {
5639 stopProgressPolling(); 5964 stopProgressPolling();
5640 showMessage('报告生成完成!', 'success'); 5965 showMessage('报告生成完成!', 'success');
5641 - graphMiniPreferredTaskId = data.task.task_id;  
5642 - refreshGraphMini(data.task.task_id, true); 5966 + graphPanelTaskId = data.task.task_id;
  5967 + refreshGraphPanel(data.task.task_id, true);
5643 5968
5644 // 自动显示报告 5969 // 自动显示报告
5645 viewReport(taskId); 5970 viewReport(taskId);
@@ -5929,13 +6254,13 @@ function getConsoleContainer() { @@ -5929,13 +6254,13 @@ function getConsoleContainer() {
5929 if (eventType === 'status' && task) { 6254 if (eventType === 'status' && task) {
5930 if (task.status === 'running') { 6255 if (task.status === 'running') {
5931 resetReportLogsForNewTask(task.task_id, '收到流式状态事件,已重置日志'); 6256 resetReportLogsForNewTask(task.task_id, '收到流式状态事件,已重置日志');
5932 - setGraphMiniWaiting(task.task_id); 6257 + setGraphPanelAwaiting(task.task_id);
5933 } 6258 }
5934 updateTaskProgressStatus(task); 6259 updateTaskProgressStatus(task);
5935 reportTaskId = task.status === 'running' ? task.task_id : null; 6260 reportTaskId = task.status === 'running' ? task.task_id : null;
5936 if (task.status === 'completed') { 6261 if (task.status === 'completed') {
5937 lastCompletedReportTask = task; 6262 lastCompletedReportTask = task;
5938 - graphMiniPreferredTaskId = task.task_id; 6263 + graphPanelTaskId = task.task_id;
5939 setGenerateButtonState(false); 6264 setGenerateButtonState(false);
5940 } else if (task.status === 'running') { 6265 } else if (task.status === 'running') {
5941 setGenerateButtonState(true); 6266 setGenerateButtonState(true);
@@ -6027,8 +6352,8 @@ function getConsoleContainer() { @@ -6027,8 +6352,8 @@ function getConsoleContainer() {
6027 if (task) { 6352 if (task) {
6028 lastCompletedReportTask = task; 6353 lastCompletedReportTask = task;
6029 updateDownloadButtonState(task); 6354 updateDownloadButtonState(task);
6030 - graphMiniPreferredTaskId = task.task_id;  
6031 - refreshGraphMini(task.task_id, true); 6355 + graphPanelTaskId = task.task_id;
  6356 + refreshGraphPanel(task.task_id, true);
6032 } 6357 }
6033 if (eventData.task_id && !reportAutoPreviewLoaded) { 6358 if (eventData.task_id && !reportAutoPreviewLoaded) {
6034 viewReport(eventData.task_id); 6359 viewReport(eventData.task_id);