feat: expand intelligent investigation workspace
This commit is contained in:
@@ -11,7 +11,7 @@ export const investigationDiagramConfig: MermaidConfig = {
|
||||
fontFamily: INVESTIGATION_DIAGRAM_FONT,
|
||||
themeVariables: {
|
||||
fontFamily: INVESTIGATION_DIAGRAM_FONT,
|
||||
fontSize: "15px",
|
||||
fontSize: "18px",
|
||||
background: "#FFFFFF",
|
||||
primaryColor: "#FFFFFF",
|
||||
primaryTextColor: "#26334D",
|
||||
@@ -28,6 +28,7 @@ export const investigationDiagramConfig: MermaidConfig = {
|
||||
clusterBorder: "#CCD6E5"
|
||||
},
|
||||
flowchart: {
|
||||
useMaxWidth: false,
|
||||
curve: "stepAfter",
|
||||
nodeSpacing: 56,
|
||||
rankSpacing: 64,
|
||||
@@ -42,10 +43,10 @@ export const investigationDiagramConfig: MermaidConfig = {
|
||||
.nodeLabel { font-weight: 500; }
|
||||
.nodeLabel p { text-wrap: balance; line-height: 1.6; }
|
||||
.edgePath path, .flowchart-link { stroke-width: 1.35px; }
|
||||
.edgeLabel { color: #536784; font-size: 12px; }
|
||||
.edgeLabel { color: #536784; font-size: 15px; }
|
||||
.edgeLabel p { padding: 3px 7px; border-radius: 4px; line-height: 1.5; }
|
||||
.cluster rect { rx: 8px; ry: 8px; stroke-width: 1px; }
|
||||
.cluster-label { font-weight: 600; font-size: 13px; }
|
||||
.cluster-label { font-weight: 600; font-size: 16px; }
|
||||
.cluster-label .nodeLabel { background: #F6F8FC; padding: 0 4px; }
|
||||
`
|
||||
};
|
||||
@@ -108,15 +109,60 @@ export const investigationDiagramPlugins = {
|
||||
}
|
||||
};
|
||||
|
||||
const roleStyles = [
|
||||
"classDef tjAgent fill:#F6F8FC,stroke:#A5B5CC,color:#26334D,stroke-width:1.4px;",
|
||||
"classDef tjHuman fill:#FFF6E8,stroke:#C4A168,color:#735018,stroke-width:1.4px;",
|
||||
"classDef tjDecision fill:#F6F8FC,stroke:#A5B5CC,color:#26334D,stroke-width:1.5px;",
|
||||
"classDef tjEvidence fill:#EDF7F4,stroke:#6B9E8B,color:#2D5E4E,stroke-width:1.6px;",
|
||||
"classDef tjAction fill:#FFF6E8,stroke:#C4A168,color:#735018,stroke-width:1.6px;"
|
||||
];
|
||||
export const investigationDiagramStates = {
|
||||
pending: { className: "tjPending", label: "尚未开始", fill: "#F6F8FC", stroke: "#A5B5CC", color: "#475976", width: 1.5, dash: "0" },
|
||||
running: { className: "tjRunning", label: "正在执行", fill: "#E8F0FF", stroke: "#4263C7", color: "#284574", width: 2.5, dash: "0" },
|
||||
completed: { className: "tjCompleted", label: "已完成", fill: "#EDF7F4", stroke: "#6B9E8B", color: "#2D5E4E", width: 1.8, dash: "0" },
|
||||
waiting: { className: "tjWaiting", label: "等待依赖 / 外部结果", fill: "#FFFBEA", stroke: "#C8AC60", color: "#78531B", width: 1.8, dash: "6 3" },
|
||||
waiting_human: { className: "tjWaitingHuman", label: "等待人工处理", fill: "#FFF6E8", stroke: "#C4A168", color: "#735018", width: 2, dash: "0" },
|
||||
failed: { className: "tjFailed", label: "执行失败", fill: "#FEF2F2", stroke: "#D96C75", color: "#922C3E", width: 2, dash: "0" },
|
||||
skipped: { className: "tjSkipped", label: "已跳过", fill: "#F6F8FC", stroke: "#A5B5CC", color: "#64748B", width: 1.5, dash: "8 4" },
|
||||
cancelled: { className: "tjCancelled", label: "已取消", fill: "#F1F5F9", stroke: "#94A3B8", color: "#64748B", width: 1.5, dash: "2 3" },
|
||||
recorded_running: { className: "tjRecordedRunning", label: "记录时执行中", fill: "#F6F8FC", stroke: "#A5B5CC", color: "#475976", width: 1.5, dash: "6 3" },
|
||||
unknown: { className: "tjUnknown", label: "未标注状态", fill: "#FFFFFF", stroke: "#CCD6E5", color: "#536784", width: 1.4, dash: "0" }
|
||||
} as const;
|
||||
|
||||
/** Style supplied relationships; never create nodes from a dangling display reference. */
|
||||
export function investigationNodeState(
|
||||
node: InvestigationDiagram["node_refs"][number],
|
||||
running = false
|
||||
): keyof typeof investigationDiagramStates {
|
||||
if (node.status === "running" && !running) return "recorded_running";
|
||||
return node.status ?? "unknown";
|
||||
}
|
||||
|
||||
function nodeStyle(state: typeof investigationDiagramStates[keyof typeof investigationDiagramStates]) {
|
||||
return `fill:${state.fill},stroke:${state.stroke},color:${state.color},stroke-width:${state.width}px,stroke-dasharray:${state.dash}`;
|
||||
}
|
||||
|
||||
export const investigationDiagramNodeId = (node: Element | null | undefined) =>
|
||||
node?.getAttribute("data-id") ?? node?.id.match(/(?:^|-)flowchart-(.+)-\d+$/)?.[1];
|
||||
|
||||
/** Update state-only changes in place, preserving the reader's pan and zoom. */
|
||||
export function paintInvestigationDiagramStates(root: Element, diagram: InvestigationDiagram, running: boolean) {
|
||||
const refs = new Map(diagram.node_refs.map((node) => [node.node_id, node]));
|
||||
const classes = Object.values(investigationDiagramStates).map((state) => state.className);
|
||||
root.querySelectorAll<SVGGElement>("svg g.node").forEach((node) => {
|
||||
const ref = refs.get(investigationDiagramNodeId(node) ?? "");
|
||||
const state = investigationDiagramStates[ref ? investigationNodeState(ref, running) : "unknown"];
|
||||
node.classList.remove(...classes);
|
||||
node.classList.add(state.className);
|
||||
node.setAttribute("aria-label", `${ref?.label ?? node.textContent?.trim() ?? "节点"}:${state.label}`);
|
||||
for (const shape of Array.from(node.children)) {
|
||||
if (!shape.matches("rect, polygon, path, circle, ellipse")) continue;
|
||||
const style = (shape as SVGElement).style;
|
||||
style.setProperty("fill", state.fill, "important");
|
||||
style.setProperty("stroke", state.stroke, "important");
|
||||
style.setProperty("stroke-width", `${state.width}px`, "important");
|
||||
style.setProperty("stroke-dasharray", state.dash, "important");
|
||||
}
|
||||
node.querySelectorAll<SVGElement | HTMLElement>(".nodeLabel, .nodeLabel p").forEach((label) => {
|
||||
label.style.setProperty("color", state.color, "important");
|
||||
label.style.setProperty("fill", state.color, "important");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Color explicit recorded states; relationships and evidence never imply completion. */
|
||||
export function styleInvestigationDiagram(diagram: InvestigationDiagram, running = false) {
|
||||
const assignments = diagram.node_refs.flatMap((node) => {
|
||||
const escaped = node.node_id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
@@ -124,14 +170,15 @@ export function styleInvestigationDiagram(diagram: InvestigationDiagram, running
|
||||
diagram.mermaid
|
||||
);
|
||||
if (!inSource) return [];
|
||||
if (running && node.status === "running") return [`class ${node.node_id} tjRunning;`];
|
||||
return node.action_refs.length
|
||||
? [`class ${node.node_id} tjAction;`]
|
||||
: node.evidence_refs.length
|
||||
? [`class ${node.node_id} tjEvidence;`]
|
||||
: [];
|
||||
const state = investigationDiagramStates[investigationNodeState(node, running)];
|
||||
return [`class ${node.node_id} ${state.className};`, `style ${node.node_id} ${nodeStyle(state)};`];
|
||||
});
|
||||
return [diagram.mermaid, ...roleStyles,
|
||||
running ? "classDef tjRunning fill:#E8F0FF,stroke:#4263C7,color:#284574,stroke-width:2.5px;" : "classDef tjRunning fill:#F6F8FC,stroke:#A5B5CC,color:#26334D,stroke-width:1.4px;",
|
||||
...assignments].join("\n");
|
||||
// Keep role/reference classes neutral. Their presence conveys association,
|
||||
// while only node_refs.status supplies an execution fact.
|
||||
const roles = ["default", "tjAgent", "tjHuman", "tjDecision", "tjEvidence", "tjAction"]
|
||||
.map((name) => `classDef ${name} ${nodeStyle(investigationDiagramStates.unknown)};`);
|
||||
const states = Object.entries(investigationDiagramStates).map(([key, state]) =>
|
||||
`classDef ${state.className} ${nodeStyle(key === "running" && !running ? investigationDiagramStates.recorded_running : state)};`
|
||||
);
|
||||
return [diagram.mermaid, ...roles, ...states, ...assignments].join("\n");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user