Ver Fonte

fix frontend process board

guantao há 1 mês atrás
pai
commit
734eb39878
1 ficheiros alterados com 208 adições e 63 exclusões
  1. 208 63
      knowhub/frontend/src/pages/Workflows.tsx

+ 208 - 63
knowhub/frontend/src/pages/Workflows.tsx

@@ -1,13 +1,15 @@
 import { useState, useEffect } from 'react';
 import type { FormEvent } from 'react';
-import { Layers, Cpu, CheckCircle2, Target, Search, Activity } from 'lucide-react';
-import { getStrategies, getCapabilities } from '../services/api';
+import { Layers, Cpu, CheckCircle2, Target, Search, Activity, ChevronDown, ChevronRight } from 'lucide-react';
+import { getStrategies, getCapabilities, getTools, getRequirements } from '../services/api';
 import { StatCard } from '../components/common/StatCard';
 import { StatusBadge } from '../components/common/EntityTag';
 
 interface WorkflowStep {
   title?: string;
   description: string;
+  input?: string;
+  tools?: string[];
   capabilities: any[];
 }
 
@@ -44,14 +46,27 @@ function parseSteps(body: string | any[] | object): WorkflowStep[] {
       const parsedCaps = capsArray.map((c: any) => typeof c === 'string' ? { id: c } : c).filter((c: any) => c && (c.capability_id || c.id || c.name || c.capability_name));
       
       let title = phaseObj.phase || phaseObj.name || phaseObj.title || phaseObj.step || phaseObj.module_label || '';
-      let description = phaseObj.description || phaseObj.desc || phaseObj.details || (typeof phase === 'string' ? phase : JSON.stringify(phase));
+      let description = phaseObj.description || phaseObj.desc || phaseObj.details || '';
+      let inputVal = phaseObj.input || phaseObj.inputs || phaseObj.output || '';
+      
+      // If phase is string and title is not set, meaning fallback
+      if (typeof phase === 'string' && !title && !description) {
+         description = phase;
+      }
+      // If we don't have a separated description but we do have a title (phase), that's fine.
+      
+      let toolsRaw = phaseObj.tools || phaseObj.tool || [];
+      const parsedTools = Array.isArray(toolsRaw) ? toolsRaw : (typeof toolsRaw === 'string' ? [toolsRaw] : []);
 
       if (typeof title === 'object') title = JSON.stringify(title);
       if (typeof description === 'object') description = JSON.stringify(description);
+      if (typeof inputVal === 'object') inputVal = JSON.stringify(inputVal);
 
       return {
         title: String(title),
         description: String(description),
+        input: String(inputVal),
+        tools: parsedTools,
         capabilities: parsedCaps
       };
     });
