aifastdb-devplan 1.5.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/autopilot.d.ts +58 -0
  2. package/dist/autopilot.d.ts.map +1 -0
  3. package/dist/autopilot.js +250 -0
  4. package/dist/autopilot.js.map +1 -0
  5. package/dist/dev-plan-document-store.d.ts +2 -0
  6. package/dist/dev-plan-document-store.d.ts.map +1 -1
  7. package/dist/dev-plan-document-store.js +3 -0
  8. package/dist/dev-plan-document-store.js.map +1 -1
  9. package/dist/dev-plan-factory.d.ts +69 -3
  10. package/dist/dev-plan-factory.d.ts.map +1 -1
  11. package/dist/dev-plan-factory.js +111 -18
  12. package/dist/dev-plan-factory.js.map +1 -1
  13. package/dist/dev-plan-graph-store.d.ts +15 -0
  14. package/dist/dev-plan-graph-store.d.ts.map +1 -1
  15. package/dist/dev-plan-graph-store.js +57 -2
  16. package/dist/dev-plan-graph-store.js.map +1 -1
  17. package/dist/index.d.ts +3 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +14 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/mcp-server/index.d.ts +3 -0
  22. package/dist/mcp-server/index.d.ts.map +1 -1
  23. package/dist/mcp-server/index.js +278 -4
  24. package/dist/mcp-server/index.js.map +1 -1
  25. package/dist/types.d.ts +72 -0
  26. package/dist/types.d.ts.map +1 -1
  27. package/dist/types.js +9 -1
  28. package/dist/types.js.map +1 -1
  29. package/dist/visualize/graph-canvas/api-compat.d.ts +20 -0
  30. package/dist/visualize/graph-canvas/api-compat.d.ts.map +1 -0
  31. package/dist/visualize/graph-canvas/api-compat.js +334 -0
  32. package/dist/visualize/graph-canvas/api-compat.js.map +1 -0
  33. package/dist/visualize/graph-canvas/clusterer.d.ts +16 -0
  34. package/dist/visualize/graph-canvas/clusterer.d.ts.map +1 -0
  35. package/dist/visualize/graph-canvas/clusterer.js +460 -0
  36. package/dist/visualize/graph-canvas/clusterer.js.map +1 -0
  37. package/dist/visualize/graph-canvas/core.d.ts +11 -0
  38. package/dist/visualize/graph-canvas/core.d.ts.map +1 -0
  39. package/dist/visualize/graph-canvas/core.js +844 -0
  40. package/dist/visualize/graph-canvas/core.js.map +1 -0
  41. package/dist/visualize/graph-canvas/index.d.ts +22 -0
  42. package/dist/visualize/graph-canvas/index.d.ts.map +1 -0
  43. package/dist/visualize/graph-canvas/index.js +69 -0
  44. package/dist/visualize/graph-canvas/index.js.map +1 -0
  45. package/dist/visualize/graph-canvas/interaction.d.ts +13 -0
  46. package/dist/visualize/graph-canvas/interaction.d.ts.map +1 -0
  47. package/dist/visualize/graph-canvas/interaction.js +446 -0
  48. package/dist/visualize/graph-canvas/interaction.js.map +1 -0
  49. package/dist/visualize/graph-canvas/layout-worker.d.ts +17 -0
  50. package/dist/visualize/graph-canvas/layout-worker.d.ts.map +1 -0
  51. package/dist/visualize/graph-canvas/layout-worker.js +541 -0
  52. package/dist/visualize/graph-canvas/layout-worker.js.map +1 -0
  53. package/dist/visualize/graph-canvas/lod.d.ts +10 -0
  54. package/dist/visualize/graph-canvas/lod.d.ts.map +1 -0
  55. package/dist/visualize/graph-canvas/lod.js +111 -0
  56. package/dist/visualize/graph-canvas/lod.js.map +1 -0
  57. package/dist/visualize/graph-canvas/renderer.d.ts +12 -0
  58. package/dist/visualize/graph-canvas/renderer.d.ts.map +1 -0
  59. package/dist/visualize/graph-canvas/renderer.js +682 -0
  60. package/dist/visualize/graph-canvas/renderer.js.map +1 -0
  61. package/dist/visualize/graph-canvas/spatial-index.d.ts +13 -0
  62. package/dist/visualize/graph-canvas/spatial-index.d.ts.map +1 -0
  63. package/dist/visualize/graph-canvas/spatial-index.js +482 -0
  64. package/dist/visualize/graph-canvas/spatial-index.js.map +1 -0
  65. package/dist/visualize/graph-canvas/styles.d.ts +11 -0
  66. package/dist/visualize/graph-canvas/styles.d.ts.map +1 -0
  67. package/dist/visualize/graph-canvas/styles.js +137 -0
  68. package/dist/visualize/graph-canvas/styles.js.map +1 -0
  69. package/dist/visualize/graph-canvas/viewport.d.ts +17 -0
  70. package/dist/visualize/graph-canvas/viewport.d.ts.map +1 -0
  71. package/dist/visualize/graph-canvas/viewport.js +375 -0
  72. package/dist/visualize/graph-canvas/viewport.js.map +1 -0
  73. package/dist/visualize/server.js +619 -6
  74. package/dist/visualize/server.js.map +1 -1
  75. package/dist/visualize/template.d.ts.map +1 -1
  76. package/dist/visualize/template.js +604 -18
  77. package/dist/visualize/template.js.map +1 -1
  78. package/package.json +1 -1
