|
@@ -1,13 +1,15 @@
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useState, useEffect } from 'react';
|
|
|
import type { FormEvent } 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 { StatCard } from '../components/common/StatCard';
|
|
|
import { StatusBadge } from '../components/common/EntityTag';
|
|
import { StatusBadge } from '../components/common/EntityTag';
|
|
|
|
|
|
|
|
interface WorkflowStep {
|
|
interface WorkflowStep {
|
|
|
title?: string;
|
|
title?: string;
|
|
|
description: string;
|
|
description: string;
|
|
|
|
|
+ input?: string;
|
|
|
|
|
+ tools?: string[];
|
|
|
capabilities: any[];
|
|
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));
|
|
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 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 title === 'object') title = JSON.stringify(title);
|
|
|
if (typeof description === 'object') description = JSON.stringify(description);
|
|
if (typeof description === 'object') description = JSON.stringify(description);
|
|
|
|
|
+ if (typeof inputVal === 'object') inputVal = JSON.stringify(inputVal);
|
|
|
|
|
|
|
|
return {
|
|
return {
|
|
|
title: String(title),
|
|
title: String(title),
|
|
|
description: String(description),
|
|
description: String(description),
|
|
|
|
|
+ input: String(inputVal),
|
|
|
|
|
+ tools: parsedTools,
|
|
|
capabilities: parsedCaps
|
|
capabilities: parsedCaps
|
|
|
};
|
|
};
|
|
|
});
|
|
});
|
|
@@ -81,6 +96,8 @@ export function Workflows() {
|
|
|
|
|
|
|
|
const [strategies, setStrategies] = useState<any[]>(() => getCache()?.strategies || []);
|
|
const [strategies, setStrategies] = useState<any[]>(() => getCache()?.strategies || []);
|
|
|
const [capabilities, setCapabilities] = useState<Record<string, any>>(() => getCache()?.capabilities || {});
|
|
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 [isLoading, setIsLoading] = useState(() => !(getCache()?.strategies?.length > 0));
|
|
|
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
|
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
|
|
const [offset, setOffset] = useState(0);
|
|
const [offset, setOffset] = useState(0);
|
|
@@ -88,6 +105,8 @@ export function Workflows() {
|
|
|
const [totalCount, setTotalCount] = useState<number>(() => getCache()?.totalCount || 0);
|
|
const [totalCount, setTotalCount] = useState<number>(() => getCache()?.totalCount || 0);
|
|
|
const [activeCount, setActiveCount] = useState<number>(() => getCache()?.activeCount || 0);
|
|
const [activeCount, setActiveCount] = useState<number>(() => getCache()?.activeCount || 0);
|
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
|
|
|
+ const [expandedReqs, setExpandedReqs] = useState<Record<string, boolean>>({});
|
|
|
|
|
+ const [expandedSteps, setExpandedSteps] = useState<Record<string, boolean>>({});
|
|
|
const LIMIT = 50;
|
|
const LIMIT = 50;
|
|
|
|
|
|
|
|
const loadStrategies = (currentOffset: number, isInit = false) => {
|
|
const loadStrategies = (currentOffset: number, isInit = false) => {
|
|
@@ -121,13 +140,26 @@ export function Workflows() {
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
setIsLoading(true);
|
|
setIsLoading(true);
|
|
|
- getCapabilities(1000).then(capRes => {
|
|
|
|
|
|
|
+ Promise.all([getCapabilities(1000), getTools(1000), getRequirements(1000)]).then(([capRes, toolRes, reqRes]) => {
|
|
|
const capMap: Record<string, any> = {};
|
|
const capMap: Record<string, any> = {};
|
|
|
(capRes.capabilities || capRes.results || (Array.isArray(capRes) ? capRes : [])).forEach((c: any) => {
|
|
(capRes.capabilities || capRes.results || (Array.isArray(capRes) ? capRes : [])).forEach((c: any) => {
|
|
|
capMap[c.capability_id || c.id || c.capability_name || c.name] = c;
|
|
capMap[c.capability_id || c.id || c.capability_name || c.name] = c;
|
|
|
});
|
|
});
|
|
|
setCapabilities(capMap);
|
|
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);
|
|
loadStrategies(0, true);
|
|
|
|
|
|
|
|
// Calculate active count without downloading all bodies
|
|
// Calculate active count without downloading all bodies
|
|
@@ -212,72 +244,185 @@ export function Workflows() {
|
|
|
const stratCaps = strategy.capability_ids || [];
|
|
const stratCaps = strategy.capability_ids || [];
|
|
|
|
|
|
|
|
return (
|
|
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>
|
|
</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 ? (
|
|
{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 className="text-sm text-slate-400 p-4 bg-slate-50 rounded-xl border border-slate-100 flex items-center gap-2">
|
|
|
该工序暂未定义内部执行流
|
|
该工序暂未定义内部执行流
|
|
|
</div>
|
|
</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>
|
|
|
|
|
- ))}
|
|
|
|
|
- </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>
|
|
|
</div>
|
|
</div>
|