马一丁

Display GraphRAG on the front-end page

@@ -8,6 +8,7 @@ @@ -8,6 +8,7 @@
8 <!-- PDF导出依赖 --> 8 <!-- PDF导出依赖 -->
9 <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script> 9 <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
10 <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script> 10 <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
  11 + <script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script>
11 <style> 12 <style>
12 * { 13 * {
13 margin: 0; 14 margin: 0;
@@ -1485,6 +1486,91 @@ @@ -1485,6 +1486,91 @@
1485 color: #666; 1486 color: #666;
1486 font-size: 14px; 1487 font-size: 14px;
1487 } 1488 }
  1489 +
  1490 + /* 知识图谱迷你面板 */
  1491 + .graph-mini-panel {
  1492 + border: 2px solid #000000;
  1493 + background-color: #f7f7f7;
  1494 + padding: 10px 12px;
  1495 + width: 280px;
  1496 + align-self: flex-start;
  1497 + display: none;
  1498 + box-shadow: 4px 4px 0 #0000001a;
  1499 + }
  1500 +
  1501 + .graph-mini-panel.collapsed .graph-mini-body {
  1502 + display: none;
  1503 + }
  1504 +
  1505 + .graph-mini-header {
  1506 + display: flex;
  1507 + align-items: center;
  1508 + justify-content: space-between;
  1509 + gap: 8px;
  1510 + margin-bottom: 8px;
  1511 + }
  1512 +
  1513 + .graph-mini-title {
  1514 + font-weight: bold;
  1515 + font-size: 14px;
  1516 + }
  1517 +
  1518 + .graph-mini-subtitle {
  1519 + font-size: 12px;
  1520 + color: #555555;
  1521 + line-height: 1.2;
  1522 + }
  1523 +
  1524 + .graph-mini-actions {
  1525 + display: flex;
  1526 + gap: 6px;
  1527 + }
  1528 +
  1529 + .graph-mini-button {
  1530 + border: 1px solid #000000;
  1531 + background-color: #ffffff;
  1532 + padding: 6px 10px;
  1533 + cursor: pointer;
  1534 + font-size: 12px;
  1535 + font-weight: bold;
  1536 + transition: background-color 0.2s ease, color 0.2s ease;
  1537 + }
  1538 +
  1539 + .graph-mini-button:hover {
  1540 + background-color: #000000;
  1541 + color: #ffffff;
  1542 + }
  1543 +
  1544 + .graph-mini-body {
  1545 + position: relative;
  1546 + height: 240px;
  1547 + width: 240px;
  1548 + border: 1px dashed #000000;
  1549 + background-color: #ffffff;
  1550 + overflow: hidden;
  1551 + }
  1552 +
  1553 + .graph-mini-placeholder {
  1554 + position: absolute;
  1555 + inset: 0;
  1556 + display: flex;
  1557 + align-items: center;
  1558 + justify-content: center;
  1559 + text-align: center;
  1560 + padding: 8px;
  1561 + font-size: 12px;
  1562 + color: #666666;
  1563 + }
  1564 +
  1565 + .graph-mini-placeholder.error {
  1566 + color: #b42318;
  1567 + }
  1568 +
  1569 + .graph-mini-canvas {
  1570 + height: 100%;
  1571 + width: 100%;
  1572 + display: none;
  1573 + }
1488 </style> 1574 </style>
1489 1575
1490 <!-- PDF中文字体数据 (优化后的子集字体,包含6763个GB2312汉字) --> 1576 <!-- PDF中文字体数据 (优化后的子集字体,包含6763个GB2312汉字) -->
@@ -1648,6 +1734,11 @@ @@ -1648,6 +1734,11 @@
1648 let customTemplate = ''; // 存储用户上传的自定义模板内容 1734 let customTemplate = ''; // 存储用户上传的自定义模板内容
1649 let configValues = {}; 1735 let configValues = {};
1650 let configDirty = false; 1736 let configDirty = false;
  1737 + let graphragEnabled = false;
  1738 + let graphragSettingLoaded = false;
  1739 + let graphMiniNetwork = null;
  1740 + let graphMiniPreferredTaskId = null;
  1741 + let graphMiniLoading = false;
