|
|
@@ -472,7 +472,7 @@ function CoverageFlowBoard({ data }: { data: any }) {
|
|
|
|
|
|
// ─── 关系列卡片 ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
-function RelationCard({ type, item, activeId, shouldScrollIntoView = false, selectedLeafNames, directMatch = false, dimmed = false, showAllSourceTags = false, metrics, onSingleClick }: {
|
|
|
+function RelationCard({ type, item, activeId, shouldScrollIntoView = false, selectedLeafNames, directMatch = false, dimmed = false, showAllSourceTags = false, metrics, onSingleClick, onSourceNodeClick }: {
|
|
|
type: string;
|
|
|
item: any;
|
|
|
activeId: string | null;
|
|
|
@@ -483,6 +483,7 @@ function RelationCard({ type, item, activeId, shouldScrollIntoView = false, sele
|
|
|
showAllSourceTags?: boolean;
|
|
|
metrics?: { nodeCount: number; reqCount: number; procCount: number; capCount: number; toolCount: number };
|
|
|
onSingleClick: (nodeId: string) => void;
|
|
|
+ onSourceNodeClick?: (nodeName: string) => void;
|
|
|
}) {
|
|
|
const cardRef = useRef<HTMLDivElement | null>(null);
|
|
|
const nodeId = `${type}:${item.id}`;
|
|
|
@@ -581,9 +582,17 @@ function RelationCard({ type, item, activeId, shouldScrollIntoView = false, sele
|
|
|
{sourceNodeTags.map((name: string) => {
|
|
|
const isHighlighted = selectedLeafNames && selectedLeafNames.has(name);
|
|
|
return (
|
|
|
- <span key={name} className={cn(
|
|
|
+ <span key={name}
|
|
|
+ onClick={(e) => {
|
|
|
+ if (onSourceNodeClick) {
|
|
|
+ e.stopPropagation();
|
|
|
+ onSourceNodeClick(name);
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ className={cn(
|
|
|
"text-[9px] px-1.5 py-0.5 rounded-md font-medium truncate max-w-[100px]",
|
|
|
- isHighlighted ? "bg-sky-100 text-sky-700 ring-1 ring-sky-400 font-bold" : cn(tc.tagBg, tc.tagText)
|
|
|
+ isHighlighted ? "bg-sky-100 text-sky-700 ring-1 ring-sky-400 font-bold" : cn(tc.tagBg, tc.tagText),
|
|
|
+ onSourceNodeClick && "cursor-pointer hover:ring-1 hover:ring-indigo-300"
|
|
|
)}>
|
|
|
{name}
|
|
|
</span>
|
|
|
@@ -631,6 +640,148 @@ function DrawerContent({ type, data, dbData, onOpenPost, nodePostsMap }: { type:
|
|
|
|
|
|
const capNameById = new Map((dbData.caps || []).map((cap: any) => [cap.id, cap.name || cap.description || cap.id]));
|
|
|
|
|
|
+ const renderStructuredValue = (value: any, path: string): ReactNode => {
|
|
|
+ if (value === null || value === undefined || value === '') {
|
|
|
+ return <span className="text-slate-400">空</span>;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (Array.isArray(value)) {
|
|
|
+ if (value.length === 0) return <span className="text-slate-400">[]</span>;
|
|
|
+ return (
|
|
|
+ <div className="space-y-2">
|
|
|
+ {value.map((item, index) => (
|
|
|
+ <div key={`${path}-${index}`} className="rounded-lg border border-slate-200 bg-white p-2">
|
|
|
+ <div className="text-[10px] font-mono text-slate-400 mb-1">[{index}]</div>
|
|
|
+ {renderStructuredValue(item, `${path}-${index}`)}
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if (typeof value === 'object') {
|
|
|
+ const entries = Object.entries(value);
|
|
|
+ if (entries.length === 0) return <span className="text-slate-400">{'{}'}</span>;
|
|
|
+ return (
|
|
|
+ <div className="space-y-2">
|
|
|
+ {entries.map(([key, nestedValue]) => (
|
|
|
+ <div key={`${path}-${key}`} className="rounded-lg border border-slate-200 bg-white p-3">
|
|
|
+ <div className="text-[10px] font-bold text-slate-400 mb-1 break-all">{key}</div>
|
|
|
+ {renderStructuredValue(nestedValue, `${path}-${key}`)}
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if (typeof value === 'boolean') {
|
|
|
+ return <span className="text-sm text-slate-700 font-medium">{value ? 'true' : 'false'}</span>;
|
|
|
+ }
|
|
|
+
|
|
|
+ return <div className="text-sm text-slate-700 whitespace-pre-wrap break-words leading-relaxed">{String(value)}</div>;
|
|
|
+ };
|
|
|
+
|
|
|
+ const normalizeList = (value: any): any[] => {
|
|
|
+ if (!value) return [];
|
|
|
+ if (Array.isArray(value)) return value;
|
|
|
+ if (typeof value === 'object') return Object.values(value);
|
|
|
+ return [];
|
|
|
+ };
|
|
|
+
|
|
|
+ const parseWorkflowBody = (rawBody: any, dataItem?: any) => {
|
|
|
+ let parsed: any = {};
|
|
|
+ let parseError = false;
|
|
|
+ try {
|
|
|
+ parsed = typeof rawBody === 'string' ? JSON.parse(rawBody || '{}') : (rawBody || {});
|
|
|
+ } catch (e) {
|
|
|
+ parseError = true;
|
|
|
+ }
|
|
|
+
|
|
|
+ let workflowOutlineRaw: any[] = [];
|
|
|
+ if (dataItem?.fine_steps && Array.isArray(dataItem.fine_steps) && dataItem.fine_steps.length > 0) {
|
|
|
+ workflowOutlineRaw = dataItem.fine_steps;
|
|
|
+ } else if (Array.isArray(parsed)) {
|
|
|
+ workflowOutlineRaw = parsed;
|
|
|
+ } else if (parsed && typeof parsed === 'object') {
|
|
|
+ workflowOutlineRaw =
|
|
|
+ parsed.workflow
|
|
|
+ || parsed.phases
|
|
|
+ || parsed.workflow_outline
|
|
|
+ || parsed.selected_strategy?.workflow_outline
|
|
|
+ || parsed.steps
|
|
|
+ || parsed.process_flow
|
|
|
+ || parsed.flow
|
|
|
+ || [];
|
|
|
+ }
|
|
|
+
|
|
|
+ const workflowOutline = normalizeList(workflowOutlineRaw).map((phase: any, index: number) => {
|
|
|
+ const phaseObj = typeof phase === 'string' ? { name: phase } : (phase || {});
|
|
|
+ const title = phaseObj.phase || phaseObj.name || phaseObj.title || phaseObj.step || phaseObj.module_label || `步骤 ${index + 1}`;
|
|
|
+ const description = phaseObj.description || phaseObj.desc || phaseObj.details || '';
|
|
|
+ const capsRaw = phaseObj.capabilities || phaseObj.capability || phaseObj.capability_ids || [];
|
|
|
+
|
|
|
+ const capabilities = normalizeList(capsRaw).map((c: any) => {
|
|
|
+ if (typeof c === 'string') return { capability_name: c, capability_id: c, is_new: false };
|
|
|
+ return {
|
|
|
+ ...c,
|
|
|
+ capability_name: c.capability_name || c.name,
|
|
|
+ capability_id: c.capability_id || c.id,
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ return {
|
|
|
+ ...phaseObj,
|
|
|
+ __title: title,
|
|
|
+ __description: description,
|
|
|
+ capabilities,
|
|
|
+ __index: index,
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ return { parsed, parseError, workflowOutline };
|
|
|
+ };
|
|
|
+
|
|
|
+ const formatFieldLabel = (key: string) =>
|
|
|
+ key
|
|
|
+ .replace(/_/g, ' ')
|
|
|
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
|
+ .replace(/\s+/g, ' ')
|
|
|
+ .trim();
|
|
|
+
|
|
|
+ const renderCompactFieldValue = (value: any): ReactNode => {
|
|
|
+ if (value === null || value === undefined || value === '') {
|
|
|
+ return <span className="text-slate-400">空</span>;
|
|
|
+ }
|
|
|
+ if (Array.isArray(value)) {
|
|
|
+ if (value.length === 0) return <span className="text-slate-400">[]</span>;
|
|
|
+ return (
|
|
|
+ <div className="flex flex-wrap gap-1.5">
|
|
|
+ {value.map((item, index) => (
|
|
|
+ <span
|
|
|
+ key={`field-${index}-${typeof item === 'object' ? 'obj' : String(item)}`}
|
|
|
+ className="px-2 py-0.5 rounded-full text-[11px] bg-slate-100 text-slate-700 border border-slate-200"
|
|
|
+ >
|
|
|
+ {typeof item === 'object' ? JSON.stringify(item) : String(item)}
|
|
|
+ </span>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+ if (typeof value === 'object') {
|
|
|
+ return (
|
|
|
+ <div className="flex flex-wrap gap-1.5">
|
|
|
+ {Object.entries(value).map(([nestedKey, nestedValue]) => (
|
|
|
+ <span key={nestedKey} className="px-2 py-0.5 rounded-full text-[11px] bg-slate-100 text-slate-700 border border-slate-200">
|
|
|
+ <span className="font-semibold text-slate-500">{formatFieldLabel(nestedKey)}:</span>{' '}
|
|
|
+ {typeof nestedValue === 'object' ? JSON.stringify(nestedValue) : String(nestedValue)}
|
|
|
+ </span>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ );
|
|
|
+ }
|
|
|
+ return <span className="text-sm text-slate-700 whitespace-pre-wrap break-words leading-relaxed">{String(value)}</span>;
|
|
|
+ };
|
|
|
+
|
|
|
return (
|
|
|
<div className="space-y-4">
|
|
|
{/* 主内容 */}
|
|
|
@@ -682,27 +833,29 @@ function DrawerContent({ type, data, dbData, onOpenPost, nodePostsMap }: { type:
|
|
|
<>
|
|
|
<div className="bg-purple-50 p-4 rounded-xl border border-purple-100">
|
|
|
<div className="text-xs font-bold text-purple-600 mb-2">工序说明</div>
|
|
|
- <p className="text-purple-900 text-sm leading-relaxed">{data.description || '暂无说明'}</p>
|
|
|
+ <p className="text-purple-900 text-sm leading-relaxed whitespace-pre-wrap">{data.description || '暂无说明'}</p>
|
|
|
</div>
|
|
|
{data.resource_ids?.length > 0 && (
|
|
|
<StrategyResourcesGrid resourceIds={data.resource_ids} onOpenPost={onOpenPost} />
|
|
|
)}
|
|
|
- <div className="bg-slate-50 p-3 rounded-xl border border-slate-100">
|
|
|
- <div className="text-[10px] text-slate-400 mb-1">工序 ID</div>
|
|
|
- <div className="font-mono text-slate-600 text-[11px] break-all">{data.id}</div>
|
|
|
- </div>
|
|
|
- {data.category && (
|
|
|
+ <div className="grid grid-cols-1 gap-3 md:grid-cols-2">
|
|
|
<div className="bg-slate-50 p-3 rounded-xl border border-slate-100">
|
|
|
- <div className="text-[10px] text-slate-400 mb-1">所属品类</div>
|
|
|
- <div className="text-sm text-slate-700 font-medium">{data.category}</div>
|
|
|
+ <div className="text-[10px] text-slate-400 mb-1">工序 ID</div>
|
|
|
+ <div className="font-mono text-slate-700 text-[11px] break-all">{data.id}</div>
|
|
|
</div>
|
|
|
- )}
|
|
|
+ {data.category && (
|
|
|
+ <div className="bg-slate-50 p-3 rounded-xl border border-slate-100">
|
|
|
+ <div className="text-[10px] text-slate-400 mb-1">所属品类</div>
|
|
|
+ <div className="text-sm text-slate-800 font-semibold">{data.category}</div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
{data.path_labels?.length > 0 && (
|
|
|
<div className="bg-slate-50 p-3 rounded-xl border border-slate-100">
|
|
|
<div className="text-[10px] text-slate-400 mb-2">工序链路</div>
|
|
|
<div className="flex flex-wrap gap-2">
|
|
|
{data.path_labels.map((label: string, idx: number) => (
|
|
|
- <span key={`${data.id}-step-${idx}`} className="text-[11px] px-2 py-1 rounded-md bg-purple-100 text-purple-700 font-medium">
|
|
|
+ <span key={`${data.id}-step-${idx}`} className="text-[11px] px-2.5 py-1 rounded-lg bg-white text-purple-700 font-semibold border border-purple-200 shadow-sm">
|
|
|
{data.path[idx]} · {label}
|
|
|
</span>
|
|
|
))}
|
|
|
@@ -710,43 +863,124 @@ function DrawerContent({ type, data, dbData, onOpenPost, nodePostsMap }: { type:
|
|
|
</div>
|
|
|
)}
|
|
|
{(() => {
|
|
|
- let fineSteps = [];
|
|
|
- try {
|
|
|
- const parsed = JSON.parse(data.body || '{}');
|
|
|
- fineSteps = (parsed.phases || parsed.selected_strategy?.workflow_outline || []).map((phase: any, index: number) => ({
|
|
|
- module_id: `step-${index + 1}`,
|
|
|
- module_label: phase.phase || phase.description || '',
|
|
|
- capability_ids: (phase.capabilities || []).map((c: any) => c.capability_id).filter(Boolean)
|
|
|
- }));
|
|
|
- } catch (e) {}
|
|
|
- if (fineSteps.length === 0) return null;
|
|
|
+ const { parsed, parseError, workflowOutline } = parseWorkflowBody(data.body, data);
|
|
|
+
|
|
|
+ const source = parsed.selected_strategy?.source || parsed.source;
|
|
|
+ const excludedBodyKeys = new Set([
|
|
|
+ 'workflow',
|
|
|
+ 'process_flow',
|
|
|
+ 'flow',
|
|
|
+ 'phases',
|
|
|
+ 'workflow_outline',
|
|
|
+ 'steps',
|
|
|
+ 'selected_strategy',
|
|
|
+ 'source',
|
|
|
+ 'reasoning',
|
|
|
+ 'highlight_coverage',
|
|
|
+ 'baseline_coverage',
|
|
|
+ 'vs_alternatives',
|
|
|
+ 'uncovered_requirements',
|
|
|
+ ]);
|
|
|
+ const globalExtraFields = Object.entries(parsed || {}).filter(([key]) => !excludedBodyKeys.has(key));
|
|
|
+
|
|
|
return (
|
|
|
- <div className="bg-slate-50 p-3 rounded-xl border border-slate-100">
|
|
|
- <div className="text-[10px] text-slate-400 mb-2">细工序关联能力</div>
|
|
|
- <div className="space-y-2">
|
|
|
- {fineSteps.map((step: any) => (
|
|
|
- <div key={`${data.id}-${step.module_id}`} className="rounded-lg border border-slate-200 bg-white p-3">
|
|
|
- <div className="text-sm font-medium text-slate-800">
|
|
|
- <span className="font-mono text-[11px] text-purple-600 mr-2">{step.module_id}</span>
|
|
|
- <span>{step.module_label}</span>
|
|
|
- </div>
|
|
|
- <div className="mt-2 flex flex-wrap gap-2">
|
|
|
- {step.capability_ids?.length > 0 ? (
|
|
|
- step.capability_ids.map((capId: string) => (
|
|
|
- <span key={`${step.module_id}-${capId}`} className="text-[11px] px-2 py-1 rounded-md bg-rose-50 text-rose-700 border border-rose-200 font-medium">
|
|
|
- {capId} · {capNameById.get(capId) || capId}
|
|
|
- </span>
|
|
|
- ))
|
|
|
- ) : (
|
|
|
- <span className="text-[11px] px-2 py-1 rounded-md bg-slate-100 text-slate-500 border border-slate-200">
|
|
|
- 暂无映射能力
|
|
|
- </span>
|
|
|
- )}
|
|
|
- </div>
|
|
|
+ <>
|
|
|
+ {source && (
|
|
|
+ <div className="bg-blue-50 p-3 rounded-xl border border-blue-100">
|
|
|
+ <div className="text-[10px] text-blue-400 mb-1">灵感来源</div>
|
|
|
+ <p className="text-sm text-blue-800 leading-relaxed">{source}</p>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {workflowOutline.length > 0 && (
|
|
|
+ <div className="bg-slate-50 p-3 rounded-xl border border-slate-100">
|
|
|
+ <div className="flex items-center justify-between mb-3">
|
|
|
+ <div className="text-[10px] text-slate-400">工序流程</div>
|
|
|
+ <div className="text-[10px] font-bold text-purple-600">{workflowOutline.length} 步</div>
|
|
|
</div>
|
|
|
- ))}
|
|
|
- </div>
|
|
|
- </div>
|
|
|
+ <div className="space-y-3">
|
|
|
+ {workflowOutline.map((phase: any, index: number) => (
|
|
|
+ <div key={`${data.id}-phase-${index}`} className="rounded-lg border border-slate-200 bg-white p-3">
|
|
|
+ <div className="flex items-start gap-3">
|
|
|
+ <div className="shrink-0 w-6 h-6 rounded-full bg-purple-100 text-purple-700 text-[11px] font-black flex items-center justify-center">
|
|
|
+ {index + 1}
|
|
|
+ </div>
|
|
|
+ <div className="min-w-0 flex-1">
|
|
|
+ <div className="text-sm font-bold text-slate-800 leading-snug mb-1">
|
|
|
+ {phase.__title}
|
|
|
+ </div>
|
|
|
+ {phase.__description && (
|
|
|
+ <p className="text-xs text-slate-600 leading-relaxed whitespace-pre-wrap">{phase.__description}</p>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ {phase.capabilities && phase.capabilities.length > 0 && (
|
|
|
+ <div className="mt-3 pl-9 space-y-2">
|
|
|
+ {phase.capabilities.map((cap: any, capIdx: number) => (
|
|
|
+ <div key={`${data.id}-phase-${index}-cap-${capIdx}`} className="bg-slate-50 p-2 rounded border border-slate-200">
|
|
|
+ <div className="flex items-start gap-2">
|
|
|
+ <span className={cn(
|
|
|
+ "text-[10px] px-1.5 py-0.5 rounded font-bold",
|
|
|
+ cap.is_new ? "bg-green-100 text-green-700" : "bg-indigo-100 text-indigo-700"
|
|
|
+ )}>
|
|
|
+ {cap.is_new ? "新能力" : "关联能力"}
|
|
|
+ </span>
|
|
|
+ <div className="flex-1">
|
|
|
+ <div className="text-xs font-medium text-slate-800">
|
|
|
+ {cap.capability_id && cap.capability_id !== cap.capability_name && (
|
|
|
+ <span className="font-mono text-indigo-600 mr-1">{cap.capability_id}</span>
|
|
|
+ )}
|
|
|
+ {cap.capability_name || cap.capability_id || '未命名能力'}
|
|
|
+ </div>
|
|
|
+ {cap.suggested_tools && cap.suggested_tools.length > 0 && (
|
|
|
+ <div className="mt-1 flex flex-wrap gap-1">
|
|
|
+ {cap.suggested_tools.map((tool: string, toolIdx: number) => (
|
|
|
+ <span key={`tool-${toolIdx}`} className="text-[10px] px-1.5 py-0.5 rounded bg-amber-50 text-amber-700 border border-amber-200">
|
|
|
+ {tool}
|
|
|
+ </span>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {cap.case_references && cap.case_references.length > 0 && (
|
|
|
+ <div className="mt-1 text-[10px] text-slate-500">
|
|
|
+ <span className="font-medium">案例引用:</span>
|
|
|
+ {cap.case_references.join(' · ')}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {!parseError && globalExtraFields.length > 0 && (
|
|
|
+ <div className="bg-slate-50 p-3 rounded-xl border border-slate-100">
|
|
|
+ <div className="text-[10px] text-slate-400 mb-2">流程补充信息</div>
|
|
|
+ <div className="flex flex-wrap gap-2">
|
|
|
+ {globalExtraFields.map(([fieldKey, fieldValue]) => (
|
|
|
+ <div key={`${data.id}-body-field-${fieldKey}`} className="rounded-lg border border-slate-200 bg-white px-3 py-2 min-w-[180px] max-w-full">
|
|
|
+ <div className="text-[10px] font-bold text-slate-400 mb-1">{formatFieldLabel(fieldKey)}</div>
|
|
|
+ {renderCompactFieldValue(fieldValue)}
|
|
|
+ </div>
|
|
|
+ ))}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {parseError && (
|
|
|
+ <div className="bg-slate-50 p-3 rounded-xl border border-slate-100">
|
|
|
+ <div className="text-[10px] text-slate-400 mb-2">原始 Body</div>
|
|
|
+ <pre className="text-[12px] text-slate-700 whitespace-pre-wrap break-words leading-relaxed bg-white rounded-lg border border-slate-200 p-3">
|
|
|
+ {typeof data.body === 'string'
|
|
|
+ ? (data.body || '无 body')
|
|
|
+ : JSON.stringify(data.body ?? {}, null, 2)}
|
|
|
+ </pre>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ </>
|
|
|
);
|
|
|
})()}
|
|
|
{data.requirement_texts?.length > 0 && (
|
|
|
@@ -763,8 +997,7 @@ function DrawerContent({ type, data, dbData, onOpenPost, nodePostsMap }: { type:
|
|
|
</div>
|
|
|
)}
|
|
|
{(() => {
|
|
|
- let parsed: any = {};
|
|
|
- try { parsed = JSON.parse(data.body || '{}'); } catch(e) {}
|
|
|
+ const { parsed } = parseWorkflowBody(data.body, data);
|
|
|
|
|
|
const rationale = data.rationale || parsed.selected_strategy?.reasoning || parsed.reasoning;
|
|
|
const highlightCoverage = parsed.selected_strategy?.highlight_coverage || parsed.highlight_coverage || [];
|
|
|
@@ -1359,6 +1592,7 @@ function PatternColumn({
|
|
|
itemsets,
|
|
|
eligibleItemsetIds,
|
|
|
contextNodeNames,
|
|
|
+ patternMatchedNodesMap,
|
|
|
nodeRoleByName,
|
|
|
hasAnyFilter,
|
|
|
focusIndex,
|
|
|
@@ -1371,6 +1605,7 @@ function PatternColumn({
|
|
|
itemsets: any[];
|
|
|
eligibleItemsetIds: Set<string>;
|
|
|
contextNodeNames: Set<string>;
|
|
|
+ patternMatchedNodesMap?: Record<string, Set<string>>;
|
|
|
nodeRoleByName: Record<string, 'substance' | 'form' | 'both'>;
|
|
|
hasAnyFilter: boolean;
|
|
|
focusIndex: number;
|
|
|
@@ -1383,16 +1618,27 @@ function PatternColumn({
|
|
|
const displayItemsets = useMemo(() => {
|
|
|
const withMatches = [...itemsets].map(itemset => {
|
|
|
const leafNames: string[] = itemset.leaf_names || [];
|
|
|
- const matchedNodes = contextNodeNames.size > 0 ? leafNames.filter(n => contextNodeNames.has(n)) : [];
|
|
|
+ let matchedNodes: string[] = [];
|
|
|
+ if (patternMatchedNodesMap && patternMatchedNodesMap[String(itemset.id)]) {
|
|
|
+ const specificMatches = patternMatchedNodesMap[String(itemset.id)];
|
|
|
+ matchedNodes = leafNames.filter(n => specificMatches.has(n));
|
|
|
+ } else {
|
|
|
+ matchedNodes = contextNodeNames.size > 0 ? leafNames.filter(n => contextNodeNames.has(n)) : [];
|
|
|
+ }
|
|
|
return { ...itemset, matched_nodes: matchedNodes };
|
|
|
});
|
|
|
return withMatches.sort((a, b) => {
|
|
|
const aEligible = eligibleItemsetIds.has(String(a.id)) ? 0 : 1;
|
|
|
const bEligible = eligibleItemsetIds.has(String(b.id)) ? 0 : 1;
|
|
|
if (aEligible !== bEligible) return aEligible - bEligible;
|
|
|
+
|
|
|
+ const aMatchCount = (a.matched_nodes || []).length;
|
|
|
+ const bMatchCount = (b.matched_nodes || []).length;
|
|
|
+ if (aMatchCount !== bMatchCount) return bMatchCount - aMatchCount;
|
|
|
+
|
|
|
return b.absolute_support - a.absolute_support;
|
|
|
});
|
|
|
- }, [contextNodeNames, itemsets, eligibleItemsetIds]);
|
|
|
+ }, [contextNodeNames, patternMatchedNodesMap, itemsets, eligibleItemsetIds]);
|
|
|
|
|
|
const matchedIndices = useMemo(() => {
|
|
|
const indices: number[] = [];
|
|
|
@@ -1503,6 +1749,14 @@ function PatternColumn({
|
|
|
|
|
|
export function Dashboard({ pendingNode, onPendingConsumed }: { pendingNode?: string | null, onPendingConsumed?: () => void }) {
|
|
|
const [treeData, setTreeData] = useState<any>(null);
|
|
|
+ const [reqPlanBData, setReqPlanBData] = useState<any>(null);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ fetch('/requirements_planb.json')
|
|
|
+ .then(res => res.json())
|
|
|
+ .then(data => setReqPlanBData(data))
|
|
|
+ .catch(console.error);
|
|
|
+ }, []);
|
|
|
const [selectedNode, setSelectedNode] = useState<any>(null);
|
|
|
const [nameToNodeMap, setNameToNodeMap] = useState<Record<string, any>>({});
|
|
|
const [idToNodeMap, setIdToNodeMap] = useState<Record<string, any>>({});
|
|
|
@@ -1520,6 +1774,7 @@ export function Dashboard({ pendingNode, onPendingConsumed }: { pendingNode?: st
|
|
|
const [selectedToolId, setSelectedToolId] = useState<string | null>(null);
|
|
|
const [columnFocusIndex, setColumnFocusIndex] = useState<Record<string, number>>({});
|
|
|
const [treeFocusIndex, setTreeFocusIndex] = useState(0);
|
|
|
+ const [treeFocusTrigger, setTreeFocusTrigger] = useState(0);
|
|
|
const [dashboardLoadingText, setDashboardLoadingText] = useState<string | null>('开始初始化底座...');
|
|
|
const [flowBoardExpanded, setFlowBoardExpanded] = useState(false);
|
|
|
const [allItemsets, setAllItemsets] = useState<any[]>([]);
|
|
|
@@ -2104,6 +2359,22 @@ export function Dashboard({ pendingNode, onPendingConsumed }: { pendingNode?: st
|
|
|
|
|
|
const itemsetReqIdsMap = useMemo(() => {
|
|
|
const map = new Map<string, Set<string>>();
|
|
|
+
|
|
|
+ if (reqPlanBData && reqPlanBData.requirements) {
|
|
|
+ allItemsets.forEach((itemset: any) => {
|
|
|
+ map.set(String(itemset.id), new Set<string>());
|
|
|
+ });
|
|
|
+ reqPlanBData.requirements.forEach((req: any) => {
|
|
|
+ const reqId = req.requirement_id;
|
|
|
+ (req.patterns || []).forEach((pattern: any) => {
|
|
|
+ const patternId = String(pattern.pattern_id);
|
|
|
+ if (!map.has(patternId)) map.set(patternId, new Set<string>());
|
|
|
+ map.get(patternId)!.add(reqId);
|
|
|
+ });
|
|
|
+ });
|
|
|
+ return map;
|
|
|
+ }
|
|
|
+
|
|
|
allItemsets.forEach((itemset: any) => {
|
|
|
const leafNames = new Set<string>((itemset.leaf_names || []).filter(Boolean));
|
|
|
const ids = new Set<string>();
|
|
|
@@ -2123,7 +2394,7 @@ export function Dashboard({ pendingNode, onPendingConsumed }: { pendingNode?: st
|
|
|
map.set(String(itemset.id), ids);
|
|
|
});
|
|
|
return map;
|
|
|
- }, [allItemsets, dbData.reqs]);
|
|
|
+ }, [allItemsets, dbData.reqs, reqPlanBData]);
|
|
|
|
|
|
const selectedItemsetLeafNames = useMemo(() => {
|
|
|
if (!selectedItemset) return null;
|
|
|
@@ -2218,6 +2489,22 @@ export function Dashboard({ pendingNode, onPendingConsumed }: { pendingNode?: st
|
|
|
return names;
|
|
|
}, [selectedNodeDirectNames, selectedReqId, dbData.reqs]);
|
|
|
|
|
|
+ const patternMatchedNodesMap = useMemo(() => {
|
|
|
+ if (!reqPlanBData || !reqPlanBData.requirements || !selectedReqId) return undefined;
|
|
|
+ const reqInfo = reqPlanBData.requirements.find((r: any) => r.requirement_id === selectedReqId);
|
|
|
+ if (!reqInfo) return undefined;
|
|
|
+
|
|
|
+ const map: Record<string, Set<string>> = {};
|
|
|
+ (reqInfo.patterns || []).forEach((pattern: any) => {
|
|
|
+ const set = new Set<string>();
|
|
|
+ (pattern.represents || []).forEach((name: string) => {
|
|
|
+ if (name) set.add(name);
|
|
|
+ });
|
|
|
+ map[String(pattern.pattern_id)] = set;
|
|
|
+ });
|
|
|
+ return map;
|
|
|
+ }, [reqPlanBData, selectedReqId]);
|
|
|
+
|
|
|
const requirementVisible = (req: any) => {
|
|
|
if (onlyCoveredFilter) {
|
|
|
const hasCoveredSourceNode = (req.source_nodes || []).some((sn: any) => {
|
|
|
@@ -2525,17 +2812,30 @@ export function Dashboard({ pendingNode, onPendingConsumed }: { pendingNode?: st
|
|
|
const selectedReqPatternNodeIds = useMemo((): Set<string> | null => {
|
|
|
if (!selectedReqId) return null;
|
|
|
const relatedLeafNames = new Set<string>();
|
|
|
- allItemsets.forEach((itemset: any) => {
|
|
|
- const reqIds = itemsetReqIdsMap.get(String(itemset.id)) || new Set<string>();
|
|
|
- if (!reqIds.has(selectedReqId)) return;
|
|
|
- (itemset.leaf_names || []).forEach((name: string) => {
|
|
|
- if (name) relatedLeafNames.add(name);
|
|
|
+
|
|
|
+ if (reqPlanBData && reqPlanBData.requirements) {
|
|
|
+ const reqInfo = reqPlanBData.requirements.find((r: any) => r.requirement_id === selectedReqId);
|
|
|
+ if (reqInfo) {
|
|
|
+ (reqInfo.patterns || []).forEach((pattern: any) => {
|
|
|
+ (pattern.leaf_names || []).forEach((name: string) => {
|
|
|
+ if (name) relatedLeafNames.add(name);
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ allItemsets.forEach((itemset: any) => {
|
|
|
+ const reqIds = itemsetReqIdsMap.get(String(itemset.id)) || new Set<string>();
|
|
|
+ if (!reqIds.has(selectedReqId)) return;
|
|
|
+ (itemset.leaf_names || []).forEach((name: string) => {
|
|
|
+ if (name) relatedLeafNames.add(name);
|
|
|
+ });
|
|
|
});
|
|
|
- });
|
|
|
+ }
|
|
|
+
|
|
|
if (relatedLeafNames.size === 0) return null;
|
|
|
const ids = collectDirectNodeIdsFromNames(relatedLeafNames);
|
|
|
return ids.size > 0 ? ids : null;
|
|
|
- }, [allItemsets, itemsetReqIdsMap, selectedReqId, collectDirectNodeIdsFromNames]);
|
|
|
+ }, [reqPlanBData, allItemsets, itemsetReqIdsMap, selectedReqId, collectDirectNodeIdsFromNames]);
|
|
|
|
|
|
const filteredProcItems = useMemo(() => {
|
|
|
return visibleProcItems.filter((workflow) => {
|
|
|
@@ -2627,11 +2927,10 @@ export function Dashboard({ pendingNode, onPendingConsumed }: { pendingNode?: st
|
|
|
|
|
|
const collectMatchedNodes = (node: any): any[] => {
|
|
|
const hasChildren = node.children && node.children.length > 0;
|
|
|
- const isSourceNode = selectedReqSourceNodeIds?.has(String(node.id)) ?? false;
|
|
|
const isPatternNode = selectedPatternNodeIds?.has(String(node.id)) ?? false;
|
|
|
const matched: any[] = [];
|
|
|
|
|
|
- if (isSourceNode || isPatternNode) {
|
|
|
+ if (isPatternNode) {
|
|
|
matched.push(node);
|
|
|
}
|
|
|
|
|
|
@@ -3278,6 +3577,7 @@ export function Dashboard({ pendingNode, onPendingConsumed }: { pendingNode?: st
|
|
|
treeFocusIndex={treeFocusIndex}
|
|
|
treeMatchedCount={treeMatchedCount}
|
|
|
focusedTreeNodeId={focusedTreeNode?.id}
|
|
|
+ focusTrigger={treeFocusTrigger}
|
|
|
onTreeFocusPrev={() => setTreeFocusIndex(prev => Math.max(0, prev - 1))}
|
|
|
onTreeFocusNext={() => setTreeFocusIndex(prev => Math.min(treeMatchedCount - 1, prev + 1))}
|
|
|
/>
|
|
|
@@ -3289,11 +3589,19 @@ export function Dashboard({ pendingNode, onPendingConsumed }: { pendingNode?: st
|
|
|
itemsets={allItemsets}
|
|
|
eligibleItemsetIds={relatedItemsetIds}
|
|
|
contextNodeNames={patternContextNodeNames}
|
|
|
+ patternMatchedNodesMap={patternMatchedNodesMap}
|
|
|
nodeRoleByName={patternRoleByName}
|
|
|
hasAnyFilter={hasAnyFilter}
|
|
|
focusIndex={columnFocusIndex.pattern ?? 0}
|
|
|
onSelectItemset={(itemsetId) => {
|
|
|
setSelectedItemsetId(itemsetId);
|
|
|
+ if (itemsetId !== null) {
|
|
|
+ setSelectedReqId(null);
|
|
|
+ setSelectedProcId(null);
|
|
|
+ setSelectedCapId(null);
|
|
|
+ setSelectedToolId(null);
|
|
|
+ setTreeFocusTrigger(prev => prev + 1);
|
|
|
+ }
|
|
|
setColumnFocusIndex(prev => ({ ...prev, pattern: 0 }));
|
|
|
if (itemsetId === null) setDrawerItem(null);
|
|
|
}}
|
|
|
@@ -3422,6 +3730,13 @@ export function Dashboard({ pendingNode, onPendingConsumed }: { pendingNode?: st
|
|
|
}
|
|
|
showAllSourceTags={col.t === 'req' && !!selectedTreePartition}
|
|
|
onSingleClick={(nodeId) => handleSingleClick(nodeId, item)}
|
|
|
+ onSourceNodeClick={(name) => {
|
|
|
+ const node = nameToNodeMap[name];
|
|
|
+ if (node) {
|
|
|
+ setSelectedNode(node);
|
|
|
+ setDrawerItem({ type: 'node', data: node });
|
|
|
+ }
|
|
|
+ }}
|
|
|
/>
|
|
|
))}
|
|
|
{col.d.length === 0 && (
|