马一丁

Optimize search when viewing GraphRAG in full screen

@@ -129,6 +129,27 @@ @@ -129,6 +129,27 @@
129 color: var(--text-muted); 129 color: var(--text-muted);
130 } 130 }
131 131
  132 + .search-group {
  133 + display: flex;
  134 + align-items: center;
  135 + gap: 10px;
  136 + flex: 1;
  137 + max-width: 560px;
  138 + }
  139 +
  140 + .search-actions {
  141 + display: flex;
  142 + align-items: center;
  143 + gap: 6px;
  144 + }
  145 +
  146 + .search-status {
  147 + min-width: 44px;
  148 + text-align: center;
  149 + font-size: 0.8rem;
  150 + color: var(--text-muted);
  151 + }
  152 +
132 /* 统计信息 */ 153 /* 统计信息 */
133 .stats { 154 .stats {
134 display: flex; 155 display: flex;
@@ -444,6 +465,7 @@ @@ -444,6 +465,7 @@
444 刷新 465 刷新
445 </button> 466 </button>
446 467
  468 + <div class="search-group">
447 <div class="search-box"> 469 <div class="search-box">
448 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 470 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
449 <circle cx="11" cy="11" r="8"/> 471 <circle cx="11" cy="11" r="8"/>
@@ -451,6 +473,13 @@ @@ -451,6 +473,13 @@
451 </svg> 473 </svg>
452 <input type="text" id="searchInput" placeholder="搜索节点..."> 474 <input type="text" id="searchInput" placeholder="搜索节点...">
453 </div> 475 </div>
  476 + <div class="search-actions">
  477 + <button class="btn" id="searchBtn" title="搜索">搜索</button>
  478 + <button class="btn" id="searchPrevBtn" title="上一个"></button>
  479 + <button class="btn" id="searchNextBtn" title="下一个"></button>
  480 + <span class="search-status" id="searchStatus">0/0</span>
  481 + </div>
  482 + </div>
454 483
455 <div class="stats" id="statsContainer"> 484 <div class="stats" id="statsContainer">
456 <div class="stat-item"> 485 <div class="stat-item">
@@ -590,6 +619,9 @@ @@ -590,6 +619,9 @@
590 let reportId = {{ report_id | tojson if report_id else 'null' }}; 619 let reportId = {{ report_id | tojson if report_id else 'null' }};
591 let graphReady = false; 620 let graphReady = false;
592 let graphPollTimer = null; 621 let graphPollTimer = null;
  622 + let graphSearchResults = [];
  623 + let graphSearchIndex = -1;
  624 + let graphSearchKeyword = '';
593 const GRAPH_POLL_INTERVAL = 4000; 625 const GRAPH_POLL_INTERVAL = 4000;
594 626
595 // 初始化 627 // 初始化
@@ -620,7 +652,13 @@ @@ -620,7 +652,13 @@
620 allEdges = data.graph.edges; 652 allEdges = data.graph.edges;
621 653
622 updateStats(data.graph.stats); 654 updateStats(data.graph.stats);
  655 + resetGraphSearchState();
623 renderGraph(); 656 renderGraph();
  657 + const input = document.getElementById('searchInput');
  658 + const currentKeyword = (input && input.value) ? input.value : '';
  659 + if (currentKeyword) {
  660 + runGraphSearch(currentKeyword);
  661 + }
624 showLoading(false); 662 showLoading(false);
625 showEmpty(false); 663 showEmpty(false);
626 graphReady = true; 664 graphReady = true;
@@ -769,6 +807,13 @@ @@ -769,6 +807,13 @@
769 network.once('stabilizationIterationsDone', () => { 807 network.once('stabilizationIterationsDone', () => {
770 network.fit({ animation: true }); 808 network.fit({ animation: true });
771 }); 809 });
  810 +
  811 + // 如果已有搜索关键词,重新聚焦当前匹配;否则更新状态显示
  812 + if (graphSearchKeyword) {
  813 + runGraphSearch(graphSearchKeyword);
  814 + } else {
  815 + updateGraphSearchStatus();
  816 + }
772 } 817 }
773 818
774 // 显示节点详情 819 // 显示节点详情
@@ -812,6 +857,76 @@ @@ -812,6 +857,76 @@
812 document.getElementById('nodeDetail').style.display = 'none'; 857 document.getElementById('nodeDetail').style.display = 'none';
813 } 858 }
814 859
  860 + function resetGraphSearchState() {
  861 + graphSearchResults = [];
  862 + graphSearchIndex = -1;
  863 + graphSearchKeyword = '';
  864 + updateGraphSearchStatus();
  865 + }
  866 +
  867 + function updateGraphSearchStatus() {
  868 + const statusEl = document.getElementById('searchStatus');
  869 + const prevBtn = document.getElementById('searchPrevBtn');
  870 + const nextBtn = document.getElementById('searchNextBtn');
  871 + const hasResults = graphSearchResults.length > 0 && graphSearchIndex >= 0;
  872 + if (statusEl) {
  873 + statusEl.textContent = hasResults ? `${graphSearchIndex + 1}/${graphSearchResults.length}` : '0/0';
  874 + statusEl.style.visibility = hasResults ? 'visible' : 'hidden';
  875 + }
  876 + if (prevBtn) prevBtn.disabled = !hasResults;
  877 + if (nextBtn) nextBtn.disabled = !hasResults;
  878 + }
  879 +
  880 + function runGraphSearch(keyword) {
  881 + if (!network) return;
  882 + const term = (keyword || '').trim();
  883 + graphSearchKeyword = term;
  884 +
  885 + if (!term) {
  886 + resetGraphSearchState();
  887 + network.selectNodes([]);
  888 + return;
  889 + }
  890 +
  891 + const lower = term.toLowerCase();
  892 + const nodesDataset = network.body && network.body.data && network.body.data.nodes ? network.body.data.nodes.get() : [];
  893 + graphSearchResults = nodesDataset.filter(n => (n.label || '').toLowerCase().includes(lower));
  894 + graphSearchResults.sort((a, b) => {
  895 + const aLabel = (a.label || '').toLowerCase();
  896 + const bLabel = (b.label || '').toLowerCase();
  897 + if (aLabel === bLabel) {
  898 + return String(a.id).localeCompare(String(b.id), 'zh');
  899 + }
  900 + return aLabel.localeCompare(bLabel, 'zh');
  901 + });
  902 + graphSearchIndex = graphSearchResults.length ? 0 : -1;
  903 +
  904 + if (!graphSearchResults.length) {
  905 + network.selectNodes([]);
  906 + hideNodeDetail();
  907 + updateGraphSearchStatus();
  908 + return;
  909 + }
  910 +
  911 + focusGraphSearchIndex(graphSearchIndex);
  912 + }
  913 +
  914 + function focusGraphSearchIndex(index) {
  915 + if (!network || !graphSearchResults.length) return;
  916 + const total = graphSearchResults.length;
  917 + graphSearchIndex = ((index % total) + total) % total;
  918 + const target = graphSearchResults[graphSearchIndex];
  919 + network.selectNodes([target.id]);
  920 + network.focus(target.id, { animation: true, scale: 1.4 });
  921 + showNodeDetail(target._data || target);
  922 + updateGraphSearchStatus();
  923 + }
  924 +
  925 + function stepGraphSearch(delta) {
  926 + if (!graphSearchResults.length) return;
  927 + focusGraphSearchIndex(graphSearchIndex + delta);
  928 + }
  929 +