1651 let configAutoRefreshTimer = null; 1742 let configAutoRefreshTimer = null;
1652 let systemStarted = false; 1743 let systemStarted = false;
1653 let systemStarting = false; 1744 let systemStarting = false;
@@ -2038,6 +2129,30 @@ @@ -2038,6 +2129,30 @@
2038 query: 8503 2129 query: 8503
2039 }; 2130 };
2040 2131
  2132 + function syncGraphragFlag(config) {
  2133 + if (!config || !Object.prototype.hasOwnProperty.call(config, 'GRAPHRAG_ENABLED')) {
  2134 + return;
  2135 + }
  2136 + graphragEnabled = String(config.GRAPHRAG_ENABLED).toLowerCase() === 'true';
  2137 + graphragSettingLoaded = true;
  2138 + }
  2139 +
  2140 + async function ensureGraphragSetting(force = false) {
  2141 + if (!force && graphragSettingLoaded) {
  2142 + return graphragEnabled;
  2143 + }
  2144 + try {
  2145 + const response = await fetch(CONFIG_ENDPOINT, { cache: 'no-store' });
  2146 + const data = await response.json();
  2147 + if (data && data.success && data.config) {
  2148 + syncGraphragFlag(data.config);
  2149 + }
  2150 + } catch (error) {
  2151 + console.warn('读取GraphRAG配置失败:', error);
  2152 + }
  2153 + return graphragEnabled;
  2154 + }
  2155 +
