Parcourir la source

feat: update knowhub frontend

elksmmx il y a 1 mois
Parent
commit
ea70b8df15
1 fichiers modifiés avec 168 ajouts et 101 suppressions
  1. 168 101
      knowhub/frontend/src/pages/Dashboard.tsx

+ 168 - 101
knowhub/frontend/src/pages/Dashboard.tsx

@@ -627,7 +627,7 @@ function RelationCard({ type, item, activeId, shouldScrollIntoView = false, sele
 
 // ─── 详情抽屉内容 ──────────────────────────────────────────────────────────────
 
-function DrawerContent({ type, data, dbData, onOpenPost, nodePostsMap }: { type: string; data: any; dbData: any; onOpenPost: (postId: string, post: any) => void; nodePostsMap?: Record<string, string[]> }) {
+function DrawerContent({ type, data, dbData, onOpenPost, nodePostsMap, selectedReqId }: { type: string; data: any; dbData: any; onOpenPost: (postId: string, post: any) => void; nodePostsMap?: Record<string, string[]>; selectedReqId?: string | null }) {
   if (type === 'req') {
     return (
       <RequirementPostsDrawer
@@ -1104,43 +1104,50 @@ function DrawerContent({ type, data, dbData, onOpenPost, nodePostsMap }: { type:
             </div>
           </div>
 
-          {(data.req_associations || []).length > 0 && (
-            <div>
-              <div className="text-xs font-bold text-slate-500 mb-2">关联需求 ({data.req_associations.length})</div>
-              <div className="space-y-3">
-                {(data.req_associations as any[]).map((assoc: any) => {
-                  const trueNodes = (assoc.judgments || []).filter((j: any) => j.represents).map((j: any) => j.node);
-                  const falseNodes = (assoc.judgments || []).filter((j: any) => !j.represents).map((j: any) => j.node);
-                  return (
-                    <div key={assoc.req_id} className="bg-white border border-slate-100 rounded-xl p-3 space-y-2">
-                      <div className="flex items-center gap-2">
-                        <span className="font-mono text-[10px] text-indigo-500 shrink-0">{assoc.req_id}</span>
-                        <span className="text-xs text-slate-700 leading-snug line-clamp-2">{assoc.req_text}</span>
-                      </div>
-                      {assoc.judgments?.length > 0 && (
-                        <div className="flex flex-wrap gap-1">
-                          {(assoc.judgments as any[]).map((j: any) => (
-                            <span key={j.node} title={j.reason} className={cn(
-                              "px-1.5 py-0.5 rounded-full text-[10px] border font-medium cursor-help",
-                              j.represents
-                                ? "bg-emerald-50 text-emerald-700 border-emerald-200"
-                                : "bg-slate-50 text-slate-400 border-slate-200 line-through"
-                            )}>{j.node}</span>
-                          ))}
+          {(() => {
+            const filteredAssocs = selectedReqId 
+              ? (data.req_associations || []).filter((assoc: any) => assoc.req_id === selectedReqId)
+              : (data.req_associations || []);
+
+            if (filteredAssocs.length === 0) {
+              return <div className="text-xs text-slate-400 text-center py-4">暂无关联需求数据</div>;
+            }
+
+            return (
+              <div>
+                <div className="text-xs font-bold text-slate-500 mb-2">关联需求 ({filteredAssocs.length})</div>
+                <div className="space-y-3">
+                  {filteredAssocs.map((assoc: any) => {
+                    const trueNodes = (assoc.judgments || []).filter((j: any) => j.represents).map((j: any) => j.node);
+                    const falseNodes = (assoc.judgments || []).filter((j: any) => !j.represents).map((j: any) => j.node);
+                    return (
+                      <div key={assoc.req_id} className="bg-white border border-slate-100 rounded-xl p-3 space-y-2">
+                        <div className="flex items-center gap-2">
+                          <span className="font-mono text-[10px] text-indigo-500 shrink-0">{assoc.req_id}</span>
+                          <span className="text-xs text-slate-700 leading-snug line-clamp-2">{assoc.req_text}</span>
                         </div>
-                      )}
-                      {trueNodes.length === 0 && falseNodes.length > 0 && (
-                        <div className="text-[10px] text-slate-400">无节点被判断为代表该需求</div>
-                      )}
-                    </div>
-                  );
-                })}
+                        {assoc.judgments?.length > 0 && (
+                          <div className="flex flex-wrap gap-1">
+                            {(assoc.judgments as any[]).map((j: any) => (
+                              <span key={j.node} title={j.reason} className={cn(
+                                "px-1.5 py-0.5 rounded-full text-[10px] border font-medium cursor-help",
+                                j.represents
+                                  ? "bg-emerald-50 text-emerald-700 border-emerald-200"
+                                  : "bg-slate-50 text-slate-400 border-slate-200 line-through"
+                              )}>{j.node}</span>
+                            ))}
+                          </div>
+                        )}
+                        {trueNodes.length === 0 && falseNodes.length > 0 && (
+                          <div className="text-[10px] text-slate-400">无节点被判断为代表该需求</div>
+                        )}
+                      </div>
+                    );
+                  })}
+                </div>
               </div>
-            </div>
-          )}
-          {(data.req_associations || []).length === 0 && (
-            <div className="text-xs text-slate-400 text-center py-4">暂无关联需求数据</div>
-          )}
+            );
+          })()}
         </>
       )}
     </div>
@@ -2417,61 +2424,7 @@ export function Dashboard({ pendingNode, onPendingConsumed }: { pendingNode?: st
     return ids.size > 0 ? ids : null;
   }, [selectedItemset, collectDirectNodeIdsFromNames]);
 
-  const relatedItemsetIds = useMemo((): Set<string> => {
-    const byNode = selectedNode
-      ? new Set<string>(
-          allItemsets
-            .filter((itemset: any) => (itemset.leaf_names || []).some((name: string) => selectedNodeDirectNames?.has(name)))
-            .map((itemset: any) => String(itemset.id))
-        )
-      : null;
-    const byReq = selectedReqId
-      ? new Set<string>(
-          allItemsets
-            .filter((itemset: any) => (itemsetReqIdsMap.get(String(itemset.id)) || new Set<string>()).has(selectedReqId))
-            .map((itemset: any) => String(itemset.id))
-        )
-      : null;
-    const byProc = selectedProcId
-      ? new Set<string>(
-          allItemsets
-            .filter((itemset: any) => {
-              const reqIds = itemsetReqIdsMap.get(String(itemset.id)) || new Set<string>();
-              return Array.from(reqIds).some((reqId) => selectedProcRequirementIds.has(reqId));
-            })
-            .map((itemset: any) => String(itemset.id))
-        )
-      : null;
-    const byCap = selectedCapId
-      ? new Set<string>(
-          allItemsets
-            .filter((itemset: any) => {
-              const reqIds = itemsetReqIdsMap.get(String(itemset.id)) || new Set<string>();
-              return Array.from(reqIds).some((reqId) => selectedCapRequirementIds.has(reqId));
-            })
-            .map((itemset: any) => String(itemset.id))
-        )
-      : null;
-    const byTool = selectedToolId
-      ? new Set<string>(
-          allItemsets
-            .filter((itemset: any) => {
-              const reqIds = itemsetReqIdsMap.get(String(itemset.id)) || new Set<string>();
-              return Array.from(reqIds).some((reqId) => selectedToolRequirementIds.has(reqId));
-            })
-            .map((itemset: any) => String(itemset.id))
-        )
-      : null;
 
-    const activeSets = [byNode, byReq, byProc, byCap, byTool].filter((set): set is Set<string> => !!set);
-    if (activeSets.length === 0) return new Set<string>();
-    const [first, ...rest] = activeSets;
-    const result = new Set<string>();
-    first.forEach((value) => {
-      if (rest.every((set) => set.has(value))) result.add(value);
-    });
-    return result;
-  }, [allItemsets, itemsetReqIdsMap, selectedNode, selectedNodeDirectNames, selectedReqId, selectedProcId, selectedCapId, selectedToolId, selectedProcRequirementIds, selectedCapRequirementIds, selectedToolRequirementIds]);
 
   const patternContextNodeNames = useMemo(() => {
     const names = new Set<string>();
@@ -2659,46 +2612,161 @@ export function Dashboard({ pendingNode, onPendingConsumed }: { pendingNode?: st
     return result;
   };
 
+  const isGlobalConflict = useMemo(() => {
+    if (selectedNode && selectedReqId && !directNodeReqIds.has(selectedReqId)) return true;
+    if (selectedNode && selectedProcId && !directNodeProcIds.has(selectedProcId)) return true;
+    if (selectedNode && selectedCapId && !directNodeCapIds.has(selectedCapId)) return true;
+    if (selectedNode && selectedToolId && !directNodeToolIds.has(selectedToolId)) return true;
+
+    if (selectedReqId && selectedProcId && !selectedReqProcIds.has(selectedProcId)) return true;
+    if (selectedReqId && selectedCapId && !selectedReqCapabilityIds.has(selectedCapId)) return true;
+    if (selectedReqId && selectedToolId && !selectedReqToolIds.has(selectedToolId)) return true;
+    if (selectedReqId && selectedItemsetId && !(itemsetReqIdsMap.get(String(selectedItemsetId)) || new Set()).has(selectedReqId)) return true;
+
+    if (selectedProcId && selectedCapId && !selectedProcCapabilityIds.has(selectedCapId)) return true;
+    if (selectedProcId && selectedToolId) {
+      const tool = visibleData.tools.find(t => t.id === selectedToolId);
+      if (tool && !(tool.capability_ids || []).some((cid: string) => selectedProcCapabilityIds.has(cid))) return true;
+    }
+    if (selectedProcId && selectedItemsetId) {
+      const proc = dbData.procs.find((item) => item.id === selectedProcId);
+      if (proc && !(proc.requirement_ids || []).some((rid: string) => selectedItemsetReqIds?.has(rid))) return true;
+    }
+
+    if (selectedCapId && selectedToolId) {
+      const tool = visibleData.tools.find(t => t.id === selectedToolId);
+      if (tool && !(tool.capability_ids || []).includes(selectedCapId)) return true;
+    }
+    if (selectedCapId && selectedItemsetId) {
+      const cap = allCaps.find((c: any) => c.id === selectedCapId);
+      if (cap && !(cap.requirement_ids || []).some((rid: string) => selectedItemsetReqIds?.has(rid))) return true;
+    }
+
+    if (selectedToolId && selectedItemsetId) {
+      const tool = visibleData.tools.find(t => t.id === selectedToolId);
+      let found = false;
+      (tool?.capability_ids || []).forEach((cid: string) => {
+        const cap = allCaps.find((c: any) => c.id === cid);
+        if (cap && (cap.requirement_ids || []).some((rid: string) => selectedItemsetReqIds?.has(rid))) found = true;
+      });
+      if (!found) return true;
+    }
+
+    return false;
+  }, [
+    selectedNode, selectedReqId, selectedProcId, selectedCapId, selectedToolId, selectedItemsetId,
+    directNodeReqIds, directNodeProcIds, directNodeCapIds, directNodeToolIds,
+    selectedReqProcIds, selectedReqCapabilityIds, selectedReqToolIds, itemsetReqIdsMap,
+    selectedProcCapabilityIds, visibleData.tools, selectedItemsetReqIds, dbData.procs, allCaps
+  ]);
+
+  const relatedItemsetIds = useMemo((): Set<string> => {
+    const byNode = selectedNode
+      ? new Set<string>(
+          allItemsets
+            .filter((itemset: any) => (itemset.leaf_names || []).some((name: string) => selectedNodeDirectNames?.has(name)))
+            .map((itemset: any) => String(itemset.id))
+        )
+      : null;
+    const byReq = selectedReqId
+      ? new Set<string>(
+          allItemsets
+            .filter((itemset: any) => (itemsetReqIdsMap.get(String(itemset.id)) || new Set<string>()).has(selectedReqId))
+            .map((itemset: any) => String(itemset.id))
+        )
+      : null;
+    const byProc = selectedProcId
+      ? new Set<string>(
+          allItemsets
+            .filter((itemset: any) => {
+              const reqIds = itemsetReqIdsMap.get(String(itemset.id)) || new Set<string>();
+              return Array.from(reqIds).some((reqId) => selectedProcRequirementIds.has(reqId));
+            })
+            .map((itemset: any) => String(itemset.id))
+        )
+      : null;
+    const byCap = selectedCapId
+      ? new Set<string>(
+          allItemsets
+            .filter((itemset: any) => {
+              const reqIds = itemsetReqIdsMap.get(String(itemset.id)) || new Set<string>();
+              return Array.from(reqIds).some((reqId) => selectedCapRequirementIds.has(reqId));
+            })
+            .map((itemset: any) => String(itemset.id))
+        )
+      : null;
+    const byTool = selectedToolId
+      ? new Set<string>(
+          allItemsets
+            .filter((itemset: any) => {
+              const reqIds = itemsetReqIdsMap.get(String(itemset.id)) || new Set<string>();
+              return Array.from(reqIds).some((reqId) => selectedToolRequirementIds.has(reqId));
+            })
+            .map((itemset: any) => String(itemset.id))
+        )
+      : null;
+    const byItemset = selectedItemsetId ? new Set<string>([String(selectedItemsetId)]) : null;
+
+    if (isGlobalConflict) return new Set<string>();
+
+    const activeSets = [byNode, byReq, byProc, byCap, byTool, byItemset].filter((set): set is Set<string> => !!set);
+    if (activeSets.length === 0) return new Set<string>();
+    const [first, ...rest] = activeSets;
+    const result = new Set<string>();
+    first.forEach((value) => {
+      if (rest.every((set) => set.has(value))) result.add(value);
+    });
+    return result;
+  }, [isGlobalConflict, allItemsets, itemsetReqIdsMap, selectedNode, selectedNodeDirectNames, selectedReqId, selectedProcId, selectedCapId, selectedToolId, selectedItemsetId, selectedProcRequirementIds, selectedCapRequirementIds, selectedToolRequirementIds]);
+
   const relatedReqIds = useMemo((): Set<string> => {
+    if (isGlobalConflict) return new Set<string>();
     const fromNode = selectedNode ? directNodeReqIds : null;
     const fromProc = selectedProcId ? selectedProcRequirementIds : null;
+    const fromReq = selectedReqId ? new Set<string>([selectedReqId]) : null;
     const fromCap = selectedCapId
       ? new Set<string>(selectedCapRequirementIds)
       : null;
     const fromTool = selectedToolId ? selectedToolRequirementIds : null;
     const fromItemset = selectedItemsetId && selectedItemsetReqIds ? selectedItemsetReqIds : null;
-    return intersectWithAllowed(intersectActiveSets(fromNode, fromProc, fromCap, fromTool, fromItemset), visibleReqIds);
-  }, [directNodeReqIds, visibleReqIds, selectedNode, selectedProcId, selectedProcRequirementIds, selectedCapId, selectedCapRequirementIds, selectedToolId, selectedToolRequirementIds, selectedItemsetId, selectedItemsetReqIds]);
+    return intersectWithAllowed(intersectActiveSets(fromNode, fromProc, fromReq, fromCap, fromTool, fromItemset), visibleReqIds);
+  }, [directNodeReqIds, visibleReqIds, selectedNode, selectedProcId, selectedProcRequirementIds, selectedReqId, selectedCapId, selectedCapRequirementIds, selectedToolId, selectedToolRequirementIds, selectedItemsetId, selectedItemsetReqIds]);
 
   const relatedProcIds = useMemo((): Set<string> => {
+    if (isGlobalConflict) return new Set<string>();
     const fromNode = selectedNode ? directNodeProcIds : null;
     const fromReq = selectedReqId ? selectedReqProcIds : null;
+    const fromProc = selectedProcId ? new Set<string>([selectedProcId]) : null;
     const fromCap = selectedCapId ? selectedCapProcIds : null;
     const fromTool = selectedToolId ? selectedToolProcIds : null;
     const fromItemset = selectedItemsetId && selectedItemsetReqIds
       ? new Set<string>(dbData.procs.filter((workflow) => (workflow.requirement_ids || []).some((reqId: string) => selectedItemsetReqIds.has(reqId))).map((workflow) => workflow.id))
       : null;
-    return intersectWithAllowed(intersectActiveSets(fromNode, fromReq, fromCap, fromTool, fromItemset), visibleProcIds);
-  }, [directNodeProcIds, visibleProcIds, selectedNode, selectedReqId, selectedReqProcIds, selectedCapId, selectedCapProcIds, selectedToolId, selectedToolProcIds, selectedItemsetId, selectedItemsetReqIds, dbData.procs]);
+    return intersectWithAllowed(intersectActiveSets(fromNode, fromReq, fromProc, fromCap, fromTool, fromItemset), visibleProcIds);
+  }, [directNodeProcIds, visibleProcIds, selectedNode, selectedReqId, selectedReqProcIds, selectedProcId, selectedCapId, selectedCapProcIds, selectedToolId, selectedToolProcIds, selectedItemsetId, selectedItemsetReqIds, dbData.procs]);
 
   const relatedCapIds = useMemo((): Set<string> => {
+    if (isGlobalConflict) return new Set<string>();
     const fromNode = selectedNode ? directNodeCapIds : null;
     const fromProc = selectedProcId ? selectedProcCapabilityIds : null;
     const fromReq = selectedReqId ? new Set<string>(selectedReqCapabilityIds) : null;
+    const fromCap = selectedCapId ? new Set<string>([selectedCapId]) : null;
     const fromTool = selectedToolId ? new Set<string>(selectedToolCapabilityIds) : null;
     const fromItemset = selectedItemsetId && selectedItemsetReqIds
       ? new Set<string>(allCaps.filter((cap: any) => (cap.requirement_ids || []).some((reqId: string) => selectedItemsetReqIds.has(reqId))).map((cap: any) => cap.id))
       : null;
-    return intersectWithAllowed(intersectActiveSets(fromNode, fromProc, fromReq, fromTool, fromItemset), visibleCapIds);
-  }, [directNodeCapIds, visibleCapIds, selectedNode, selectedProcId, selectedProcCapabilityIds, selectedReqId, selectedReqCapabilityIds, selectedToolId, selectedToolCapabilityIds, selectedItemsetId, selectedItemsetReqIds, allCaps]);
+    return intersectWithAllowed(intersectActiveSets(fromNode, fromProc, fromReq, fromCap, fromTool, fromItemset), visibleCapIds);
+  }, [directNodeCapIds, visibleCapIds, selectedNode, selectedProcId, selectedProcCapabilityIds, selectedReqId, selectedReqCapabilityIds, selectedCapId, selectedToolId, selectedToolCapabilityIds, selectedItemsetId, selectedItemsetReqIds, allCaps]);
 
   const relatedToolIds = useMemo((): Set<string> => {
+    if (isGlobalConflict) return new Set<string>();
     const fromNode = selectedNode ? directNodeToolIds : null;
     const fromProc = selectedProcId
       ? new Set<string>(visibleData.tools.filter((tool: any) => (tool.capability_ids || []).some((cid: string) => selectedProcCapabilityIds.has(cid))).map((tool: any) => tool.id))
       : null;
     const fromReq = selectedReqId ? selectedReqToolIds : null;
     const fromCap = selectedCapId ? selectedCapToolIds : null;
+    const fromTool = selectedToolId ? new Set<string>([selectedToolId]) : null;
     const fromItemset = selectedItemsetId && selectedItemsetReqIds
       ? new Set<string>(
           visibleData.tools
@@ -2714,10 +2782,11 @@ export function Dashboard({ pendingNode, onPendingConsumed }: { pendingNode?: st
             .map((tool: any) => tool.id)
         )
       : null;
-    return intersectWithAllowed(intersectActiveSets(fromNode, fromProc, fromReq, fromCap, fromItemset), visibleToolIds);
+    return intersectWithAllowed(intersectActiveSets(fromNode, fromProc, fromReq, fromCap, fromTool, fromItemset), visibleToolIds);
   }, [directNodeToolIds, visibleData.tools, visibleToolIds, selectedReqId, selectedReqToolIds, selectedCapId, selectedCapToolIds, selectedProcId, selectedProcCapabilityIds, selectedNode, selectedItemsetId, selectedItemsetReqIds, allCaps]);
 
   const filteredData = useMemo(() => {
+    if (isGlobalConflict) return { reqs: [], caps: [], tools: [] };
     const reqs = visibleData.reqs.filter((req: any) => {
       if (!requirementMatches(req)) return false;
       // 如果有节点选择,需求必须在 relatedReqIds 中
@@ -2838,6 +2907,7 @@ export function Dashboard({ pendingNode, onPendingConsumed }: { pendingNode?: st
   }, [reqPlanBData, allItemsets, itemsetReqIdsMap, selectedReqId, collectDirectNodeIdsFromNames]);
 
   const filteredProcItems = useMemo(() => {
+    if (isGlobalConflict) return [];
     return visibleProcItems.filter((workflow) => {
       if (selectedReqId && !(workflow.requirement_ids || []).includes(selectedReqId)) return false;
       if (selectedProcId && workflow.id !== selectedProcId) return false;
@@ -3596,10 +3666,6 @@ export function Dashboard({ pendingNode, onPendingConsumed }: { pendingNode?: st
           onSelectItemset={(itemsetId) => {
             setSelectedItemsetId(itemsetId);
             if (itemsetId !== null) {
-              setSelectedReqId(null);
-              setSelectedProcId(null);
-              setSelectedCapId(null);
-              setSelectedToolId(null);
               setTreeFocusTrigger(prev => prev + 1);
             }
             setColumnFocusIndex(prev => ({ ...prev, pattern: 0 }));
@@ -3771,6 +3837,7 @@ export function Dashboard({ pendingNode, onPendingConsumed }: { pendingNode?: st
               dbData={{ ...dbData, caps: allCaps }}
               nodePostsMap={drawerItem.nodePostsMap}
               onOpenPost={(postId, post) => setSelectedPostDetail({ postId, post })}
+              selectedReqId={selectedReqId}
             />
           )
         )}