@@ -338,8 +338,50 @@ function getVisualizationHTML(projectName) {
338
338
  .docs-content-body::-webkit-scrollbar { width: 6px; }
339
339
  .docs-content-body::-webkit-scrollbar-track { background: transparent; }
340
340
  .docs-content-body::-webkit-scrollbar-thumb { background: #374151; border-radius: 3px; }
341
- .docs-content-empty { flex: 1; display: flex; align-items: center; justify-content: center; color: #4b5563; font-size: 14px; flex-direction: column; gap: 8px; }
341
+ .docs-content-empty { flex: 1; display: flex; flex-direction: column; min-height: 0; overflow: hidden; }
342
342
  .docs-content-empty .empty-icon { font-size: 48px; opacity: 0.4; }
343
+
344
+ /* RAG Chat UI */
345
+ .docs-chat-container { display: flex; flex-direction: column; flex: 1; min-height: 0; }
346
+ .docs-chat-messages { flex: 1; overflow-y: auto; padding: 20px 28px; scrollbar-width: thin; scrollbar-color: #374151 transparent; }
347
+ .docs-chat-messages::-webkit-scrollbar { width: 6px; }
348
+ .docs-chat-messages::-webkit-scrollbar-track { background: transparent; }
349
+ .docs-chat-messages::-webkit-scrollbar-thumb { background: #374151; border-radius: 3px; }
350
+ .docs-chat-welcome { text-align: center; padding: 60px 40px 30px; color: #6b7280; }
351
+ .docs-chat-welcome .welcome-icon { font-size: 48px; opacity: 0.4; margin-bottom: 12px; }
352
+ .docs-chat-welcome .welcome-title { font-size: 16px; font-weight: 600; color: #9ca3af; margin-bottom: 6px; }
353
+ .docs-chat-welcome .welcome-desc { font-size: 13px; color: #6b7280; line-height: 1.6; }
354
+ .docs-chat-welcome .welcome-tips { margin-top: 20px; display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
355
+ .docs-chat-welcome .tip-chip { font-size: 12px; padding: 6px 14px; border-radius: 16px; background: rgba(99,102,241,0.1); border: 1px solid rgba(99,102,241,0.2); color: #a5b4fc; cursor: pointer; transition: all 0.15s; }
356
+ .docs-chat-welcome .tip-chip:hover { background: rgba(99,102,241,0.2); border-color: rgba(99,102,241,0.4); }
357
+
358
+ .chat-bubble { margin-bottom: 16px; max-width: 90%; animation: chatFadeIn 0.25s ease; }
359
+ .chat-bubble.user { margin-left: auto; }
360
+ .chat-bubble.assistant { margin-right: auto; }
361
+ .chat-bubble-inner { padding: 10px 16px; border-radius: 12px; font-size: 13px; line-height: 1.6; }
362
+ .chat-bubble.user .chat-bubble-inner { background: rgba(99,102,241,0.2); color: #c7d2fe; border-bottom-right-radius: 4px; }
363
+ .chat-bubble.assistant .chat-bubble-inner { background: #1f2937; color: #d1d5db; border: 1px solid #374151; border-bottom-left-radius: 4px; }
364
+ .chat-result-card { margin-top: 8px; padding: 10px 14px; border-radius: 8px; background: rgba(55,65,81,0.4); border: 1px solid #374151; cursor: pointer; transition: all 0.15s; }
365
+ .chat-result-card:hover { background: rgba(99,102,241,0.1); border-color: rgba(99,102,241,0.3); }
366
+ .chat-result-title { font-size: 13px; font-weight: 600; color: #a5b4fc; margin-bottom: 4px; display: flex; align-items: center; gap: 6px; }
367
+ .chat-result-score { font-size: 10px; padding: 1px 6px; border-radius: 4px; background: rgba(16,185,129,0.15); color: #6ee7b7; font-weight: 500; }
368
+ .chat-result-snippet { font-size: 11px; color: #9ca3af; line-height: 1.5; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; }
369
+ .chat-result-meta { font-size: 10px; color: #6b7280; margin-top: 4px; display: flex; gap: 8px; }
370
+ .chat-no-result { color: #6b7280; font-size: 12px; margin-top: 8px; }
371
+ .chat-typing { display: flex; gap: 4px; padding: 12px 16px; }
372
+ .chat-typing-dot { width: 6px; height: 6px; border-radius: 50%; background: #6b7280; animation: chatTyping 1.2s infinite; }
373
+ .chat-typing-dot:nth-child(2) { animation-delay: 0.2s; }
374
+ .chat-typing-dot:nth-child(3) { animation-delay: 0.4s; }
375
+ @keyframes chatTyping { 0%,60%,100% { opacity: 0.3; transform: scale(0.8); } 30% { opacity: 1; transform: scale(1); } }
376
+ @keyframes chatFadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
377
+
378
+ .docs-chat-input-wrap { padding: 12px 20px 16px; border-top: 1px solid #374151; flex-shrink: 0; display: flex; gap: 8px; align-items: flex-end; background: #111827; }
379
+ .docs-chat-input { flex: 1; background: #1f2937; border: 1px solid #374151; border-radius: 12px; padding: 10px 16px; color: #e5e7eb; font-size: 13px; outline: none; resize: none; min-height: 20px; max-height: 120px; line-height: 1.5; font-family: inherit; transition: border-color 0.2s; }
380
+ .docs-chat-input:focus { border-color: #6366f1; }
381
+ .docs-chat-input::placeholder { color: #6b7280; }
382
+ .docs-chat-send { width: 36px; height: 36px; border-radius: 10px; border: none; background: #6366f1; color: white; cursor: pointer; display: flex; align-items: center; justify-content: center; flex-shrink: 0; transition: all 0.15s; font-size: 16px; }
383
+ .docs-chat-send:hover { background: #818cf8; }
384
+ .docs-chat-send:disabled { background: #374151; color: #6b7280; cursor: not-allowed; }
343
385
  .docs-related { margin-top: 20px; border-top: 1px solid #374151; padding-top: 16px; }
344
386
  .docs-related-title { font-size: 13px; font-weight: 600; color: #9ca3af; margin-bottom: 10px; display: flex; align-items: center; gap: 6px; }
345
387
  .docs-related-item { display: flex; align-items: center; gap: 8px; padding: 5px 0; font-size: 12px; color: #d1d5db; }
@@ -498,18 +540,45 @@ function getVisualizationHTML(projectName) {
498
540
  <div style="text-align:center;padding:40px;color:#6b7280;font-size:12px;">加载中...</div>
499
541
  </div>
500
542
  </div>
501
- <!-- Right: Document Content -->
543
+ <!-- Right: Document Content / Chat -->
502
544
  <div class="docs-content">
545
+ <!-- RAG Chat (default view) -->
503
546
  <div class="docs-content-empty" id="docsEmptyState">
504
- <div class="empty-icon">📄</div>
505
- <div>选择左侧文档查看内容</div>
547
+ <div class="docs-chat-container">
548
+ <div class="docs-chat-messages" id="docsChatMessages">
549
+ <div class="docs-chat-welcome" id="docsChatWelcome">
550
+ <div class="welcome-icon">🔍</div>
551
+ <div class="welcome-title">文档智能搜索</div>
552
+ <div class="welcome-desc">输入问题,AI 将在文档库中搜索相关内容<br>支持语义搜索,理解你的意图而非仅匹配关键词</div>
553
+ <div class="welcome-tips">
554
+ <span class="tip-chip" onclick="chatSendTip(this)">有多少篇文档?</span>
555
+ <span class="tip-chip" onclick="chatSendTip(this)">项目进度</span>
556
+ <span class="tip-chip" onclick="chatSendTip(this)">有哪些阶段?</span>
557
+ <span class="tip-chip" onclick="chatSendTip(this)">最近更新</span>
558
+ <span class="tip-chip" onclick="chatSendTip(this)">帮助</span>
559
+ </div>
560
+ <div class="welcome-tips" style="margin-top:8px;">
561
+ <span class="tip-chip" onclick="chatSendTip(this)">向量搜索</span>
562
+ <span class="tip-chip" onclick="chatSendTip(this)">aifastdb vs LanceDB</span>
563
+ <span class="tip-chip" onclick="chatSendTip(this)">GPU 加速</span>
564
+ <span class="tip-chip" onclick="chatSendTip(this)">全文搜索</span>
565
+ </div>
566
+ </div>
567
+ </div>
568
+ <div class="docs-chat-input-wrap">
569
+ <textarea class="docs-chat-input" id="docsChatInput" placeholder="发送消息搜索文档数据库..." rows="1" onkeydown="chatInputKeydown(event)" oninput="chatAutoResize(this)"></textarea>
570
+ <button class="docs-chat-send" id="docsChatSend" onclick="chatSend()" title="发送">↑</button>
571
+ </div>
572
+ </div>
506
573
  </div>
574
+ <!-- Document Content View -->
507
575
  <div id="docsContentView" style="display:none;flex-direction:column;flex:1;min-height:0;">
508
576
  <div class="docs-content-header">
509
- <div>
577
+ <div style="flex:1;min-width:0;">
510
578
  <div class="docs-content-title" id="docsContentTitle">文档标题</div>
511
579
  <div class="docs-content-meta" id="docsContentMeta"></div>
512
580
  </div>
581
+ <button style="flex-shrink:0;background:none;border:1px solid #374151;border-radius:6px;padding:4px 10px;color:#9ca3af;font-size:11px;cursor:pointer;transition:all 0.15s;" onmouseover="this.style.borderColor='#6366f1';this.style.color='#a5b4fc'" onmouseout="this.style.borderColor='#374151';this.style.color='#9ca3af'" onclick="backToChat()" title="返回对话搜索">← 返回搜索</button>
513
582
  </div>
514
583
  <div class="docs-content-body" id="docsContentBody">
515
584
  <div class="doc-content" id="docsContentInner"></div>
@@ -623,7 +692,103 @@ function log(msg, ok) {
623
692
  dbg.innerHTML = (ok ? '<span class="ok">✓</span> ' : '<span class="err">✗</span> ') + msg;
624
693
  }
625
694
 
626
- // ========== 动态加载 vis-network ==========
695
+ // ========== 渲染引擎选择: GraphCanvas (高性能) vs vis-network (兼容) ==========
696
+ // URL 参数 ?renderer=vis 可强制使用 vis-network; 默认使用 GraphCanvas
697
+ var RENDERER_ENGINE = 'auto'; // 'auto' | 'graphcanvas' | 'vis'
698
+ (function() {
699
+ var params = new URLSearchParams(window.location.search);
700
+ var r = params.get('renderer');
701
+ if (r === 'vis') RENDERER_ENGINE = 'vis';
702
+ else if (r === 'graphcanvas' || r === 'gc') RENDERER_ENGINE = 'graphcanvas';
703
+ })();
704
+ var USE_GRAPH_CANVAS = false; // set after engine loads
705
+
706
+ // ========== SimpleDataSet — vis.DataSet shim for GraphCanvas mode ==========
707
+ function SimpleDataSet(items) {
708
+ this._data = {};
709
+ this._ids = [];
710
+ if (items) {
711
+ for (var i = 0; i < items.length; i++) {
712
+ var item = items[i];
713
+ this._data[item.id] = item;
714
+ this._ids.push(item.id);
715
+ }
716
+ }
717
+ }
718
+ SimpleDataSet.prototype.get = function(id) {
719
+ if (id === undefined || id === null) {
720
+ // Return all items as array
721
+ var result = [];
722
+ for (var i = 0; i < this._ids.length; i++) result.push(this._data[this._ids[i]]);
723
+ return result;
724
+ }
725
+ return this._data[id] || null;
726
+ };
727
+ SimpleDataSet.prototype.getIds = function() {
728
+ return this._ids.slice();
729
+ };
730
+ SimpleDataSet.prototype.forEach = function(callback) {
731
+ for (var i = 0; i < this._ids.length; i++) {
732
+ callback(this._data[this._ids[i]], this._ids[i]);
733
+ }
734
+ };
735
+ SimpleDataSet.prototype.update = function(itemOrArray) {
736
+ var items = Array.isArray(itemOrArray) ? itemOrArray : [itemOrArray];
737
+ for (var i = 0; i < items.length; i++) {
738
+ var item = items[i];
739
+ if (this._data[item.id]) {
740
+ for (var key in item) {
741
+ if (item.hasOwnProperty(key)) this._data[item.id][key] = item[key];
742
+ }
743
+ }
744
+ }
745
+ };
746
+ SimpleDataSet.prototype.add = function(itemOrArray) {
747
+ var items = Array.isArray(itemOrArray) ? itemOrArray : [itemOrArray];
748
+ for (var i = 0; i < items.length; i++) {
749
+ var item = items[i];
750
+ this._data[item.id] = item;
751
+ if (this._ids.indexOf(item.id) === -1) this._ids.push(item.id);
752
+ }
753
+ };
754
+ SimpleDataSet.prototype.remove = function(idOrArray) {
755
+ var ids = Array.isArray(idOrArray) ? idOrArray : [idOrArray];
756
+ for (var i = 0; i < ids.length; i++) {
757
+ var id = typeof ids[i] === 'object' ? ids[i].id : ids[i];
758
+ delete this._data[id];
759
+ var idx = this._ids.indexOf(id);
760
+ if (idx >= 0) this._ids.splice(idx, 1);
761
+ }
762
+ };
763
+ // ========== 动态加载渲染引擎 ==========
764
+ function loadRenderEngine() {
765
+ if (RENDERER_ENGINE === 'vis') {
766
+ log('强制使用 vis-network 渲染器 (?renderer=vis)', true);
767
+ loadVisNetwork(0);
768
+ return;
769
+ }
770
+
771
+ // Try loading GraphCanvas first (from local server)
772
+ log('正在加载 GraphCanvas 高性能渲染引擎...', true);
773
+ var s = document.createElement('script');
774
+ s.src = '/graph-canvas.js';
775
+ s.onload = function() {
776
+ if (typeof GraphCanvas !== 'undefined' && typeof DevPlanGraph !== 'undefined') {
777
+ log('GraphCanvas 引擎加载成功 ✓', true);
778
+ USE_GRAPH_CANVAS = true;
779
+ startApp();
780
+ } else {
781
+ log('GraphCanvas 加载但对象不完整, 回退到 vis-network', false);
782
+ loadVisNetwork(0);
783
+ }
784
+ };
785
+ s.onerror = function() {
786
+ log('GraphCanvas 加载失败, 回退到 vis-network', false);
787
+ loadVisNetwork(0);
788
+ };
789
+ document.head.appendChild(s);
790
+ }
791
+
627
792
  var VIS_URLS = [
628
793
  'https://unpkg.com/vis-network@9.1.9/standalone/umd/vis-network.min.js',
629
794
  'https://cdn.jsdelivr.net/npm/vis-network@9.1.9/standalone/umd/vis-network.min.js',
@@ -633,16 +798,17 @@ var VIS_URLS = [
633
798
  function loadVisNetwork(index) {
634
799
  if (index >= VIS_URLS.length) {
635
800
  log('所有 CDN 均加载失败,请检查网络连接', false);
636
- document.getElementById('loading').innerHTML = '<div style="text-align:center"><div style="font-size:48px;margin-bottom:16px;">⚠️</div><p style="color:#f87171;">vis-network 库加载失败</p><p style="color:#9ca3af;margin-top:8px;font-size:13px;">所有 CDN 源均不可用,请检查网络连接</p><button class="refresh-btn" onclick="location.reload()" style="margin-top:12px;">刷新页面</button></div>';
801
+ document.getElementById('loading').innerHTML = '<div style="text-align:center"><div style="font-size:48px;margin-bottom:16px;">⚠️</div><p style="color:#f87171;">渲染引擎加载失败</p><p style="color:#9ca3af;margin-top:8px;font-size:13px;">GraphCanvas 和 vis-network CDN 均不可用</p><button class="refresh-btn" onclick="location.reload()" style="margin-top:12px;">刷新页面</button></div>';
637
802
  return;
638
803
  }
639
804
  var url = VIS_URLS[index];
640
- log('尝试加载 CDN #' + (index+1) + ': ' + url.split('/')[2], true);
805
+ log('尝试加载 vis-network CDN #' + (index+1) + ': ' + url.split('/')[2], true);
641
806
  var s = document.createElement('script');
642
807
  s.src = url;
643
808
  s.onload = function() {
644
809
  if (typeof vis !== 'undefined' && vis.Network && vis.DataSet) {
645
810
  log('vis-network 加载成功 (CDN #' + (index+1) + ')', true);
811
+ USE_GRAPH_CANVAS = false;
646
812
  startApp();
647
813
  } else {
648
814
  log('CDN #' + (index+1) + ' 加载但 vis 对象不完整, 尝试下一个', false);
@@ -1175,6 +1341,10 @@ function edgeStyle(edge) {
1175
1341
  }
1176
1342
 
1177
1343
  // ========== Data Loading ==========
1344
+ // ── Phase-8C: Chunked loading configuration ──
1345
+ var CHUNK_SIZE = 5000; // nodes per page
1346
+ var CHUNK_THRESHOLD = 3000; // use chunked loading if total > this
1347
+
1178
1348
  function loadData() {
1179
1349
  document.getElementById('loading').style.display = 'flex';
1180
1350
  log('正在获取图谱数据...', true);
@@ -1191,13 +1361,206 @@ function loadData() {
1191
1361
  allEdges = graphRes.edges || [];
1192
1362
  log('数据获取成功: ' + allNodes.length + ' 节点, ' + allEdges.length + ' 边', true);
1193
1363
  renderStats(progressRes, graphRes);
1194
- renderGraph();
1364
+
1365
+ // Phase-8C: If data is large and GraphCanvas is active, use chunked loading
1366
+ if (USE_GRAPH_CANVAS && allNodes.length > CHUNK_THRESHOLD) {
1367
+ renderGraphChunked();
1368
+ } else {
1369
+ renderGraph();
1370
+ }
1195
1371
  }).catch(function(err) {
1196
1372
  log('数据获取失败: ' + err.message, false);
1197
1373
  document.getElementById('loading').innerHTML = '<div style="text-align:center"><div style="font-size:48px;margin-bottom:16px;">⚠️</div><p style="color:#f87171;">数据加载失败: ' + err.message + '</p><button class="refresh-btn" onclick="loadData()" style="margin-top:12px;">重试</button></div>';
1198
1374
  });
1199
1375
  }
1200
1376
 
1377
+ /**
1378
+ * Phase-8C T8C.3+T8C.4: Chunked progressive rendering for large datasets.
1379
+ * Renders the first CHUNK_SIZE nodes immediately, then loads remaining chunks
1380
+ * in the background using addNodes/addEdges incremental API.
1381
+ */
1382
+ function renderGraphChunked() {
1383
+ try {
1384
+ var container = document.getElementById('graph');
1385
+ var rect = container.getBoundingClientRect();
1386
+ if (rect.height < 50) {
1387
+ container.style.height = (window.innerHeight - 140) + 'px';
1388
+ }
1389
+
1390
+ // Sort nodes: center-priority (closest to centroid first)
1391
+ var cx = 0, cy = 0;
1392
+ for (var i = 0; i < allNodes.length; i++) {
1393
+ cx += (allNodes[i].x || 0);
1394
+ cy += (allNodes[i].y || 0);
1395
+ }
1396
+ if (allNodes.length > 0) { cx /= allNodes.length; cy /= allNodes.length; }
1397
+ var sortedNodes = allNodes.slice().sort(function(a, b) {
1398
+ var da = Math.pow(((a.x||0) - cx), 2) + Math.pow(((a.y||0) - cy), 2);
1399
+ var db = Math.pow(((b.x||0) - cx), 2) + Math.pow(((b.y||0) - cy), 2);
1400
+ return da - db;
1401
+ });
1402
+
1403
+ // First chunk
1404
+ var firstChunkNodes = sortedNodes.slice(0, CHUNK_SIZE);
1405
+ var firstChunkIds = {};
1406
+ for (var i = 0; i < firstChunkNodes.length; i++) firstChunkIds[firstChunkNodes[i].id] = true;
1407
+ var firstChunkEdges = [];
1408
+ for (var i = 0; i < allEdges.length; i++) {
1409
+ if (firstChunkIds[allEdges[i].from] && firstChunkIds[allEdges[i].to]) {
1410
+ firstChunkEdges.push(allEdges[i]);
1411
+ }
1412
+ }
1413
+
1414
+ // Prepare first chunk visible nodes/edges (same transform as renderGraph)
1415
+ var visibleNodes = [];
1416
+ for (var i = 0; i < firstChunkNodes.length; i++) {
1417
+ var n = firstChunkNodes[i];
1418
+ if (hiddenTypes[n.type]) continue;
1419
+ if (isNodeCollapsedByParent(n.id)) continue;
1420
+ var deg = getNodeDegree(n);
1421
+ var s = nodeStyle(n, deg);
1422
+ visibleNodes.push({
1423
+ id: n.id, label: n.label, _origLabel: n.label,
1424
+ title: n.label + ' (连接: ' + deg + ')',
1425
+ shape: s.shape, size: s.size, color: s.color, font: s.font,
1426
+ borderWidth: s.borderWidth, _type: n.type,
1427
+ _props: n.properties || {}, _isParentDoc: isParentDocNode(n),
1428
+ });
1429
+ }
1430
+ var visibleIds = {};
1431
+ for (var i = 0; i < visibleNodes.length; i++) visibleIds[visibleNodes[i].id] = true;
1432
+ var visibleEdges = [];
1433
+ for (var i = 0; i < firstChunkEdges.length; i++) {
1434
+ var e = firstChunkEdges[i];
1435
+ if (!visibleIds[e.from] || !visibleIds[e.to]) continue;
1436
+ var es = edgeStyle(e);
1437
+ visibleEdges.push({
1438
+ id: 'e' + i, from: e.from, to: e.to,
1439
+ width: es.width, _origWidth: es.width,
1440
+ color: es.color, dashes: es.dashes, arrows: es.arrows,
1441
+ _label: e.label, _highlightColor: es._highlightColor || '#9ca3af',
1442
+ });
1443
+ }
1444
+
1445
+ log('分块加载: 首批 ' + visibleNodes.length + '/' + allNodes.length + ' 节点', true);
1446
+
1447
+ if (network) { network.destroy(); network = null; }
1448
+
1449
+ // Create network with first chunk
1450
+ nodesDataSet = new SimpleDataSet(visibleNodes);
1451
+ edgesDataSet = new SimpleDataSet(visibleEdges);
1452
+
1453
+ var networkOptions = {
1454
+ nodes: { borderWidth: 2, shadow: { enabled: true, color: 'rgba(0,0,0,0.3)', size: 5, x: 0, y: 2 } },
1455
+ edges: { smooth: { enabled: true, type: 'continuous', roundness: 0.5 }, shadow: false },
1456
+ physics: { enabled: true, solver: 'forceAtlas2Based',
1457
+ forceAtlas2Based: { gravitationalConstant: -80, centralGravity: 0.015, springLength: 150, springConstant: 0.05, damping: 0.4, avoidOverlap: 0.8 },
1458
+ stabilization: { enabled: true, iterations: 200, updateInterval: 25 }
1459
+ },
1460
+ interaction: { hover: true, tooltipDelay: 100, navigationButtons: false, keyboard: false, zoomView: true, dragView: true },
1461
+ layout: { improvedLayout: false, hierarchical: false }
1462
+ };
1463
+
1464
+ network = new DevPlanGraph(container, { nodes: visibleNodes, edges: visibleEdges }, networkOptions);
1465
+
1466
+ // Show loading indicator with progress
1467
+ document.getElementById('loading').style.display = 'none';
1468
+ log('首批数据已渲染,后台加载剩余 ' + (sortedNodes.length - CHUNK_SIZE) + ' 节点...', true);
1469
+
1470
+ // ── Progressive background loading ──
1471
+ var loadedNodeIds = Object.assign({}, firstChunkIds);
1472
+ var chunkIndex = 1;
1473
+ var totalChunks = Math.ceil(sortedNodes.length / CHUNK_SIZE);
1474
+
1475
+ function loadNextChunk() {
1476
+ var start = chunkIndex * CHUNK_SIZE;
1477
+ var end = Math.min(start + CHUNK_SIZE, sortedNodes.length);
1478
+ if (start >= sortedNodes.length) {
1479
+ log('✅ 全部数据加载完成: ' + allNodes.length + ' 节点, ' + allEdges.length + ' 边', true);
1480
+ return;
1481
+ }
1482
+
1483
+ var chunkNodes = [];
1484
+ for (var i = start; i < end; i++) {
1485
+ var n = sortedNodes[i];
1486
+ if (hiddenTypes[n.type]) continue;
1487
+ if (isNodeCollapsedByParent(n.id)) continue;
1488
+ var deg = getNodeDegree(n);
1489
+ var s = nodeStyle(n, deg);
1490
+ chunkNodes.push({
1491
+ id: n.id, label: n.label, _origLabel: n.label,
1492
+ title: n.label, shape: s.shape, size: s.size,
1493
+ color: s.color, font: s.font, borderWidth: s.borderWidth,
1494
+ _type: n.type, _props: n.properties || {},
1495
+ x: n.x || 0, y: n.y || 0,
1496
+ });
1497
+ loadedNodeIds[n.id] = true;
1498
+ }
1499
+
1500
+ // Edges for this chunk (both endpoints must be loaded)
1501
+ var chunkEdges = [];
1502
+ for (var i = 0; i < allEdges.length; i++) {
1503
+ var e = allEdges[i];
1504
+ if (loadedNodeIds[e.from] && loadedNodeIds[e.to]) {
1505
+ var es = edgeStyle(e);
1506
+ chunkEdges.push({
1507
+ id: 'ec' + chunkIndex + '_' + i, from: e.from, to: e.to,
1508
+ width: es.width, _origWidth: es.width,
1509
+ color: es.color, dashes: es.dashes, arrows: es.arrows,
1510
+ _label: e.label, _highlightColor: es._highlightColor || '#9ca3af',
1511
+ });
1512
+ }
1513
+ }
1514
+
1515
+ // Use incremental API (Phase-8C T8C.5)
1516
+ if (network && network._gc) {
1517
+ network._gc.addNodes(chunkNodes);
1518
+ network._gc.addEdges(chunkEdges);
1519
+ }
1520
+
1521
+ chunkIndex++;
1522
+ var pct = Math.min(100, Math.round(chunkIndex / totalChunks * 100));
1523
+ log('加载进度: ' + pct + '% (' + (chunkIndex * CHUNK_SIZE) + '/' + sortedNodes.length + ')', true);
1524
+
1525
+ // Schedule next chunk (yield to main thread for rendering)
1526
+ if (chunkIndex < totalChunks) {
1527
+ setTimeout(loadNextChunk, 50);
1528
+ } else {
1529
+ log('✅ 全部数据加载完成: ' + Object.keys(loadedNodeIds).length + ' 节点', true);
1530
+ }
1531
+ }
1532
+
1533
+ // Start loading remaining chunks after first render stabilizes
1534
+ network.on('stabilizationIterationsDone', function() {
1535
+ network.setOptions({ physics: { enabled: false } });
1536
+ log('首批渲染稳定,开始后台增量加载...', true);
1537
+ setTimeout(loadNextChunk, 100);
1538
+ });
1539
+
1540
+ // Wire up click handler (same as renderGraph)
1541
+ network.on('click', function(params) {
1542
+ if (params.pointer && params.pointer.canvas) {
1543
+ var hitNodeId = hitTestDocToggleBtn(params.pointer.canvas.x, params.pointer.canvas.y);
1544
+ if (hitNodeId) { toggleDocNodeExpand(hitNodeId); return; }
1545
+ }
1546
+ if (params.nodes.length > 0) {
1547
+ panelHistory = [];
1548
+ currentPanelNodeId = null;
1549
+ highlightConnectedEdges(params.nodes[0]);
1550
+ showPanel(params.nodes[0]);
1551
+ } else {
1552
+ resetAllEdgeColors();
1553
+ closePanel();
1554
+ }
1555
+ });
1556
+
1557
+ } catch (e) {
1558
+ log('分块渲染失败: ' + e.message, false);
1559
+ log('回退到标准渲染模式', true);
1560
+ renderGraph();
1561
+ }
1562
+ }
1563
+
1201
1564
  function renderStats(progress, graph) {
1202
1565
  var bar = document.getElementById('statsBar');
1203
1566
  var pct = progress.overallPercent || 0;
@@ -1265,15 +1628,20 @@ function renderGraph() {
1265
1628
 
1266
1629
  log('可见节点: ' + visibleNodes.length + ', 可见边: ' + visibleEdges.length, true);
1267
1630
 
1268
- nodesDataSet = new vis.DataSet(visibleNodes);
1269
- edgesDataSet = new vis.DataSet(visibleEdges);
1631
+ if (USE_GRAPH_CANVAS) {
1632
+ nodesDataSet = new SimpleDataSet(visibleNodes);
1633
+ edgesDataSet = new SimpleDataSet(visibleEdges);
1634
+ } else {
1635
+ nodesDataSet = new vis.DataSet(visibleNodes);
1636
+ edgesDataSet = new vis.DataSet(visibleEdges);
1637
+ }
1270
1638
 
1271
1639
  if (network) {
1272
1640
  network.destroy();
1273
1641
  network = null;
1274
1642
  }
1275
1643
 
1276
- network = new vis.Network(container, { nodes: nodesDataSet, edges: edgesDataSet }, {
1644
+ var networkOptions = {
1277
1645
  nodes: {
1278
1646
  borderWidth: 2,
1279
1647
  shadow: { enabled: true, color: 'rgba(0,0,0,0.3)', size: 5, x: 0, y: 2 }
@@ -1307,7 +1675,19 @@ function renderGraph() {
1307
1675
  improvedLayout: false,
1308
1676
  hierarchical: false
1309
1677
  }
1310
- });
1678
+ };
1679
+
1680
+ if (USE_GRAPH_CANVAS) {
1681
+ network = new DevPlanGraph(container,
1682
+ { nodes: visibleNodes, edges: visibleEdges },
1683
+ networkOptions
1684
+ );
1685
+ } else {
1686
+ network = new vis.Network(container,
1687
+ { nodes: nodesDataSet, edges: edgesDataSet },
1688
+ networkOptions
1689
+ );
1690
+ }
1311
1691
 
1312
1692
  log('Network 实例已创建, 等待物理稳定化...', true);
1313
1693
 
@@ -1804,6 +2184,18 @@ function fmtTime(ts) {
1804
2184
  return time;
1805
2185
  }
1806
2186
 
2187
+ /** 文档列表用的短日期格式:MM-DD 或 YYYY-MM-DD */
2188
+ function fmtDateShort(ts) {
2189
+ if (!ts) return '';
2190
+ var d = new Date(ts);
2191
+ var m = String(d.getMonth() + 1).padStart(2, '0');
2192
+ var day = String(d.getDate()).padStart(2, '0');
2193
+ if (d.getFullYear() !== new Date().getFullYear()) {
2194
+ return d.getFullYear() + '-' + m + '-' + day;
2195
+ }
2196
+ return m + '-' + day;
2197
+ }
2198
+
1807
2199
  // ========== Phase Expand (Stats page) ==========
1808
2200
  function togglePhaseExpand(el) {
1809
2201
  var wrap = el.closest('.phase-item-wrap');
@@ -2345,7 +2737,11 @@ function updateNodeStyles() {
2345
2737
 
2346
2738
  // ========== App Start ==========
2347
2739
  function startApp() {
2348
- log('vis-network 就绪, 开始加载数据...', true);
2740
+ if (USE_GRAPH_CANVAS) {
2741
+ log('GraphCanvas 引擎就绪, 开始加载数据...', true);
2742
+ } else {
2743
+ log('vis-network 就绪, 开始加载数据...', true);
2744
+ }
2349
2745
  loadData();
2350
2746
  }
2351
2747
 
@@ -2434,6 +2830,39 @@ function renderDocsList(docs) {
2434
2830
  groups[sec].push(d);
2435
2831
  }
2436
2832
 
2833
+ // 每组内按 updatedAt 倒序排列(最新的在上方)
2834
+ for (var gi = 0; gi < groupOrder.length; gi++) {
2835
+ groups[groupOrder[gi]].sort(function(a, b) {
2836
+ var ta = a.updatedAt || 0;
2837
+ var tb = b.updatedAt || 0;
2838
+ return tb - ta; // 降序
2839
+ });
2840
+ }
2841
+
2842
+ // 子文档也按 updatedAt 倒序
2843
+ var parentKeys = Object.keys(childrenMap);
2844
+ for (var pi = 0; pi < parentKeys.length; pi++) {
2845
+ childrenMap[parentKeys[pi]].sort(function(a, b) {
2846
+ var ta = a.updatedAt || 0;
2847
+ var tb = b.updatedAt || 0;
2848
+ return tb - ta;
2849
+ });
2850
+ }
2851
+
2852
+ // 分组按最新文档日期排序(最新的分组在上)
2853
+ groupOrder.sort(function(secA, secB) {
2854
+ var maxA = 0, maxB = 0;
2855
+ var itemsA = groups[secA] || [];
2856
+ var itemsB = groups[secB] || [];
2857
+ for (var k = 0; k < itemsA.length; k++) {
2858
+ if ((itemsA[k].updatedAt || 0) > maxA) maxA = itemsA[k].updatedAt || 0;
2859
+ }
2860
+ for (var k = 0; k < itemsB.length; k++) {
2861
+ if ((itemsB[k].updatedAt || 0) > maxB) maxB = itemsB[k].updatedAt || 0;
2862
+ }
2863
+ return maxB - maxA;
2864
+ });
2865
+
2437
2866
  if (groupOrder.length === 0) {
2438
2867
  list.innerHTML = '<div style="text-align:center;padding:40px;color:#6b7280;font-size:12px;">暂无文档</div>';
2439
2868
  return;
@@ -2492,8 +2921,10 @@ function renderDocItemWithChildren(item, childrenMap, secIcon) {
2492
2921
  html += '<span class="docs-item-text" title="' + escHtml(item.title) + '">' + escHtml(item.title) + '</span>';
2493
2922
  if (hasChildren) {
2494
2923
  html += '<span class="docs-item-sub" style="color:#818cf8;">' + children.length + ' 子文档</span>';
2495
- } else if (item.subSection) {
2496
- html += '<span class="docs-item-sub">' + escHtml(item.subSection) + '</span>';
2924
+ }
2925
+ // 右侧显示日期(替代原来的 subSection 标签)
2926
+ if (item.updatedAt) {
2927
+ html += '<span class="docs-item-sub">' + fmtDateShort(item.updatedAt) + '</span>';
2497
2928
  }
2498
2929
  html += '</div>';
2499
2930
 
@@ -2694,6 +3125,161 @@ function renderDocContent(doc, section, subSection) {
2694
3125
  document.getElementById('docsContentInner').innerHTML = contentHtml;
2695
3126
  }
2696
3127
 
3128
+ // ========== RAG Chat ==========
3129
+ var chatHistory = []; // [{role:'user'|'assistant', content:string, results?:array}]
3130
+ var chatBusy = false;
3131
+
3132
+ /** 点击推荐话题 */
3133
+ function chatSendTip(el) {
3134
+ var input = document.getElementById('docsChatInput');
3135
+ if (input) { input.value = el.textContent; chatSend(); }
3136
+ }
3137
+
3138
+ /** Enter 发送(Shift+Enter 换行) */
3139
+ function chatInputKeydown(e) {
3140
+ if (e.key === 'Enter' && !e.shiftKey) {
3141
+ e.preventDefault();
3142
+ chatSend();
3143
+ }
3144
+ }
3145
+
3146
+ /** 自动调整 textarea 高度 */
3147
+ function chatAutoResize(el) {
3148
+ el.style.height = 'auto';
3149
+ el.style.height = Math.min(el.scrollHeight, 120) + 'px';
3150
+ }
3151
+
3152
+ /** 发送消息并搜索 */
3153
+ function chatSend() {
3154
+ if (chatBusy) return;
3155
+ var input = document.getElementById('docsChatInput');
3156
+ var query = (input.value || '').trim();
3157
+ if (!query) return;
3158
+
3159
+ // 隐藏欢迎信息
3160
+ var welcome = document.getElementById('docsChatWelcome');
3161
+ if (welcome) welcome.style.display = 'none';
3162
+
3163
+ // 添加用户消息
3164
+ chatHistory.push({ role: 'user', content: query });
3165
+ chatRenderBubble('user', query);
3166
+ input.value = '';
3167
+ chatAutoResize(input);
3168
+
3169
+ // 显示加载动画
3170
+ chatBusy = true;
3171
+ document.getElementById('docsChatSend').disabled = true;
3172
+ var loadingId = 'chat-loading-' + Date.now();
3173
+ var msgBox = document.getElementById('docsChatMessages');
3174
+ var loadingHtml = '<div class="chat-bubble assistant" id="' + loadingId + '"><div class="chat-bubble-inner"><div class="chat-typing"><div class="chat-typing-dot"></div><div class="chat-typing-dot"></div><div class="chat-typing-dot"></div></div></div></div>';
3175
+ msgBox.insertAdjacentHTML('beforeend', loadingHtml);
3176
+ msgBox.scrollTop = msgBox.scrollHeight;
3177
+
3178
+ // 调用搜索 API
3179
+ fetch('/api/chat', {
3180
+ method: 'POST',
3181
+ headers: { 'Content-Type': 'application/json' },
3182
+ body: JSON.stringify({ query: query, limit: 5 })
3183
+ }).then(function(r) { return r.json(); }).then(function(data) {
3184
+ // 移除加载动画
3185
+ var loadEl = document.getElementById(loadingId);
3186
+ if (loadEl) loadEl.remove();
3187
+
3188
+ var replyHtml = '';
3189
+
3190
+ if (data.type === 'meta') {
3191
+ // ---- 元信息直接回答 ----
3192
+ replyHtml = chatFormatMarkdown(data.answer || '');
3193
+ } else {
3194
+ // ---- 文档搜索结果 ----
3195
+ var results = data.results || [];
3196
+ if (results.length > 0) {
3197
+ replyHtml += '<div style="margin-bottom:8px;color:#9ca3af;font-size:12px;">找到 <strong style="color:#a5b4fc;">' + results.length + '</strong> 篇相关文档';
3198
+ if (data.mode === 'hybrid') replyHtml += ' <span style="font-size:10px;color:#6b7280;">(语义+字面混合)</span>';
3199
+ else if (data.mode === 'semantic') replyHtml += ' <span style="font-size:10px;color:#6b7280;">(语义搜索)</span>';
3200
+ else replyHtml += ' <span style="font-size:10px;color:#6b7280;">(字面搜索)</span>';
3201
+ replyHtml += '</div>';
3202
+
3203
+ for (var i = 0; i < results.length; i++) {
3204
+ var r = results[i];
3205
+ var docKey = r.section + (r.subSection ? '|' + r.subSection : '');
3206
+ replyHtml += '<div class="chat-result-card" onclick="chatOpenDoc(\\x27' + docKey.replace(/'/g, "\\\\'") + '\\x27)">';
3207
+ replyHtml += '<div class="chat-result-title">';
3208
+ replyHtml += '<span>📄 ' + escHtml(r.title) + '</span>';
3209
+ if (r.score != null) replyHtml += '<span class="chat-result-score">' + r.score.toFixed(3) + '</span>';
3210
+ replyHtml += '</div>';
3211
+ if (r.snippet) replyHtml += '<div class="chat-result-snippet">' + escHtml(r.snippet) + '</div>';
3212
+ var metaParts = [];
3213
+ if (r.section) metaParts.push(r.section);
3214
+ if (r.updatedAt) metaParts.push(fmtDateShort(r.updatedAt));
3215
+ if (r.version) metaParts.push('v' + r.version);
3216
+ if (metaParts.length > 0) replyHtml += '<div class="chat-result-meta">' + metaParts.join(' · ') + '</div>';
3217
+ replyHtml += '</div>';
3218
+ }
3219
+ } else {
3220
+ replyHtml += '<div class="chat-no-result">🤔 未找到高度相关的文档。</div>';
3221
+ replyHtml += '<div style="margin-top:8px;font-size:12px;color:#6b7280;line-height:1.6;">';
3222
+ replyHtml += '建议:<br>';
3223
+ replyHtml += '• 尝试使用更具体的 <strong>关键词</strong>(如 "向量搜索"、"GPU"、"LanceDB")<br>';
3224
+ replyHtml += '• 问项目统计问题(如 "有多少篇文档"、"项目进度"、"有哪些阶段")<br>';
3225
+ replyHtml += '• 输入 <strong>"帮助"</strong> 查看我的全部能力';
3226
+ replyHtml += '</div>';
3227
+ }
3228
+ }
3229
+
3230
+ chatHistory.push({ role: 'assistant', content: replyHtml, results: data.results || [] });
3231
+ chatRenderBubble('assistant', replyHtml, true);
3232
+
3233
+ }).catch(function(err) {
3234
+ var loadEl = document.getElementById(loadingId);
3235
+ if (loadEl) loadEl.remove();
3236
+ chatRenderBubble('assistant', '<span style="color:#f87171;">搜索出错: ' + escHtml(err.message) + '</span>', true);
3237
+ }).finally(function() {
3238
+ chatBusy = false;
3239
+ document.getElementById('docsChatSend').disabled = false;
3240
+ document.getElementById('docsChatInput').focus();
3241
+ });
3242
+ }
3243
+
3244
+ /** 简单 Markdown → HTML 转换(用于元信息回答) */
3245
+ function chatFormatMarkdown(text) {
3246
+ return text
3247
+ .replace(/\\*\\*(.+?)\\*\\*/g, '<strong style="color:#a5b4fc;">$1</strong>')
3248
+ .replace(/\\n/g, '<br>');
3249
+ }
3250
+
3251
+ /** 渲染一条消息气泡 */
3252
+ function chatRenderBubble(role, content, isHtml) {
3253
+ var msgBox = document.getElementById('docsChatMessages');
3254
+ var bubble = document.createElement('div');
3255
+ bubble.className = 'chat-bubble ' + role;
3256
+ var inner = document.createElement('div');
3257
+ inner.className = 'chat-bubble-inner';
3258
+ if (isHtml) { inner.innerHTML = content; }
3259
+ else { inner.textContent = content; }
3260
+ bubble.appendChild(inner);
3261
+ msgBox.appendChild(bubble);
3262
+ msgBox.scrollTop = msgBox.scrollHeight;
3263
+ }
3264
+
3265
+ /** 从聊天结果中点击打开文档 */
3266
+ function chatOpenDoc(docKey) {
3267
+ selectDoc(docKey);
3268
+ }
3269
+
3270
+ /** 返回聊天视图 */
3271
+ function backToChat() {
3272
+ document.getElementById('docsContentView').style.display = 'none';
3273
+ document.getElementById('docsEmptyState').style.display = 'flex';
3274
+ // 取消左侧选中
3275
+ currentDocKey = '';
3276
+ var items = document.querySelectorAll('.docs-item');
3277
+ for (var i = 0; i < items.length; i++) items[i].classList.remove('active');
3278
+ // 聚焦输入框
3279
+ var input = document.getElementById('docsChatInput');
3280
+ if (input) input.focus();
3281
+ }
3282
+
2697
3283
  // ========== Stats Dashboard ==========
2698
3284
  var statsLoaded = false;
2699
3285
 
@@ -2910,8 +3496,8 @@ function phaseItem(task, status, icon) {
2910
3496
  return h;
2911
3497
  }
2912
3498
 
2913
- // ========== Init: 动态加载 vis-network ==========
2914
- loadVisNetwork(0);
3499
+ // ========== Init: 动态加载渲染引擎 ==========
3500
+ loadRenderEngine();
2915
3501
  </script>
2916
3502
  </body>
2917
3503
  </html>`;