2041 const configFieldGroups = [ 2156 const configFieldGroups = [
2042 { 2157 {
2043 title: '数据库连接', 2158 title: '数据库连接',
@@ -2420,6 +2535,7 @@ @@ -2420,6 +2535,7 @@
2420 throw new Error(data.message || '读取配置失败'); 2535 throw new Error(data.message || '读取配置失败');
2421 } 2536 }
2422 configValues = data.config || {}; 2537 configValues = data.config || {};
  2538 + syncGraphragFlag(configValues);
2423 renderConfigForm(configValues); 2539 renderConfigForm(configValues);
2424 configDirty = false; 2540 configDirty = false;
2425 if (messageOverride) { 2541 if (messageOverride) {
@@ -2713,6 +2829,7 @@ @@ -2713,6 +2829,7 @@
2713 throw new Error(data.message || '保存失败'); 2829 throw new Error(data.message || '保存失败');
2714 } 2830 }
2715 configValues = data.config || {}; 2831 configValues = data.config || {};
  2832 + syncGraphragFlag(configValues);
2716 renderConfigForm(configValues); 2833 renderConfigForm(configValues);
2717 configDirty = false; 2834 configDirty = false;
2718 if (silent) { 2835 if (silent) {
@@ -4764,6 +4881,24 @@ function getConsoleContainer() { @@ -4764,6 +4881,24 @@ function getConsoleContainer() {
4764 4881
4765 <!-- 任务进度区域 --> 4882 <!-- 任务进度区域 -->
4766 <div id="taskProgressArea"></div> 4883 <div id="taskProgressArea"></div>
  4884 +
  4885 + <!-- 知识图谱迷你预览(GraphRAG 开启时显示) -->
  4886 + <div class="graph-mini-panel" id="graphMiniPanel">
  4887 + <div class="graph-mini-header">
  4888 + <div>
  4889 + <div class="graph-mini-title">知识图谱</div>
  4890 + <div class="graph-mini-subtitle">GraphRAG 生成概览</div>
  4891 + </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>
  4895 + </div>
  4896 + </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>
  4900 + </div>
  4901 + </div>
4767 4902
4768 <!-- 报告预览区域 --> 4903 <!-- 报告预览区域 -->
4769 <div class="report-preview" id="reportPreview"> 4904 <div class="report-preview" id="reportPreview">
@@ -4775,6 +4910,7 @@ function getConsoleContainer() { @@ -4775,6 +4910,7 @@ function getConsoleContainer() {
4775 4910
4776 reportContent.innerHTML = interfaceHTML; 4911 reportContent.innerHTML = interfaceHTML;
4777 initializeReportControls(); 4912 initializeReportControls();
  4913 + initializeGraphMiniPanel(statusData);
4778 resetReportStreamOutput('等待新的Report任务启动...'); 4914 resetReportStreamOutput('等待新的Report任务启动...');
4779 updateReportStreamStatus('idle'); 4915 updateReportStreamStatus('idle');
4780 4916
@@ -4806,6 +4942,215 @@ function getConsoleContainer() { @@ -4806,6 +4942,215 @@ function getConsoleContainer() {
4806 } 4942 }
4807 } 4943 }
4808 4944
  4945 + async function initializeGraphMiniPanel(statusData) {
  4946 + const panel = document.getElementById('graphMiniPanel');
  4947 + if (!panel) return;
  4948 +
  4949 + const enabled = await ensureGraphragSetting();
  4950 + if (!enabled) {
  4951 + panel.style.display = 'none';
  4952 + return;
  4953 + }
  4954 +
  4955 + panel.style.display = 'block';
  4956 + bindGraphMiniEvents();
  4957 +
  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 + }
  4967 +
  4968 + if (currentTaskStatus === 'running') {
  4969 + setGraphMiniWaiting(currentTaskId);
  4970 + return;
  4971 + }
  4972 +
  4973 + if (panel.classList.contains('collapsed')) {
  4974 + setGraphMiniPlaceholder('展开以查看知识图谱');
  4975 + return;
  4976 + }
  4977 +
  4978 + refreshGraphMini(graphMiniPreferredTaskId, true);
  4979 + }
  4980 +
  4981 + function bindGraphMiniEvents() {
  4982 + const toggleBtn = document.getElementById('graphMiniToggle');
  4983 + const refreshBtn = document.getElementById('graphMiniRefresh');
  4984 +
  4985 + if (toggleBtn && !toggleBtn.dataset.bound) {
  4986 + toggleBtn.dataset.bound = 'true';
  4987 + toggleBtn.addEventListener('click', toggleGraphMiniPanel);
  4988 + }
  4989 +
  4990 + if (refreshBtn && !refreshBtn.dataset.bound) {
  4991 + refreshBtn.dataset.bound = 'true';
  4992 + refreshBtn.addEventListener('click', () => refreshGraphMini(graphMiniPreferredTaskId, true));
  4993 + }
  4994 + }
  4995 +
  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('已折叠,展开以查看知识图谱');
  5008 + }
  5009 + }
  5010 +
  5011 + function setGraphMiniPlaceholder(message, type = '') {
  5012 + const placeholder = document.getElementById('graphMiniPlaceholder');
  5013 + const canvas = document.getElementById('graphMiniCanvas');
  5014 + if (!placeholder || !canvas) return;
  5015 + 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';
  5023 + }
  5024 +
  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;
  5032 + }
  5033 + return data;
  5034 + } catch (error) {
  5035 + console.warn('获取知识图谱失败:', error);
  5036 + return null;
  5037 + }
  5038 + }
  5039 +
  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)) {
  5045 + setGraphMiniPlaceholder('图谱组件未加载,请检查网络后重试', 'error');
  5046 + return;
  5047 + }
  5048 +
  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 + })));
  5056 +
  5057 + const edges = new vis.DataSet((graph.edges || []).map(edge => ({
  5058 + from: edge.from,
  5059 + to: edge.to,
  5060 + label: edge.label || '',
  5061 + arrows: 'to',
  5062 + color: '#444444',
  5063 + font: { align: 'top', size: 10 }
  5064 + })));
  5065 +
  5066 + const options = {
  5067 + height: '100%',
  5068 + width: '100%',
  5069 + interaction: { hover: true, dragView: true, zoomView: true },
  5070 + nodes: {
  5071 + shape: 'dot',
  5072 + borderWidth: 1,
  5073 + font: { size: 12 },
  5074 + scaling: { min: 8, max: 18 }
  5075 + },
  5076 + edges: {
  5077 + smooth: true,
  5078 + color: '#999999',
  5079 + width: 1
  5080 + },
  5081 + physics: {
  5082 + stabilization: true,
  5083 + barnesHut: { avoidOverlap: 0.5, springLength: 80, springConstant: 0.02 }
  5084 + }
  5085 + };
  5086 +
  5087 + if (graphMiniNetwork) {
  5088 + graphMiniNetwork.destroy();
  5089 + }
  5090 +
  5091 + placeholder.style.display = 'none';
  5092 + canvas.style.display = 'block';
  5093 + graphMiniNetwork = new vis.Network(canvas, { nodes, edges }, options);
  5094 + const fitOnce = () => {
  5095 + graphMiniNetwork.fit({ animation: { duration: 300, easing: 'easeInOutQuad' } });
  5096 + graphMiniNetwork.off('stabilizationIterationsDone', fitOnce);
  5097 + };
  5098 + graphMiniNetwork.on('stabilizationIterationsDone', fitOnce);
  5099 + }
  5100 +
  5101 + async function refreshGraphMini(taskId = null, allowFallback = true) {
  5102 + const panel = document.getElementById('graphMiniPanel');
  5103 + if (!panel) return;
  5104 + const enabled = await ensureGraphragSetting();
  5105 + if (!enabled) {
  5106 + panel.style.display = 'none';
  5107 + return;
  5108 + }
  5109 + if (panel.classList.contains('collapsed')) {
  5110 + return;
  5111 + }
  5112 +
  5113 + bindGraphMiniEvents();
  5114 + panel.style.display = 'block';
  5115 +
  5116 + if (taskId) {
  5117 + graphMiniPreferredTaskId = taskId;
  5118 + }
  5119 +
  5120 + if (graphMiniLoading) {
  5121 + return;
  5122 + }
  5123 +
  5124 + const targetTaskId = taskId || graphMiniPreferredTaskId || (lastCompletedReportTask ? lastCompletedReportTask.task_id : null);
  5125 + setGraphMiniPlaceholder('加载知识图谱...');
  5126 + graphMiniLoading = true;
  5127 + try {
  5128 + let data = await fetchGraphData(targetTaskId);
  5129 + if (!data && allowFallback && targetTaskId) {
  5130 + data = await fetchGraphData(null);
  5131 + }
  5132 + if (data && data.graph) {
  5133 + renderGraphMini(data.graph);
  5134 + } else {
  5135 + setGraphMiniPlaceholder('暂未找到知识图谱,请稍后点击刷新重试', 'error');
  5136 + }
  5137 + } finally {
  5138 + graphMiniLoading = false;
  5139 + }
  5140 + }
  5141 +
  5142 + function setGraphMiniWaiting(taskId) {
  5143 + graphMiniPreferredTaskId = taskId || graphMiniPreferredTaskId;
  5144 + ensureGraphragSetting().then(enabled => {
  5145 + if (!enabled) return;
  5146 + const panel = document.getElementById('graphMiniPanel');
  5147 + if (!panel) return;
  5148 + panel.style.display = 'block';
  5149 + bindGraphMiniEvents();
  5150 + setGraphMiniPlaceholder('等待图谱生成...');
  5151 + });
  5152 + }
  5153 +
