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.
- package/dist/autopilot.d.ts +58 -0
- package/dist/autopilot.d.ts.map +1 -0
- package/dist/autopilot.js +250 -0
- package/dist/autopilot.js.map +1 -0
- package/dist/dev-plan-document-store.d.ts +2 -0
- package/dist/dev-plan-document-store.d.ts.map +1 -1
- package/dist/dev-plan-document-store.js +3 -0
- package/dist/dev-plan-document-store.js.map +1 -1
- package/dist/dev-plan-factory.d.ts +69 -3
- package/dist/dev-plan-factory.d.ts.map +1 -1
- package/dist/dev-plan-factory.js +111 -18
- package/dist/dev-plan-factory.js.map +1 -1
- package/dist/dev-plan-graph-store.d.ts +15 -0
- package/dist/dev-plan-graph-store.d.ts.map +1 -1
- package/dist/dev-plan-graph-store.js +57 -2
- package/dist/dev-plan-graph-store.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-server/index.d.ts +3 -0
- package/dist/mcp-server/index.d.ts.map +1 -1
- package/dist/mcp-server/index.js +278 -4
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/types.d.ts +72 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -1
- package/dist/types.js.map +1 -1
- package/dist/visualize/graph-canvas/api-compat.d.ts +20 -0
- package/dist/visualize/graph-canvas/api-compat.d.ts.map +1 -0
- package/dist/visualize/graph-canvas/api-compat.js +334 -0
- package/dist/visualize/graph-canvas/api-compat.js.map +1 -0
- package/dist/visualize/graph-canvas/clusterer.d.ts +16 -0
- package/dist/visualize/graph-canvas/clusterer.d.ts.map +1 -0
- package/dist/visualize/graph-canvas/clusterer.js +460 -0
- package/dist/visualize/graph-canvas/clusterer.js.map +1 -0
- package/dist/visualize/graph-canvas/core.d.ts +11 -0
- package/dist/visualize/graph-canvas/core.d.ts.map +1 -0
- package/dist/visualize/graph-canvas/core.js +844 -0
- package/dist/visualize/graph-canvas/core.js.map +1 -0
- package/dist/visualize/graph-canvas/index.d.ts +22 -0
- package/dist/visualize/graph-canvas/index.d.ts.map +1 -0
- package/dist/visualize/graph-canvas/index.js +69 -0
- package/dist/visualize/graph-canvas/index.js.map +1 -0
- package/dist/visualize/graph-canvas/interaction.d.ts +13 -0
- package/dist/visualize/graph-canvas/interaction.d.ts.map +1 -0
- package/dist/visualize/graph-canvas/interaction.js +446 -0
- package/dist/visualize/graph-canvas/interaction.js.map +1 -0
- package/dist/visualize/graph-canvas/layout-worker.d.ts +17 -0
- package/dist/visualize/graph-canvas/layout-worker.d.ts.map +1 -0
- package/dist/visualize/graph-canvas/layout-worker.js +541 -0
- package/dist/visualize/graph-canvas/layout-worker.js.map +1 -0
- package/dist/visualize/graph-canvas/lod.d.ts +10 -0
- package/dist/visualize/graph-canvas/lod.d.ts.map +1 -0
- package/dist/visualize/graph-canvas/lod.js +111 -0
- package/dist/visualize/graph-canvas/lod.js.map +1 -0
- package/dist/visualize/graph-canvas/renderer.d.ts +12 -0
- package/dist/visualize/graph-canvas/renderer.d.ts.map +1 -0
- package/dist/visualize/graph-canvas/renderer.js +682 -0
- package/dist/visualize/graph-canvas/renderer.js.map +1 -0
- package/dist/visualize/graph-canvas/spatial-index.d.ts +13 -0
- package/dist/visualize/graph-canvas/spatial-index.d.ts.map +1 -0
- package/dist/visualize/graph-canvas/spatial-index.js +482 -0
- package/dist/visualize/graph-canvas/spatial-index.js.map +1 -0
- package/dist/visualize/graph-canvas/styles.d.ts +11 -0
- package/dist/visualize/graph-canvas/styles.d.ts.map +1 -0
- package/dist/visualize/graph-canvas/styles.js +137 -0
- package/dist/visualize/graph-canvas/styles.js.map +1 -0
- package/dist/visualize/graph-canvas/viewport.d.ts +17 -0
- package/dist/visualize/graph-canvas/viewport.d.ts.map +1 -0
- package/dist/visualize/graph-canvas/viewport.js +375 -0
- package/dist/visualize/graph-canvas/viewport.js.map +1 -0
- package/dist/visualize/server.js +619 -6
- package/dist/visualize/server.js.map +1 -1
- package/dist/visualize/template.d.ts.map +1 -1
- package/dist/visualize/template.js +604 -18
- package/dist/visualize/template.js.map +1 -1
- 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;
|
|
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="
|
|
505
|
-
|
|
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
|
-
// ==========
|
|
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;"
|
|
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
|
-
|
|
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
|
-
|
|
1269
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
2496
|
-
|
|
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:
|
|
2914
|
-
|
|
3499
|
+
// ========== Init: 动态加载渲染引擎 ==========
|
|
3500
|
+
loadRenderEngine();
|
|
2915
3501
|
</script>
|
|
2916
3502
|
</body>
|
|
2917
3503
|
</html>`;
|