815 // 更新统计 930 // 更新统计
816 function updateStats(stats) { 931 function updateStats(stats) {
817 document.getElementById('nodeCount').textContent = stats.total_nodes || 0; 932 document.getElementById('nodeCount').textContent = stats.total_nodes || 0;
@@ -888,22 +1003,32 @@ @@ -888,22 +1003,32 @@
888 }); 1003 });
889 1004
890 // 搜索 1005 // 搜索
891 - document.getElementById('searchInput').addEventListener('input', (e) => {  
892 - const query = e.target.value.toLowerCase();  
893 - if (!query) {  
894 - if (network) network.selectNodes([]);  
895 - return; 1006 + const searchInput = document.getElementById('searchInput');
  1007 + const searchBtn = document.getElementById('searchBtn');
  1008 + const searchPrevBtn = document.getElementById('searchPrevBtn');
  1009 + const searchNextBtn = document.getElementById('searchNextBtn');
  1010 + if (searchInput) {
  1011 + searchInput.addEventListener('keydown', (e) => {
  1012 + if (e.key === 'Enter') {
  1013 + runGraphSearch(searchInput.value);
896 } 1014 }
897 -  
898 - const matchedIds = allNodes  
899 - .filter(n => n.label.toLowerCase().includes(query))  
900 - .map(n => n.id);  
901 -  
902 - if (network && matchedIds.length > 0) {  
903 - network.selectNodes(matchedIds);  
904 - network.focus(matchedIds[0], { animation: true, scale: 1.5 }); 1015 + });
  1016 + searchInput.addEventListener('input', () => {
  1017 + if (!searchInput.value) {
  1018 + resetGraphSearchState();
  1019 + if (network) network.selectNodes([]);
905 } 1020 }
906 }); 1021 });
  1022 + }
  1023 + if (searchBtn) {
  1024 + searchBtn.addEventListener('click', () => runGraphSearch(searchInput ? searchInput.value : ''));
  1025 + }
  1026 + if (searchPrevBtn) {
  1027 + searchPrevBtn.addEventListener('click', () => stepGraphSearch(-1));
  1028 + }
  1029 + if (searchNextBtn) {
  1030 + searchNextBtn.addEventListener('click', () => stepGraphSearch(1));
  1031 + }
907 1032
908 // 筛选 1033 // 筛选
909 document.querySelectorAll('.filter-item input[type="checkbox"]').forEach(cb => { 1034 document.querySelectorAll('.filter-item input[type="checkbox"]').forEach(cb => {