4809 function initializeReportControls() { 5154 function initializeReportControls() {
4810 const generateButton = document.getElementById('generateReportButton'); 5155 const generateButton = document.getElementById('generateReportButton');
4811 if (generateButton && !generateButton.dataset.bound) { 5156 if (generateButton && !generateButton.dataset.bound) {
@@ -5203,6 +5548,7 @@ function getConsoleContainer() { @@ -5203,6 +5548,7 @@ function getConsoleContainer() {
5203 if (data.success) { 5548 if (data.success) {
5204 reportTaskId = data.task_id; 5549 reportTaskId = data.task_id;
5205 showMessage('报告生成已启动', 'success'); 5550 showMessage('报告生成已启动', 'success');
  5551 + setGraphMiniWaiting(reportTaskId);
5206 5552
5207 // 更新任务状态显示 5553 // 更新任务状态显示
5208 updateTaskProgressStatus({ 5554 updateTaskProgressStatus({
@@ -5292,6 +5638,8 @@ function getConsoleContainer() { @@ -5292,6 +5638,8 @@ function getConsoleContainer() {
5292 if (data.task.status === 'completed') { 5638 if (data.task.status === 'completed') {
5293 stopProgressPolling(); 5639 stopProgressPolling();
5294 showMessage('报告生成完成!', 'success'); 5640 showMessage('报告生成完成!', 'success');
  5641 + graphMiniPreferredTaskId = data.task.task_id;
  5642 + refreshGraphMini(data.task.task_id, true);
5295 5643
5296 // 自动显示报告 5644 // 自动显示报告
5297 viewReport(taskId); 5645 viewReport(taskId);
@@ -5581,11 +5929,13 @@ function getConsoleContainer() { @@ -5581,11 +5929,13 @@ function getConsoleContainer() {
5581 if (eventType === 'status' && task) { 5929 if (eventType === 'status' && task) {
5582 if (task.status === 'running') { 5930 if (task.status === 'running') {
5583 resetReportLogsForNewTask(task.task_id, '收到流式状态事件,已重置日志'); 5931 resetReportLogsForNewTask(task.task_id, '收到流式状态事件,已重置日志');
  5932 + setGraphMiniWaiting(task.task_id);
5584 } 5933 }
5585 updateTaskProgressStatus(task); 5934 updateTaskProgressStatus(task);
5586 reportTaskId = task.status === 'running' ? task.task_id : null; 5935 reportTaskId = task.status === 'running' ? task.task_id : null;
5587 if (task.status === 'completed') { 5936 if (task.status === 'completed') {
5588 lastCompletedReportTask = task; 5937 lastCompletedReportTask = task;
  5938 + graphMiniPreferredTaskId = task.task_id;
5589 setGenerateButtonState(false); 5939 setGenerateButtonState(false);
5590 } else if (task.status === 'running') { 5940 } else if (task.status === 'running') {
5591 setGenerateButtonState(true); 5941 setGenerateButtonState(true);
@@ -5677,6 +6027,8 @@ function getConsoleContainer() { @@ -5677,6 +6027,8 @@ function getConsoleContainer() {
5677 if (task) { 6027 if (task) {
5678 lastCompletedReportTask = task; 6028 lastCompletedReportTask = task;
5679 updateDownloadButtonState(task); 6029 updateDownloadButtonState(task);
  6030 + graphMiniPreferredTaskId = task.task_id;
  6031 + refreshGraphMini(task.task_id, true);
5680 } 6032 }
5681 if (eventData.task_id && !reportAutoPreviewLoaded) { 6033 if (eventData.task_id && !reportAutoPreviewLoaded) {
5682 viewReport(eventData.task_id); 6034 viewReport(eventData.task_id);