@@ -81,6 +96,8 @@ export function Workflows() {
   
   const [strategies, setStrategies] = useState<any[]>(() => getCache()?.strategies || []);
   const [capabilities, setCapabilities] = useState<Record<string, any>>(() => getCache()?.capabilities || {});
+  const [allToolsMap, setAllToolsMap] = useState<Record<string, any>>(() => getCache()?.allToolsMap || {});
+  const [allReqsMap, setAllReqsMap] = useState<Record<string, any>>(() => getCache()?.allReqsMap || {});
   const [isLoading, setIsLoading] = useState(() => !(getCache()?.strategies?.length > 0));
   const [isLoadingMore, setIsLoadingMore] = useState(false);
   const [offset, setOffset] = useState(0);
@@ -88,6 +105,8 @@ export function Workflows() {
   const [totalCount, setTotalCount] = useState<number>(() => getCache()?.totalCount || 0);
   const [activeCount, setActiveCount] = useState<number>(() => getCache()?.activeCount || 0);
   const [searchQuery, setSearchQuery] = useState("");
+  const [expandedReqs, setExpandedReqs] = useState<Record<string, boolean>>({});
+  const [expandedSteps, setExpandedSteps] = useState<Record<string, boolean>>({});
   const LIMIT = 50;
   
   const loadStrategies = (currentOffset: number, isInit = false) => {
@@ -121,13 +140,26 @@ export function Workflows() {
 
   useEffect(() => {
     setIsLoading(true);
-    getCapabilities(1000).then(capRes => {
+    Promise.all([getCapabilities(1000), getTools(1000), getRequirements(1000)]).then(([capRes, toolRes, reqRes]) => {
       const capMap: Record<string, any> = {};
       (capRes.capabilities || capRes.results || (Array.isArray(capRes) ? capRes : [])).forEach((c: any) => {
         capMap[c.capability_id || c.id || c.capability_name || c.name] = c;
       });
       setCapabilities(capMap);
-      saveCache({ capabilities: capMap });
+
+      const tMap: Record<string, any> = {};
+      (toolRes.tools || toolRes.results || (Array.isArray(toolRes) ? toolRes : [])).forEach((t: any) => {
+        tMap[t.id] = t;
+      });
+      setAllToolsMap(tMap);
+
+      const rMap: Record<string, any> = {};
+      (reqRes.requirements || reqRes.results || (Array.isArray(reqRes) ? reqRes : [])).forEach((r: any) => {
+        rMap[r.req_id || r.id] = r;
+      });
+      setAllReqsMap(rMap);
+
+      saveCache({ capabilities: capMap, allToolsMap: tMap, allReqsMap: rMap });
       loadStrategies(0, true);
       
       // Calculate active count without downloading all bodies
@@ -212,72 +244,185 @@ export function Workflows() {
             const stratCaps = strategy.capability_ids || [];
 
           return (
-            <div key={strategy.id} className="bg-white rounded-3xl border border-slate-200 shadow-sm overflow-hidden hover:border-indigo-300 transition-colors duration-300">
-              <div className="p-6 border-b border-slate-100 flex flex-col md:flex-row justify-between items-start gap-4 bg-slate-50/50">
-                <div>
-                   <div className="flex items-center gap-3 mb-2">
-                     <h2 className="text-lg font-black text-slate-900">{strategy.name || '未命名工序'}</h2>
-                     <span className="text-[11px] font-mono text-slate-400 bg-white px-2 py-0.5 rounded border border-slate-100">ID: {strategy.id}</span>
+            <div key={strategy.id} className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden mb-6">
+              <div className="px-5 py-4 border-b border-slate-100 flex flex-col gap-3 bg-slate-50">
+                 <div className="flex items-center justify-between">
+                   <div className="flex items-center gap-3">
+                     <span className="text-sm font-bold text-slate-500 font-mono select-all">{strategy.id}</span>
+                     <h2 className="text-[16px] font-bold text-slate-900">{strategy.name || '未命名工序'}</h2>
                    </div>
-                   <p className="text-sm text-slate-600 leading-relaxed max-w-3xl">
-                     {strategy.description || '无详细描述'}
-                   </p>
-                </div>
-              </div>
+                 </div>
+                 
+                 {strategy.description && (
+                   <p className="text-sm text-slate-600 leading-relaxed max-w-4xl">{strategy.description}</p>
+                 )}
+                     
+                     {(() => {
+                       let bodyObj: any = {};
+                       try {
+                         bodyObj = typeof strategy.body === 'string' ? JSON.parse(strategy.body || '{}') : (strategy.body || {});
+                       } catch { /* ignore */ }
+                       
+                       const evals = bodyObj.coverage_evaluations;
+                       const hasEvals = evals && Object.keys(evals).length > 0;
+                       
+                       return (
+                         <div className="mt-4 border border-slate-100 rounded-xl overflow-hidden bg-slate-50/50">
+                           <div className="px-4 py-2 bg-slate-100/50 border-b border-slate-100 text-[11px] font-bold text-slate-500 uppercase tracking-wider flex items-center gap-2">
+                              <Target size={14} className="text-indigo-400" />
+                              覆盖需求判定
+                           </div>
+                           <div className="divide-y divide-slate-100">
+                           {!hasEvals ? (
+                             <div className="p-4 text-sm text-slate-400 text-center italic">该工序暂无需求判定记录</div>
+                           ) : (
+                             Object.entries(evals).map(([reqId, val]: [string, any]) => {
+                             const score = val?.score ?? 0;
+                             let scoreColor = "text-rose-600";
+                             if (score >= 0.8) scoreColor = "text-emerald-600";
+                             else if (score >= 0.5) scoreColor = "text-amber-600";
+                             
+                             const explainText = val?.explanation || val?.explaination || val?.reason || '';
+                             const reqObj = allReqsMap[reqId];
+                             const reqDescription = reqObj?.description || reqObj?.subject || reqObj?.name || '暂无描述';
+                             const isExpanded = expandedReqs[`${strategy.id}-${reqId}`];
+                             
+                             return (
+                               <div key={reqId} className="flex flex-col bg-white hover:bg-slate-50/50 transition-colors">
+                                 <div 
+                                    className="flex flex-col md:flex-row gap-4 p-4 items-start cursor-pointer group"
+                                    onClick={(e) => {
+                                      e.stopPropagation();
+                                      const key = `${strategy.id}-${reqId}`;
+                                      setExpandedReqs(prev => ({ ...prev, [key]: !prev[key] }));
+                                    }}
+                                 >
+                                   <div className="w-6 shrink-0 flex items-center justify-center text-slate-300 group-hover:text-indigo-400 transition-colors pt-1">
+                                     {isExpanded ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
+                                   </div>
+                                   <div className="w-[120px] shrink-0 pt-0.5">
+                                     <span className="inline-block px-2 py-1 bg-slate-100 text-slate-600 border border-slate-200 rounded text-[11px] font-bold font-mono truncate max-w-full" title={reqId}>
+                                       {reqId}
+                                     </span>
+                                   </div>
+                                   <div className="flex-1 min-w-[200px] pt-1">
+                                     <div className="text-[13px] font-medium text-slate-800 leading-relaxed whitespace-pre-wrap">
+                                       {reqDescription}
+                                     </div>
+                                   </div>
+                                   <div className={`w-[80px] shrink-0 text-right font-mono font-black text-[16px] pt-0.5 ${scoreColor}`}>
+                                     {Number(score).toFixed(2)}
+                                   </div>
+                                 </div>
+                                 {isExpanded && (
+                                   <div className="px-4 pb-4 md:pl-[11.5rem] animate-in fade-in slide-in-from-top-1">
+                                     <div className="p-3 bg-slate-50 rounded-lg border border-slate-100 text-[12px] text-slate-500 leading-relaxed whitespace-pre-wrap">
+                                       {explainText || <span className="italic text-slate-400">暂无判定说明</span>}
+                                     </div>
+                                   </div>
+                                 )}
+                               </div>
+                             );
+                           })
+                           )}
+                           </div>
+                         </div>
+                       );
+                     })()}
+                  </div>
 
-              <div className="p-6 bg-white">
-                <h3 className="font-black text-[13px] text-indigo-900 tracking-wide mb-4 flex items-center gap-2">
-                   <Activity size={16} className="text-indigo-500" />
-                   执行流展开 (Workflow)
-                </h3>
-                
+                  <div className="overflow-x-auto p-5">
                 {steps.length === 0 ? (
                   <div className="text-sm text-slate-400 p-4 bg-slate-50 rounded-xl border border-slate-100 flex items-center gap-2">
                      该工序暂未定义内部执行流
                   </div>
                 ) : (
-                  <div className="space-y-3">
-                    {steps.map((step, idx) => (
-                      <div key={idx} className="group flex gap-4 p-4 bg-slate-50/50 border border-slate-100 rounded-2xl items-stretch hover:bg-indigo-50/30 hover:border-indigo-100 transition-colors">
-                        
-                        <div className="w-8 shrink-0 flex flex-col items-center">
-                           <span className="w-7 h-7 rounded-xl bg-white border border-slate-200 text-indigo-600 font-black text-sm flex items-center justify-center shadow-sm group-hover:border-indigo-300 group-hover:bg-indigo-600 group-hover:text-white transition-all">
-                             {idx + 1}
-                           </span>
-                           {idx !== steps.length - 1 && (
-                             <div className="w-[2px] h-full bg-slate-200 mt-2 mb-[-16px] group-hover:bg-indigo-200 transition-colors"></div>
-                           )}
-                        </div>
-                        
-                        <div className="flex-1 py-1">
-                           {step.title && <div className="font-bold text-slate-800 text-[15px] mb-1.5">{step.title}</div>}
-                           <div className="text-slate-600 text-[13px] leading-relaxed break-words whitespace-pre-wrap">{step.description}</div>
-                        </div>
-                        
-                        <div className="w-1/3 shrink-0 py-1 pl-4 border-l border-slate-200 border-dashed flex flex-col justify-start">
-                           <span className="text-[10px] font-black text-slate-400 mb-2 uppercase tracking-wide">调用的原子能力</span>
-                           {(!step.capabilities || step.capabilities.length === 0) ? (
-                             <span className="text-[11px] text-slate-400">无特定关联能力</span>
-                           ) : (
-                             <div className="flex flex-wrap gap-2">
-                               {step.capabilities.map((capObj: any, i: number) => {
-                                 const capIdStr = typeof capObj === 'string' ? capObj : (capObj?.capability_id || capObj?.id);
-                                 const cap = capabilities[capIdStr] || (typeof capObj === 'object' ? capObj : null);
-                                 const fallbackStr = typeof capObj === 'string' ? capObj : (capObj?.capability_name || capObj?.name || capIdStr);
-                                 return (
-                                   <div key={i} className="flex items-center gap-1.5 bg-white border border-emerald-100 px-2 py-1.5 rounded-lg text-xs hover:border-emerald-400 hover:bg-emerald-50 transition-colors shadow-sm w-full">
-                                     <Cpu size={12} className="text-emerald-500 shrink-0" />
-                                     <span className="text-emerald-900 font-bold truncate">{cap?.capability_name || cap?.name || fallbackStr}</span>
-                                   </div>
-                                 );
-                               })}
+                  <table className="w-full text-left text-sm whitespace-nowrap min-w-[700px]">
+                    <thead className="bg-slate-50 border-b border-slate-100 text-[13px] text-slate-500 font-bold">
+                      <tr>
+                        <th className="px-5 py-3 w-16 text-center">步骤</th>
+                        <th className="px-5 py-3 w-[30%] whitespace-normal">描述</th>
+                        <th className="px-5 py-3 w-[20%] whitespace-normal">工具</th>
+                        <th className="px-5 py-3 w-[30%] whitespace-normal">原子能力分类</th>
+                      </tr>
+                    </thead>
+                    <tbody className="divide-y divide-slate-100/50 text-[13px]">
+                      {steps.map((step, idx) => {
+                         const isStepExpanded = expandedSteps[`${strategy.id}-${idx}`];
+                         return (
+                         <tr 
+                           key={idx} 
+                           className="hover:bg-indigo-50/20 transition-colors cursor-pointer group"
+                           onClick={() => {
+                             const key = `${strategy.id}-${idx}`;
+                             setExpandedSteps(prev => ({...prev, [key]: !prev[key]}));
+                           }}
+                         >
+                           <td className="px-5 py-4 text-center font-black text-slate-400">
+                             <div className="flex items-center gap-2 justify-center">
+                               <div className="text-slate-300 group-hover:text-indigo-400 transition-colors">
+                                 {isStepExpanded ? <ChevronDown size={14}/> : <ChevronRight size={14}/>}
+                               </div>
+                               <span>{idx + 1}</span>
                              </div>
-                           )}
-                        </div>
-                        
-                      </div>
-                    ))}
-                  </div>
+                           </td>
+                           <td className="px-5 py-4 font-medium text-slate-800 whitespace-normal leading-relaxed">
+                              {step.title ? (
+                                <div className="font-bold">{step.title}</div>
+                              ) : (
+                                <div className="font-bold text-slate-500">{(step.description || '').slice(0, 30)}...</div>
+                              )}
+                              
+                              {isStepExpanded && step.description && (
+                                <div className="text-slate-600 font-normal mt-2 whitespace-pre-wrap animate-in fade-in slide-in-from-top-1">
+                                  {step.description}
+                                </div>
+                              )}
+                           </td>
+                           <td className="px-5 py-4 text-slate-600 whitespace-normal">
+                              {(() => {
+                                 const derivedToolIds = new Set<string>();
+                                 if (step.capabilities && step.capabilities.length > 0) {
+                                     step.capabilities.forEach((capObj: any) => {
+                                        const capIdStr = typeof capObj === 'string' ? capObj : (capObj?.capability_id || capObj?.id);
+                                        const cap = capabilities[capIdStr];
+                                        if (cap && cap.tool_ids) {
+                                            cap.tool_ids.forEach((tid: string) => derivedToolIds.add(tid));
+                                        }
+                                     });
+                                 }
+                                 let derivedTools = Array.from(derivedToolIds).map(tid => allToolsMap[tid]?.name || tid);
+                                 if (derivedTools.length === 0 && step.tools && step.tools.length > 0) {
+                                     derivedTools = step.tools;
+                                 }
+                                 return derivedTools.length > 0 ? derivedTools.map((t, i) => (
+                                    <div key={i} className="mb-1">{t}</div>
+                                 )) : <span className="text-slate-400 italic">暂无</span>;
+                              })()}
+                           </td>
+                           <td className="px-5 py-4 whitespace-normal">
+                              {(!step.capabilities || step.capabilities.length === 0) ? (
+                                <span className="text-slate-400 italic">暂无</span>
+                              ) : (
+                                <div className="flex flex-wrap gap-1.5">
+                                  {step.capabilities.map((capObj: any, i: number) => {
+                                      const capIdStr = typeof capObj === 'string' ? capObj : (capObj?.capability_id || capObj?.id);
+                                      const cap = capabilities[capIdStr] || (typeof capObj === 'object' ? capObj : null);
+                                      const capName = cap?.capability_name || cap?.name || capObj?.capability_name || capObj?.name || capIdStr;
+                                      return (
+                                        <span key={i} className="px-2 py-1 bg-indigo-50 text-indigo-600 rounded text-xs font-bold whitespace-nowrap">
+                                          {capName}
+                                        </span>
+                                      );
+                                  })}
+                                </div>
+                              )}
+                           </td>
+                         </tr>
+                       );
+                      })}
+                    </tbody>
+                  </table>
                 )}
               </div>
             </div>