agentflow-dashboard 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-25MUPUYY.js → chunk-YDFLDRWO.js} +92 -0
- package/dist/cli.cjs +92 -0
- package/dist/cli.js +1 -1
- package/dist/index.cjs +92 -0
- package/dist/index.js +1 -1
- package/dist/public/dashboard.js +217 -0
- package/dist/public/index.html +19 -0
- package/dist/server.cjs +92 -0
- package/dist/server.js +1 -1
- package/package.json +1 -1
- package/public/dashboard.js +217 -0
- package/public/index.html +19 -0
|
@@ -1667,6 +1667,98 @@ var DashboardServer = class {
|
|
|
1667
1667
|
res.status(500).json({ error: "Failed to load statistics" });
|
|
1668
1668
|
}
|
|
1669
1669
|
});
|
|
1670
|
+
this.app.get("/api/agents/:agentId/process-graph", (req, res) => {
|
|
1671
|
+
try {
|
|
1672
|
+
const agentId = req.params.agentId;
|
|
1673
|
+
const traces = this.watcher.getTracesByAgent(agentId).map(serializeTrace);
|
|
1674
|
+
if (traces.length === 0) {
|
|
1675
|
+
return res.status(404).json({ error: "No traces for agent" });
|
|
1676
|
+
}
|
|
1677
|
+
const activityCounts = /* @__PURE__ */ new Map();
|
|
1678
|
+
const transitionCounts = /* @__PURE__ */ new Map();
|
|
1679
|
+
const activityDurations = /* @__PURE__ */ new Map();
|
|
1680
|
+
const activityStatuses = /* @__PURE__ */ new Map();
|
|
1681
|
+
let totalTraces = 0;
|
|
1682
|
+
for (const trace of traces) {
|
|
1683
|
+
totalTraces++;
|
|
1684
|
+
const activities = [];
|
|
1685
|
+
if (trace.sessionEvents && trace.sessionEvents.length > 0) {
|
|
1686
|
+
for (const evt of trace.sessionEvents) {
|
|
1687
|
+
const name = evt.toolName || evt.name || evt.type;
|
|
1688
|
+
if (!name) continue;
|
|
1689
|
+
activities.push({
|
|
1690
|
+
name,
|
|
1691
|
+
type: evt.type,
|
|
1692
|
+
status: evt.toolError ? "failed" : "completed",
|
|
1693
|
+
duration: evt.duration || 0
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
} else {
|
|
1697
|
+
const nodes2 = trace.nodes || {};
|
|
1698
|
+
const sorted = Object.values(nodes2).sort((a, b) => (a.startTime || 0) - (b.startTime || 0));
|
|
1699
|
+
for (const node of sorted) {
|
|
1700
|
+
activities.push({
|
|
1701
|
+
name: node.name || node.type || node.id,
|
|
1702
|
+
type: node.type || "unknown",
|
|
1703
|
+
status: node.status || "completed",
|
|
1704
|
+
duration: (node.endTime || node.startTime || 0) - (node.startTime || 0)
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
const seq = ["[START]", ...activities.map((a) => a.name), "[END]"];
|
|
1709
|
+
for (let i = 0; i < seq.length; i++) {
|
|
1710
|
+
const act = seq[i];
|
|
1711
|
+
activityCounts.set(act, (activityCounts.get(act) || 0) + 1);
|
|
1712
|
+
if (i < seq.length - 1) {
|
|
1713
|
+
const key = act + " \u2192 " + seq[i + 1];
|
|
1714
|
+
transitionCounts.set(key, (transitionCounts.get(key) || 0) + 1);
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
for (const act of activities) {
|
|
1718
|
+
if (act.duration > 0) {
|
|
1719
|
+
const durs = activityDurations.get(act.name) || [];
|
|
1720
|
+
durs.push(act.duration);
|
|
1721
|
+
activityDurations.set(act.name, durs);
|
|
1722
|
+
}
|
|
1723
|
+
const st = activityStatuses.get(act.name) || { ok: 0, fail: 0 };
|
|
1724
|
+
if (act.status === "failed") st.fail++;
|
|
1725
|
+
else st.ok++;
|
|
1726
|
+
activityStatuses.set(act.name, st);
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
const nodes = Array.from(activityCounts.entries()).map(([name, count]) => {
|
|
1730
|
+
const durs = activityDurations.get(name) || [];
|
|
1731
|
+
const st = activityStatuses.get(name) || { ok: 0, fail: 0 };
|
|
1732
|
+
const avgDuration = durs.length > 0 ? durs.reduce((a, b) => a + b, 0) / durs.length : 0;
|
|
1733
|
+
return {
|
|
1734
|
+
id: name,
|
|
1735
|
+
label: name,
|
|
1736
|
+
count,
|
|
1737
|
+
frequency: count / totalTraces,
|
|
1738
|
+
avgDuration,
|
|
1739
|
+
failRate: st.ok + st.fail > 0 ? st.fail / (st.ok + st.fail) : 0,
|
|
1740
|
+
isVirtual: name === "[START]" || name === "[END]"
|
|
1741
|
+
};
|
|
1742
|
+
});
|
|
1743
|
+
const edges = Array.from(transitionCounts.entries()).map(([key, count]) => {
|
|
1744
|
+
const [source, target] = key.split(" \u2192 ");
|
|
1745
|
+
return { source, target, count, frequency: count / totalTraces };
|
|
1746
|
+
});
|
|
1747
|
+
const maxEdgeCount = Math.max(...edges.map((e) => e.count), 1);
|
|
1748
|
+
const maxNodeCount = Math.max(...nodes.filter((n) => !n.isVirtual).map((n) => n.count), 1);
|
|
1749
|
+
res.json({
|
|
1750
|
+
agentId,
|
|
1751
|
+
totalTraces,
|
|
1752
|
+
nodes,
|
|
1753
|
+
edges,
|
|
1754
|
+
maxEdgeCount,
|
|
1755
|
+
maxNodeCount
|
|
1756
|
+
});
|
|
1757
|
+
} catch (error) {
|
|
1758
|
+
console.error("Process graph error:", error);
|
|
1759
|
+
res.status(500).json({ error: "Failed to build process graph" });
|
|
1760
|
+
}
|
|
1761
|
+
});
|
|
1670
1762
|
this.app.get("/api/stats/:agentId", (req, res) => {
|
|
1671
1763
|
try {
|
|
1672
1764
|
const agentStats = this.stats.getAgentStats(req.params.agentId);
|
package/dist/cli.cjs
CHANGED
|
@@ -1561,6 +1561,98 @@ var DashboardServer = class {
|
|
|
1561
1561
|
res.status(500).json({ error: "Failed to load statistics" });
|
|
1562
1562
|
}
|
|
1563
1563
|
});
|
|
1564
|
+
this.app.get("/api/agents/:agentId/process-graph", (req, res) => {
|
|
1565
|
+
try {
|
|
1566
|
+
const agentId = req.params.agentId;
|
|
1567
|
+
const traces = this.watcher.getTracesByAgent(agentId).map(serializeTrace);
|
|
1568
|
+
if (traces.length === 0) {
|
|
1569
|
+
return res.status(404).json({ error: "No traces for agent" });
|
|
1570
|
+
}
|
|
1571
|
+
const activityCounts = /* @__PURE__ */ new Map();
|
|
1572
|
+
const transitionCounts = /* @__PURE__ */ new Map();
|
|
1573
|
+
const activityDurations = /* @__PURE__ */ new Map();
|
|
1574
|
+
const activityStatuses = /* @__PURE__ */ new Map();
|
|
1575
|
+
let totalTraces = 0;
|
|
1576
|
+
for (const trace of traces) {
|
|
1577
|
+
totalTraces++;
|
|
1578
|
+
const activities = [];
|
|
1579
|
+
if (trace.sessionEvents && trace.sessionEvents.length > 0) {
|
|
1580
|
+
for (const evt of trace.sessionEvents) {
|
|
1581
|
+
const name = evt.toolName || evt.name || evt.type;
|
|
1582
|
+
if (!name) continue;
|
|
1583
|
+
activities.push({
|
|
1584
|
+
name,
|
|
1585
|
+
type: evt.type,
|
|
1586
|
+
status: evt.toolError ? "failed" : "completed",
|
|
1587
|
+
duration: evt.duration || 0
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
} else {
|
|
1591
|
+
const nodes2 = trace.nodes || {};
|
|
1592
|
+
const sorted = Object.values(nodes2).sort((a, b) => (a.startTime || 0) - (b.startTime || 0));
|
|
1593
|
+
for (const node of sorted) {
|
|
1594
|
+
activities.push({
|
|
1595
|
+
name: node.name || node.type || node.id,
|
|
1596
|
+
type: node.type || "unknown",
|
|
1597
|
+
status: node.status || "completed",
|
|
1598
|
+
duration: (node.endTime || node.startTime || 0) - (node.startTime || 0)
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
const seq = ["[START]", ...activities.map((a) => a.name), "[END]"];
|
|
1603
|
+
for (let i = 0; i < seq.length; i++) {
|
|
1604
|
+
const act = seq[i];
|
|
1605
|
+
activityCounts.set(act, (activityCounts.get(act) || 0) + 1);
|
|
1606
|
+
if (i < seq.length - 1) {
|
|
1607
|
+
const key = act + " \u2192 " + seq[i + 1];
|
|
1608
|
+
transitionCounts.set(key, (transitionCounts.get(key) || 0) + 1);
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
for (const act of activities) {
|
|
1612
|
+
if (act.duration > 0) {
|
|
1613
|
+
const durs = activityDurations.get(act.name) || [];
|
|
1614
|
+
durs.push(act.duration);
|
|
1615
|
+
activityDurations.set(act.name, durs);
|
|
1616
|
+
}
|
|
1617
|
+
const st = activityStatuses.get(act.name) || { ok: 0, fail: 0 };
|
|
1618
|
+
if (act.status === "failed") st.fail++;
|
|
1619
|
+
else st.ok++;
|
|
1620
|
+
activityStatuses.set(act.name, st);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
const nodes = Array.from(activityCounts.entries()).map(([name, count]) => {
|
|
1624
|
+
const durs = activityDurations.get(name) || [];
|
|
1625
|
+
const st = activityStatuses.get(name) || { ok: 0, fail: 0 };
|
|
1626
|
+
const avgDuration = durs.length > 0 ? durs.reduce((a, b) => a + b, 0) / durs.length : 0;
|
|
1627
|
+
return {
|
|
1628
|
+
id: name,
|
|
1629
|
+
label: name,
|
|
1630
|
+
count,
|
|
1631
|
+
frequency: count / totalTraces,
|
|
1632
|
+
avgDuration,
|
|
1633
|
+
failRate: st.ok + st.fail > 0 ? st.fail / (st.ok + st.fail) : 0,
|
|
1634
|
+
isVirtual: name === "[START]" || name === "[END]"
|
|
1635
|
+
};
|
|
1636
|
+
});
|
|
1637
|
+
const edges = Array.from(transitionCounts.entries()).map(([key, count]) => {
|
|
1638
|
+
const [source, target] = key.split(" \u2192 ");
|
|
1639
|
+
return { source, target, count, frequency: count / totalTraces };
|
|
1640
|
+
});
|
|
1641
|
+
const maxEdgeCount = Math.max(...edges.map((e) => e.count), 1);
|
|
1642
|
+
const maxNodeCount = Math.max(...nodes.filter((n) => !n.isVirtual).map((n) => n.count), 1);
|
|
1643
|
+
res.json({
|
|
1644
|
+
agentId,
|
|
1645
|
+
totalTraces,
|
|
1646
|
+
nodes,
|
|
1647
|
+
edges,
|
|
1648
|
+
maxEdgeCount,
|
|
1649
|
+
maxNodeCount
|
|
1650
|
+
});
|
|
1651
|
+
} catch (error) {
|
|
1652
|
+
console.error("Process graph error:", error);
|
|
1653
|
+
res.status(500).json({ error: "Failed to build process graph" });
|
|
1654
|
+
}
|
|
1655
|
+
});
|
|
1564
1656
|
this.app.get("/api/stats/:agentId", (req, res) => {
|
|
1565
1657
|
try {
|
|
1566
1658
|
const agentStats = this.stats.getAgentStats(req.params.agentId);
|
package/dist/cli.js
CHANGED
package/dist/index.cjs
CHANGED
|
@@ -1706,6 +1706,98 @@ var DashboardServer = class {
|
|
|
1706
1706
|
res.status(500).json({ error: "Failed to load statistics" });
|
|
1707
1707
|
}
|
|
1708
1708
|
});
|
|
1709
|
+
this.app.get("/api/agents/:agentId/process-graph", (req, res) => {
|
|
1710
|
+
try {
|
|
1711
|
+
const agentId = req.params.agentId;
|
|
1712
|
+
const traces = this.watcher.getTracesByAgent(agentId).map(serializeTrace);
|
|
1713
|
+
if (traces.length === 0) {
|
|
1714
|
+
return res.status(404).json({ error: "No traces for agent" });
|
|
1715
|
+
}
|
|
1716
|
+
const activityCounts = /* @__PURE__ */ new Map();
|
|
1717
|
+
const transitionCounts = /* @__PURE__ */ new Map();
|
|
1718
|
+
const activityDurations = /* @__PURE__ */ new Map();
|
|
1719
|
+
const activityStatuses = /* @__PURE__ */ new Map();
|
|
1720
|
+
let totalTraces = 0;
|
|
1721
|
+
for (const trace of traces) {
|
|
1722
|
+
totalTraces++;
|
|
1723
|
+
const activities = [];
|
|
1724
|
+
if (trace.sessionEvents && trace.sessionEvents.length > 0) {
|
|
1725
|
+
for (const evt of trace.sessionEvents) {
|
|
1726
|
+
const name = evt.toolName || evt.name || evt.type;
|
|
1727
|
+
if (!name) continue;
|
|
1728
|
+
activities.push({
|
|
1729
|
+
name,
|
|
1730
|
+
type: evt.type,
|
|
1731
|
+
status: evt.toolError ? "failed" : "completed",
|
|
1732
|
+
duration: evt.duration || 0
|
|
1733
|
+
});
|
|
1734
|
+
}
|
|
1735
|
+
} else {
|
|
1736
|
+
const nodes2 = trace.nodes || {};
|
|
1737
|
+
const sorted = Object.values(nodes2).sort((a, b) => (a.startTime || 0) - (b.startTime || 0));
|
|
1738
|
+
for (const node of sorted) {
|
|
1739
|
+
activities.push({
|
|
1740
|
+
name: node.name || node.type || node.id,
|
|
1741
|
+
type: node.type || "unknown",
|
|
1742
|
+
status: node.status || "completed",
|
|
1743
|
+
duration: (node.endTime || node.startTime || 0) - (node.startTime || 0)
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
const seq = ["[START]", ...activities.map((a) => a.name), "[END]"];
|
|
1748
|
+
for (let i = 0; i < seq.length; i++) {
|
|
1749
|
+
const act = seq[i];
|
|
1750
|
+
activityCounts.set(act, (activityCounts.get(act) || 0) + 1);
|
|
1751
|
+
if (i < seq.length - 1) {
|
|
1752
|
+
const key = act + " \u2192 " + seq[i + 1];
|
|
1753
|
+
transitionCounts.set(key, (transitionCounts.get(key) || 0) + 1);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
for (const act of activities) {
|
|
1757
|
+
if (act.duration > 0) {
|
|
1758
|
+
const durs = activityDurations.get(act.name) || [];
|
|
1759
|
+
durs.push(act.duration);
|
|
1760
|
+
activityDurations.set(act.name, durs);
|
|
1761
|
+
}
|
|
1762
|
+
const st = activityStatuses.get(act.name) || { ok: 0, fail: 0 };
|
|
1763
|
+
if (act.status === "failed") st.fail++;
|
|
1764
|
+
else st.ok++;
|
|
1765
|
+
activityStatuses.set(act.name, st);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
const nodes = Array.from(activityCounts.entries()).map(([name, count]) => {
|
|
1769
|
+
const durs = activityDurations.get(name) || [];
|
|
1770
|
+
const st = activityStatuses.get(name) || { ok: 0, fail: 0 };
|
|
1771
|
+
const avgDuration = durs.length > 0 ? durs.reduce((a, b) => a + b, 0) / durs.length : 0;
|
|
1772
|
+
return {
|
|
1773
|
+
id: name,
|
|
1774
|
+
label: name,
|
|
1775
|
+
count,
|
|
1776
|
+
frequency: count / totalTraces,
|
|
1777
|
+
avgDuration,
|
|
1778
|
+
failRate: st.ok + st.fail > 0 ? st.fail / (st.ok + st.fail) : 0,
|
|
1779
|
+
isVirtual: name === "[START]" || name === "[END]"
|
|
1780
|
+
};
|
|
1781
|
+
});
|
|
1782
|
+
const edges = Array.from(transitionCounts.entries()).map(([key, count]) => {
|
|
1783
|
+
const [source, target] = key.split(" \u2192 ");
|
|
1784
|
+
return { source, target, count, frequency: count / totalTraces };
|
|
1785
|
+
});
|
|
1786
|
+
const maxEdgeCount = Math.max(...edges.map((e) => e.count), 1);
|
|
1787
|
+
const maxNodeCount = Math.max(...nodes.filter((n) => !n.isVirtual).map((n) => n.count), 1);
|
|
1788
|
+
res.json({
|
|
1789
|
+
agentId,
|
|
1790
|
+
totalTraces,
|
|
1791
|
+
nodes,
|
|
1792
|
+
edges,
|
|
1793
|
+
maxEdgeCount,
|
|
1794
|
+
maxNodeCount
|
|
1795
|
+
});
|
|
1796
|
+
} catch (error) {
|
|
1797
|
+
console.error("Process graph error:", error);
|
|
1798
|
+
res.status(500).json({ error: "Failed to build process graph" });
|
|
1799
|
+
}
|
|
1800
|
+
});
|
|
1709
1801
|
this.app.get("/api/stats/:agentId", (req, res) => {
|
|
1710
1802
|
try {
|
|
1711
1803
|
const agentStats = this.stats.getAgentStats(req.params.agentId);
|
package/dist/index.js
CHANGED
package/dist/public/dashboard.js
CHANGED
|
@@ -307,6 +307,12 @@ class AgentFlowDashboard {
|
|
|
307
307
|
this.selectedTrace = trace;
|
|
308
308
|
this.selectedTraceData = trace;
|
|
309
309
|
|
|
310
|
+
// Reset process map cache when agent changes
|
|
311
|
+
if (this._processMapAgent !== trace.agentId) {
|
|
312
|
+
this._processMapAgent = null;
|
|
313
|
+
if (this._cyProcessMap) { this._cyProcessMap.destroy(); this._cyProcessMap = null; }
|
|
314
|
+
}
|
|
315
|
+
|
|
310
316
|
// Update sidebar selection
|
|
311
317
|
document.querySelectorAll('.session-item').forEach(function(el) { el.classList.remove('active'); });
|
|
312
318
|
var activeEl = document.querySelector('.session-item[data-filename="' + CSS.escape(filename) + '"]');
|
|
@@ -850,6 +856,7 @@ class AgentFlowDashboard {
|
|
|
850
856
|
case 'state': this.renderStateMachine(); break;
|
|
851
857
|
case 'summary': this.renderSummary(); break;
|
|
852
858
|
case 'transcript': this.renderTranscript(); break;
|
|
859
|
+
case 'processmap': this.renderProcessMap(); break;
|
|
853
860
|
}
|
|
854
861
|
this.updateToolbarInfo();
|
|
855
862
|
}
|
|
@@ -1906,6 +1913,216 @@ class AgentFlowDashboard {
|
|
|
1906
1913
|
|
|
1907
1914
|
return 'other';
|
|
1908
1915
|
}
|
|
1916
|
+
|
|
1917
|
+
// ---------------------------------------------------------------------------
|
|
1918
|
+
// Tab 8: Process Map (Process Mining Graph)
|
|
1919
|
+
// ---------------------------------------------------------------------------
|
|
1920
|
+
renderProcessMap() {
|
|
1921
|
+
var trace = this.selectedTraceData || this.selectedTrace;
|
|
1922
|
+
if (!trace || !trace.agentId) {
|
|
1923
|
+
document.getElementById('processMapEmpty').style.display = '';
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
var agentId = trace.agentId;
|
|
1928
|
+
var self = this;
|
|
1929
|
+
|
|
1930
|
+
// Avoid re-fetching for same agent
|
|
1931
|
+
if (this._processMapAgent === agentId && this._cyProcessMap) return;
|
|
1932
|
+
this._processMapAgent = agentId;
|
|
1933
|
+
|
|
1934
|
+
document.getElementById('processMapEmpty').innerHTML =
|
|
1935
|
+
'<div class="empty-state-icon" style="animation:spin 1s linear infinite">⚙</div>' +
|
|
1936
|
+
'<div class="empty-state-text">Building process map for ' + escapeHtml(agentId) + '...</div>';
|
|
1937
|
+
document.getElementById('processMapEmpty').style.display = '';
|
|
1938
|
+
|
|
1939
|
+
fetch('/api/agents/' + encodeURIComponent(agentId) + '/process-graph')
|
|
1940
|
+
.then(function(r) { return r.json(); })
|
|
1941
|
+
.then(function(data) {
|
|
1942
|
+
if (data.error || !data.nodes || data.nodes.length === 0) {
|
|
1943
|
+
document.getElementById('processMapEmpty').innerHTML =
|
|
1944
|
+
'<div class="empty-state-icon">⚙</div>' +
|
|
1945
|
+
'<div class="empty-state-text">No process data for ' + escapeHtml(agentId) + '</div>';
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
document.getElementById('processMapEmpty').style.display = 'none';
|
|
1949
|
+
self._buildProcessMapGraph(data);
|
|
1950
|
+
})
|
|
1951
|
+
.catch(function() {
|
|
1952
|
+
document.getElementById('processMapEmpty').innerHTML =
|
|
1953
|
+
'<div class="empty-state-icon">⚙</div>' +
|
|
1954
|
+
'<div class="empty-state-text">Failed to load process map.</div>';
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
_buildProcessMapGraph(data) {
|
|
1959
|
+
if (this._cyProcessMap) { this._cyProcessMap.destroy(); this._cyProcessMap = null; }
|
|
1960
|
+
|
|
1961
|
+
var elements = [];
|
|
1962
|
+
var maxNode = data.maxNodeCount || 1;
|
|
1963
|
+
var maxEdge = data.maxEdgeCount || 1;
|
|
1964
|
+
|
|
1965
|
+
// Add nodes
|
|
1966
|
+
for (var i = 0; i < data.nodes.length; i++) {
|
|
1967
|
+
var node = data.nodes[i];
|
|
1968
|
+
// Skip very rare activities (< 2% frequency) to reduce clutter, but keep virtual nodes
|
|
1969
|
+
if (!node.isVirtual && node.frequency < 0.02 && data.nodes.length > 15) continue;
|
|
1970
|
+
|
|
1971
|
+
var size = node.isVirtual ? 30 : Math.max(25, Math.min(70, 25 + 45 * (node.count / maxNode)));
|
|
1972
|
+
var label = node.label;
|
|
1973
|
+
if (!node.isVirtual && node.count > 1) label += ' (' + node.count + ')';
|
|
1974
|
+
|
|
1975
|
+
elements.push({
|
|
1976
|
+
group: 'nodes',
|
|
1977
|
+
data: {
|
|
1978
|
+
id: node.id,
|
|
1979
|
+
label: label,
|
|
1980
|
+
count: node.count,
|
|
1981
|
+
frequency: node.frequency,
|
|
1982
|
+
avgDuration: node.avgDuration,
|
|
1983
|
+
failRate: node.failRate,
|
|
1984
|
+
isVirtual: node.isVirtual,
|
|
1985
|
+
size: size,
|
|
1986
|
+
fullData: node
|
|
1987
|
+
}
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
// Collect valid node IDs
|
|
1992
|
+
var validIds = new Set(elements.map(function(e) { return e.data.id; }));
|
|
1993
|
+
|
|
1994
|
+
// Add edges (only between valid nodes)
|
|
1995
|
+
for (var j = 0; j < data.edges.length; j++) {
|
|
1996
|
+
var edge = data.edges[j];
|
|
1997
|
+
if (!validIds.has(edge.source) || !validIds.has(edge.target)) continue;
|
|
1998
|
+
// Skip very rare transitions
|
|
1999
|
+
if (edge.frequency < 0.02 && data.edges.length > 30) continue;
|
|
2000
|
+
|
|
2001
|
+
var width = Math.max(1, Math.min(8, 1 + 7 * (edge.count / maxEdge)));
|
|
2002
|
+
var opacity = Math.max(0.3, Math.min(1.0, 0.3 + 0.7 * (edge.count / maxEdge)));
|
|
2003
|
+
|
|
2004
|
+
elements.push({
|
|
2005
|
+
group: 'edges',
|
|
2006
|
+
data: {
|
|
2007
|
+
id: 'pe-' + edge.source + '-' + edge.target,
|
|
2008
|
+
source: edge.source,
|
|
2009
|
+
target: edge.target,
|
|
2010
|
+
count: edge.count,
|
|
2011
|
+
frequency: edge.frequency,
|
|
2012
|
+
width: width,
|
|
2013
|
+
opacity: opacity,
|
|
2014
|
+
label: edge.count > 1 ? String(edge.count) : ''
|
|
2015
|
+
}
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
var container = document.getElementById('cyProcessMap');
|
|
2020
|
+
var self = this;
|
|
2021
|
+
|
|
2022
|
+
this._cyProcessMap = cytoscape({
|
|
2023
|
+
container: container,
|
|
2024
|
+
elements: elements,
|
|
2025
|
+
style: [
|
|
2026
|
+
{
|
|
2027
|
+
selector: 'node',
|
|
2028
|
+
style: {
|
|
2029
|
+
'label': 'data(label)',
|
|
2030
|
+
'width': 'data(size)',
|
|
2031
|
+
'height': 'data(size)',
|
|
2032
|
+
'font-size': '9px',
|
|
2033
|
+
'text-valign': 'bottom',
|
|
2034
|
+
'text-halign': 'center',
|
|
2035
|
+
'text-margin-y': 6,
|
|
2036
|
+
'color': '#c9d1d9',
|
|
2037
|
+
'text-outline-color': '#0d1117',
|
|
2038
|
+
'text-outline-width': 2,
|
|
2039
|
+
'text-wrap': 'ellipsis',
|
|
2040
|
+
'text-max-width': '100px',
|
|
2041
|
+
'border-width': 2,
|
|
2042
|
+
'border-color': '#30363d',
|
|
2043
|
+
'background-color': '#3b82f6',
|
|
2044
|
+
'shape': 'round-rectangle'
|
|
2045
|
+
}
|
|
2046
|
+
},
|
|
2047
|
+
// Virtual START/END nodes
|
|
2048
|
+
{ selector: 'node[?isVirtual]', style: {
|
|
2049
|
+
'background-color': '#6b7280', 'shape': 'ellipse', 'border-color': '#4b5563',
|
|
2050
|
+
'font-size': '8px', 'font-weight': 'bold', 'text-valign': 'center', 'text-margin-y': 0
|
|
2051
|
+
}},
|
|
2052
|
+
// Color by fail rate: green → yellow → red
|
|
2053
|
+
{ selector: 'node[failRate <= 0]', style: { 'background-color': '#10b981', 'border-color': '#2ea043' }},
|
|
2054
|
+
{ selector: 'node[failRate > 0][failRate <= 0.1]', style: { 'background-color': '#22c55e', 'border-color': '#3fb950' }},
|
|
2055
|
+
{ selector: 'node[failRate > 0.1][failRate <= 0.3]', style: { 'background-color': '#eab308', 'border-color': '#d29922' }},
|
|
2056
|
+
{ selector: 'node[failRate > 0.3]', style: { 'background-color': '#ef4444', 'border-color': '#f85149' }},
|
|
2057
|
+
// Selected
|
|
2058
|
+
{ selector: ':selected', style: { 'border-width': 4, 'border-color': '#f59e0b', 'overlay-opacity': 0.08 }},
|
|
2059
|
+
// Edges
|
|
2060
|
+
{
|
|
2061
|
+
selector: 'edge',
|
|
2062
|
+
style: {
|
|
2063
|
+
'width': 'data(width)',
|
|
2064
|
+
'opacity': 'data(opacity)',
|
|
2065
|
+
'line-color': '#6b7280',
|
|
2066
|
+
'target-arrow-color': '#6b7280',
|
|
2067
|
+
'target-arrow-shape': 'triangle',
|
|
2068
|
+
'curve-style': 'bezier',
|
|
2069
|
+
'arrow-scale': 0.7,
|
|
2070
|
+
'label': 'data(label)',
|
|
2071
|
+
'font-size': '8px',
|
|
2072
|
+
'color': '#8b949e',
|
|
2073
|
+
'text-outline-color': '#0d1117',
|
|
2074
|
+
'text-outline-width': 1.5,
|
|
2075
|
+
'text-rotation': 'autorotate'
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
],
|
|
2079
|
+
layout: {
|
|
2080
|
+
name: 'breadthfirst',
|
|
2081
|
+
directed: true,
|
|
2082
|
+
padding: 50,
|
|
2083
|
+
spacingFactor: 1.6,
|
|
2084
|
+
animate: true,
|
|
2085
|
+
animationDuration: 400,
|
|
2086
|
+
roots: elements.filter(function(e) { return e.data && e.data.id === '[START]'; }).length > 0
|
|
2087
|
+
? ['[START]'] : undefined
|
|
2088
|
+
},
|
|
2089
|
+
minZoom: 0.15,
|
|
2090
|
+
maxZoom: 4,
|
|
2091
|
+
wheelSensitivity: 0.3
|
|
2092
|
+
});
|
|
2093
|
+
|
|
2094
|
+
// Click node → show detail
|
|
2095
|
+
this._cyProcessMap.on('tap', 'node', function(e) {
|
|
2096
|
+
var d = e.target.data().fullData;
|
|
2097
|
+
if (!d || d.isVirtual) return;
|
|
2098
|
+
var panel = document.getElementById('processMapDetailPanel');
|
|
2099
|
+
var title = document.getElementById('processMapDetailTitle');
|
|
2100
|
+
var body = document.getElementById('processMapDetailBody');
|
|
2101
|
+
|
|
2102
|
+
title.textContent = d.label;
|
|
2103
|
+
var html = '';
|
|
2104
|
+
html += self.detailRow('Occurrences', d.count);
|
|
2105
|
+
html += self.detailRow('Frequency', (d.frequency * 100).toFixed(1) + '% of traces');
|
|
2106
|
+
if (d.avgDuration > 0) html += self.detailRow('Avg Duration', self.computeDuration(0, d.avgDuration));
|
|
2107
|
+
html += self.detailRow('Failure Rate', (d.failRate * 100).toFixed(1) + '%');
|
|
2108
|
+
body.innerHTML = html;
|
|
2109
|
+
panel.classList.add('active');
|
|
2110
|
+
});
|
|
2111
|
+
|
|
2112
|
+
this._cyProcessMap.on('tap', function(e) {
|
|
2113
|
+
if (e.target === self._cyProcessMap) {
|
|
2114
|
+
document.getElementById('processMapDetailPanel').classList.remove('active');
|
|
2115
|
+
}
|
|
2116
|
+
});
|
|
2117
|
+
|
|
2118
|
+
// Close button
|
|
2119
|
+
var closeBtn = document.getElementById('processMapDetailClose');
|
|
2120
|
+
if (closeBtn) {
|
|
2121
|
+
closeBtn.onclick = function() {
|
|
2122
|
+
document.getElementById('processMapDetailPanel').classList.remove('active');
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
1909
2126
|
}
|
|
1910
2127
|
|
|
1911
2128
|
// Initialize
|
package/dist/public/index.html
CHANGED
|
@@ -1235,6 +1235,7 @@
|
|
|
1235
1235
|
<div class="tab" data-tab="state">State Machine</div>
|
|
1236
1236
|
<div class="tab" data-tab="summary">Summary</div>
|
|
1237
1237
|
<div class="tab" data-tab="transcript">Transcript</div>
|
|
1238
|
+
<div class="tab" data-tab="processmap">Process Map</div>
|
|
1238
1239
|
</div>
|
|
1239
1240
|
|
|
1240
1241
|
<div class="toolbar" id="toolbar">
|
|
@@ -1332,6 +1333,24 @@
|
|
|
1332
1333
|
</div>
|
|
1333
1334
|
</div>
|
|
1334
1335
|
</div>
|
|
1336
|
+
|
|
1337
|
+
<!-- Process Map tab -->
|
|
1338
|
+
<div class="tab-panel" id="panel-processmap">
|
|
1339
|
+
<div id="cyProcessMap" style="width:100%;height:100%;min-height:500px;">
|
|
1340
|
+
<div class="empty-state" id="processMapEmpty">
|
|
1341
|
+
<div class="empty-state-icon">⚙</div>
|
|
1342
|
+
<div class="empty-state-title">Process Map</div>
|
|
1343
|
+
<div class="empty-state-text">Select a trace to view the process mining graph for its agent.<br>Shows activity flows aggregated across all executions.</div>
|
|
1344
|
+
</div>
|
|
1345
|
+
</div>
|
|
1346
|
+
<div class="node-detail-panel" id="processMapDetailPanel">
|
|
1347
|
+
<div class="node-detail-header">
|
|
1348
|
+
<h4 id="processMapDetailTitle">Activity Details</h4>
|
|
1349
|
+
<button class="node-detail-close" id="processMapDetailClose">×</button>
|
|
1350
|
+
</div>
|
|
1351
|
+
<div class="node-detail-body" id="processMapDetailBody"></div>
|
|
1352
|
+
</div>
|
|
1353
|
+
</div>
|
|
1335
1354
|
</div>
|
|
1336
1355
|
</main>
|
|
1337
1356
|
</div>
|
package/dist/server.cjs
CHANGED
|
@@ -1701,6 +1701,98 @@ var DashboardServer = class {
|
|
|
1701
1701
|
res.status(500).json({ error: "Failed to load statistics" });
|
|
1702
1702
|
}
|
|
1703
1703
|
});
|
|
1704
|
+
this.app.get("/api/agents/:agentId/process-graph", (req, res) => {
|
|
1705
|
+
try {
|
|
1706
|
+
const agentId = req.params.agentId;
|
|
1707
|
+
const traces = this.watcher.getTracesByAgent(agentId).map(serializeTrace);
|
|
1708
|
+
if (traces.length === 0) {
|
|
1709
|
+
return res.status(404).json({ error: "No traces for agent" });
|
|
1710
|
+
}
|
|
1711
|
+
const activityCounts = /* @__PURE__ */ new Map();
|
|
1712
|
+
const transitionCounts = /* @__PURE__ */ new Map();
|
|
1713
|
+
const activityDurations = /* @__PURE__ */ new Map();
|
|
1714
|
+
const activityStatuses = /* @__PURE__ */ new Map();
|
|
1715
|
+
let totalTraces = 0;
|
|
1716
|
+
for (const trace of traces) {
|
|
1717
|
+
totalTraces++;
|
|
1718
|
+
const activities = [];
|
|
1719
|
+
if (trace.sessionEvents && trace.sessionEvents.length > 0) {
|
|
1720
|
+
for (const evt of trace.sessionEvents) {
|
|
1721
|
+
const name = evt.toolName || evt.name || evt.type;
|
|
1722
|
+
if (!name) continue;
|
|
1723
|
+
activities.push({
|
|
1724
|
+
name,
|
|
1725
|
+
type: evt.type,
|
|
1726
|
+
status: evt.toolError ? "failed" : "completed",
|
|
1727
|
+
duration: evt.duration || 0
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
} else {
|
|
1731
|
+
const nodes2 = trace.nodes || {};
|
|
1732
|
+
const sorted = Object.values(nodes2).sort((a, b) => (a.startTime || 0) - (b.startTime || 0));
|
|
1733
|
+
for (const node of sorted) {
|
|
1734
|
+
activities.push({
|
|
1735
|
+
name: node.name || node.type || node.id,
|
|
1736
|
+
type: node.type || "unknown",
|
|
1737
|
+
status: node.status || "completed",
|
|
1738
|
+
duration: (node.endTime || node.startTime || 0) - (node.startTime || 0)
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
const seq = ["[START]", ...activities.map((a) => a.name), "[END]"];
|
|
1743
|
+
for (let i = 0; i < seq.length; i++) {
|
|
1744
|
+
const act = seq[i];
|
|
1745
|
+
activityCounts.set(act, (activityCounts.get(act) || 0) + 1);
|
|
1746
|
+
if (i < seq.length - 1) {
|
|
1747
|
+
const key = act + " \u2192 " + seq[i + 1];
|
|
1748
|
+
transitionCounts.set(key, (transitionCounts.get(key) || 0) + 1);
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
for (const act of activities) {
|
|
1752
|
+
if (act.duration > 0) {
|
|
1753
|
+
const durs = activityDurations.get(act.name) || [];
|
|
1754
|
+
durs.push(act.duration);
|
|
1755
|
+
activityDurations.set(act.name, durs);
|
|
1756
|
+
}
|
|
1757
|
+
const st = activityStatuses.get(act.name) || { ok: 0, fail: 0 };
|
|
1758
|
+
if (act.status === "failed") st.fail++;
|
|
1759
|
+
else st.ok++;
|
|
1760
|
+
activityStatuses.set(act.name, st);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
const nodes = Array.from(activityCounts.entries()).map(([name, count]) => {
|
|
1764
|
+
const durs = activityDurations.get(name) || [];
|
|
1765
|
+
const st = activityStatuses.get(name) || { ok: 0, fail: 0 };
|
|
1766
|
+
const avgDuration = durs.length > 0 ? durs.reduce((a, b) => a + b, 0) / durs.length : 0;
|
|
1767
|
+
return {
|
|
1768
|
+
id: name,
|
|
1769
|
+
label: name,
|
|
1770
|
+
count,
|
|
1771
|
+
frequency: count / totalTraces,
|
|
1772
|
+
avgDuration,
|
|
1773
|
+
failRate: st.ok + st.fail > 0 ? st.fail / (st.ok + st.fail) : 0,
|
|
1774
|
+
isVirtual: name === "[START]" || name === "[END]"
|
|
1775
|
+
};
|
|
1776
|
+
});
|
|
1777
|
+
const edges = Array.from(transitionCounts.entries()).map(([key, count]) => {
|
|
1778
|
+
const [source, target] = key.split(" \u2192 ");
|
|
1779
|
+
return { source, target, count, frequency: count / totalTraces };
|
|
1780
|
+
});
|
|
1781
|
+
const maxEdgeCount = Math.max(...edges.map((e) => e.count), 1);
|
|
1782
|
+
const maxNodeCount = Math.max(...nodes.filter((n) => !n.isVirtual).map((n) => n.count), 1);
|
|
1783
|
+
res.json({
|
|
1784
|
+
agentId,
|
|
1785
|
+
totalTraces,
|
|
1786
|
+
nodes,
|
|
1787
|
+
edges,
|
|
1788
|
+
maxEdgeCount,
|
|
1789
|
+
maxNodeCount
|
|
1790
|
+
});
|
|
1791
|
+
} catch (error) {
|
|
1792
|
+
console.error("Process graph error:", error);
|
|
1793
|
+
res.status(500).json({ error: "Failed to build process graph" });
|
|
1794
|
+
}
|
|
1795
|
+
});
|
|
1704
1796
|
this.app.get("/api/stats/:agentId", (req, res) => {
|
|
1705
1797
|
try {
|
|
1706
1798
|
const agentStats = this.stats.getAgentStats(req.params.agentId);
|
package/dist/server.js
CHANGED
package/package.json
CHANGED
package/public/dashboard.js
CHANGED
|
@@ -307,6 +307,12 @@ class AgentFlowDashboard {
|
|
|
307
307
|
this.selectedTrace = trace;
|
|
308
308
|
this.selectedTraceData = trace;
|
|
309
309
|
|
|
310
|
+
// Reset process map cache when agent changes
|
|
311
|
+
if (this._processMapAgent !== trace.agentId) {
|
|
312
|
+
this._processMapAgent = null;
|
|
313
|
+
if (this._cyProcessMap) { this._cyProcessMap.destroy(); this._cyProcessMap = null; }
|
|
314
|
+
}
|
|
315
|
+
|
|
310
316
|
// Update sidebar selection
|
|
311
317
|
document.querySelectorAll('.session-item').forEach(function(el) { el.classList.remove('active'); });
|
|
312
318
|
var activeEl = document.querySelector('.session-item[data-filename="' + CSS.escape(filename) + '"]');
|
|
@@ -850,6 +856,7 @@ class AgentFlowDashboard {
|
|
|
850
856
|
case 'state': this.renderStateMachine(); break;
|
|
851
857
|
case 'summary': this.renderSummary(); break;
|
|
852
858
|
case 'transcript': this.renderTranscript(); break;
|
|
859
|
+
case 'processmap': this.renderProcessMap(); break;
|
|
853
860
|
}
|
|
854
861
|
this.updateToolbarInfo();
|
|
855
862
|
}
|
|
@@ -1906,6 +1913,216 @@ class AgentFlowDashboard {
|
|
|
1906
1913
|
|
|
1907
1914
|
return 'other';
|
|
1908
1915
|
}
|
|
1916
|
+
|
|
1917
|
+
// ---------------------------------------------------------------------------
|
|
1918
|
+
// Tab 8: Process Map (Process Mining Graph)
|
|
1919
|
+
// ---------------------------------------------------------------------------
|
|
1920
|
+
renderProcessMap() {
|
|
1921
|
+
var trace = this.selectedTraceData || this.selectedTrace;
|
|
1922
|
+
if (!trace || !trace.agentId) {
|
|
1923
|
+
document.getElementById('processMapEmpty').style.display = '';
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
var agentId = trace.agentId;
|
|
1928
|
+
var self = this;
|
|
1929
|
+
|
|
1930
|
+
// Avoid re-fetching for same agent
|
|
1931
|
+
if (this._processMapAgent === agentId && this._cyProcessMap) return;
|
|
1932
|
+
this._processMapAgent = agentId;
|
|
1933
|
+
|
|
1934
|
+
document.getElementById('processMapEmpty').innerHTML =
|
|
1935
|
+
'<div class="empty-state-icon" style="animation:spin 1s linear infinite">⚙</div>' +
|
|
1936
|
+
'<div class="empty-state-text">Building process map for ' + escapeHtml(agentId) + '...</div>';
|
|
1937
|
+
document.getElementById('processMapEmpty').style.display = '';
|
|
1938
|
+
|
|
1939
|
+
fetch('/api/agents/' + encodeURIComponent(agentId) + '/process-graph')
|
|
1940
|
+
.then(function(r) { return r.json(); })
|
|
1941
|
+
.then(function(data) {
|
|
1942
|
+
if (data.error || !data.nodes || data.nodes.length === 0) {
|
|
1943
|
+
document.getElementById('processMapEmpty').innerHTML =
|
|
1944
|
+
'<div class="empty-state-icon">⚙</div>' +
|
|
1945
|
+
'<div class="empty-state-text">No process data for ' + escapeHtml(agentId) + '</div>';
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
document.getElementById('processMapEmpty').style.display = 'none';
|
|
1949
|
+
self._buildProcessMapGraph(data);
|
|
1950
|
+
})
|
|
1951
|
+
.catch(function() {
|
|
1952
|
+
document.getElementById('processMapEmpty').innerHTML =
|
|
1953
|
+
'<div class="empty-state-icon">⚙</div>' +
|
|
1954
|
+
'<div class="empty-state-text">Failed to load process map.</div>';
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
_buildProcessMapGraph(data) {
|
|
1959
|
+
if (this._cyProcessMap) { this._cyProcessMap.destroy(); this._cyProcessMap = null; }
|
|
1960
|
+
|
|
1961
|
+
var elements = [];
|
|
1962
|
+
var maxNode = data.maxNodeCount || 1;
|
|
1963
|
+
var maxEdge = data.maxEdgeCount || 1;
|
|
1964
|
+
|
|
1965
|
+
// Add nodes
|
|
1966
|
+
for (var i = 0; i < data.nodes.length; i++) {
|
|
1967
|
+
var node = data.nodes[i];
|
|
1968
|
+
// Skip very rare activities (< 2% frequency) to reduce clutter, but keep virtual nodes
|
|
1969
|
+
if (!node.isVirtual && node.frequency < 0.02 && data.nodes.length > 15) continue;
|
|
1970
|
+
|
|
1971
|
+
var size = node.isVirtual ? 30 : Math.max(25, Math.min(70, 25 + 45 * (node.count / maxNode)));
|
|
1972
|
+
var label = node.label;
|
|
1973
|
+
if (!node.isVirtual && node.count > 1) label += ' (' + node.count + ')';
|
|
1974
|
+
|
|
1975
|
+
elements.push({
|
|
1976
|
+
group: 'nodes',
|
|
1977
|
+
data: {
|
|
1978
|
+
id: node.id,
|
|
1979
|
+
label: label,
|
|
1980
|
+
count: node.count,
|
|
1981
|
+
frequency: node.frequency,
|
|
1982
|
+
avgDuration: node.avgDuration,
|
|
1983
|
+
failRate: node.failRate,
|
|
1984
|
+
isVirtual: node.isVirtual,
|
|
1985
|
+
size: size,
|
|
1986
|
+
fullData: node
|
|
1987
|
+
}
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
// Collect valid node IDs
|
|
1992
|
+
var validIds = new Set(elements.map(function(e) { return e.data.id; }));
|
|
1993
|
+
|
|
1994
|
+
// Add edges (only between valid nodes)
|
|
1995
|
+
for (var j = 0; j < data.edges.length; j++) {
|
|
1996
|
+
var edge = data.edges[j];
|
|
1997
|
+
if (!validIds.has(edge.source) || !validIds.has(edge.target)) continue;
|
|
1998
|
+
// Skip very rare transitions
|
|
1999
|
+
if (edge.frequency < 0.02 && data.edges.length > 30) continue;
|
|
2000
|
+
|
|
2001
|
+
var width = Math.max(1, Math.min(8, 1 + 7 * (edge.count / maxEdge)));
|
|
2002
|
+
var opacity = Math.max(0.3, Math.min(1.0, 0.3 + 0.7 * (edge.count / maxEdge)));
|
|
2003
|
+
|
|
2004
|
+
elements.push({
|
|
2005
|
+
group: 'edges',
|
|
2006
|
+
data: {
|
|
2007
|
+
id: 'pe-' + edge.source + '-' + edge.target,
|
|
2008
|
+
source: edge.source,
|
|
2009
|
+
target: edge.target,
|
|
2010
|
+
count: edge.count,
|
|
2011
|
+
frequency: edge.frequency,
|
|
2012
|
+
width: width,
|
|
2013
|
+
opacity: opacity,
|
|
2014
|
+
label: edge.count > 1 ? String(edge.count) : ''
|
|
2015
|
+
}
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
|
|
2019
|
+
var container = document.getElementById('cyProcessMap');
|
|
2020
|
+
var self = this;
|
|
2021
|
+
|
|
2022
|
+
this._cyProcessMap = cytoscape({
|
|
2023
|
+
container: container,
|
|
2024
|
+
elements: elements,
|
|
2025
|
+
style: [
|
|
2026
|
+
{
|
|
2027
|
+
selector: 'node',
|
|
2028
|
+
style: {
|
|
2029
|
+
'label': 'data(label)',
|
|
2030
|
+
'width': 'data(size)',
|
|
2031
|
+
'height': 'data(size)',
|
|
2032
|
+
'font-size': '9px',
|
|
2033
|
+
'text-valign': 'bottom',
|
|
2034
|
+
'text-halign': 'center',
|
|
2035
|
+
'text-margin-y': 6,
|
|
2036
|
+
'color': '#c9d1d9',
|
|
2037
|
+
'text-outline-color': '#0d1117',
|
|
2038
|
+
'text-outline-width': 2,
|
|
2039
|
+
'text-wrap': 'ellipsis',
|
|
2040
|
+
'text-max-width': '100px',
|
|
2041
|
+
'border-width': 2,
|
|
2042
|
+
'border-color': '#30363d',
|
|
2043
|
+
'background-color': '#3b82f6',
|
|
2044
|
+
'shape': 'round-rectangle'
|
|
2045
|
+
}
|
|
2046
|
+
},
|
|
2047
|
+
// Virtual START/END nodes
|
|
2048
|
+
{ selector: 'node[?isVirtual]', style: {
|
|
2049
|
+
'background-color': '#6b7280', 'shape': 'ellipse', 'border-color': '#4b5563',
|
|
2050
|
+
'font-size': '8px', 'font-weight': 'bold', 'text-valign': 'center', 'text-margin-y': 0
|
|
2051
|
+
}},
|
|
2052
|
+
// Color by fail rate: green → yellow → red
|
|
2053
|
+
{ selector: 'node[failRate <= 0]', style: { 'background-color': '#10b981', 'border-color': '#2ea043' }},
|
|
2054
|
+
{ selector: 'node[failRate > 0][failRate <= 0.1]', style: { 'background-color': '#22c55e', 'border-color': '#3fb950' }},
|
|
2055
|
+
{ selector: 'node[failRate > 0.1][failRate <= 0.3]', style: { 'background-color': '#eab308', 'border-color': '#d29922' }},
|
|
2056
|
+
{ selector: 'node[failRate > 0.3]', style: { 'background-color': '#ef4444', 'border-color': '#f85149' }},
|
|
2057
|
+
// Selected
|
|
2058
|
+
{ selector: ':selected', style: { 'border-width': 4, 'border-color': '#f59e0b', 'overlay-opacity': 0.08 }},
|
|
2059
|
+
// Edges
|
|
2060
|
+
{
|
|
2061
|
+
selector: 'edge',
|
|
2062
|
+
style: {
|
|
2063
|
+
'width': 'data(width)',
|
|
2064
|
+
'opacity': 'data(opacity)',
|
|
2065
|
+
'line-color': '#6b7280',
|
|
2066
|
+
'target-arrow-color': '#6b7280',
|
|
2067
|
+
'target-arrow-shape': 'triangle',
|
|
2068
|
+
'curve-style': 'bezier',
|
|
2069
|
+
'arrow-scale': 0.7,
|
|
2070
|
+
'label': 'data(label)',
|
|
2071
|
+
'font-size': '8px',
|
|
2072
|
+
'color': '#8b949e',
|
|
2073
|
+
'text-outline-color': '#0d1117',
|
|
2074
|
+
'text-outline-width': 1.5,
|
|
2075
|
+
'text-rotation': 'autorotate'
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
],
|
|
2079
|
+
layout: {
|
|
2080
|
+
name: 'breadthfirst',
|
|
2081
|
+
directed: true,
|
|
2082
|
+
padding: 50,
|
|
2083
|
+
spacingFactor: 1.6,
|
|
2084
|
+
animate: true,
|
|
2085
|
+
animationDuration: 400,
|
|
2086
|
+
roots: elements.filter(function(e) { return e.data && e.data.id === '[START]'; }).length > 0
|
|
2087
|
+
? ['[START]'] : undefined
|
|
2088
|
+
},
|
|
2089
|
+
minZoom: 0.15,
|
|
2090
|
+
maxZoom: 4,
|
|
2091
|
+
wheelSensitivity: 0.3
|
|
2092
|
+
});
|
|
2093
|
+
|
|
2094
|
+
// Click node → show detail
|
|
2095
|
+
this._cyProcessMap.on('tap', 'node', function(e) {
|
|
2096
|
+
var d = e.target.data().fullData;
|
|
2097
|
+
if (!d || d.isVirtual) return;
|
|
2098
|
+
var panel = document.getElementById('processMapDetailPanel');
|
|
2099
|
+
var title = document.getElementById('processMapDetailTitle');
|
|
2100
|
+
var body = document.getElementById('processMapDetailBody');
|
|
2101
|
+
|
|
2102
|
+
title.textContent = d.label;
|
|
2103
|
+
var html = '';
|
|
2104
|
+
html += self.detailRow('Occurrences', d.count);
|
|
2105
|
+
html += self.detailRow('Frequency', (d.frequency * 100).toFixed(1) + '% of traces');
|
|
2106
|
+
if (d.avgDuration > 0) html += self.detailRow('Avg Duration', self.computeDuration(0, d.avgDuration));
|
|
2107
|
+
html += self.detailRow('Failure Rate', (d.failRate * 100).toFixed(1) + '%');
|
|
2108
|
+
body.innerHTML = html;
|
|
2109
|
+
panel.classList.add('active');
|
|
2110
|
+
});
|
|
2111
|
+
|
|
2112
|
+
this._cyProcessMap.on('tap', function(e) {
|
|
2113
|
+
if (e.target === self._cyProcessMap) {
|
|
2114
|
+
document.getElementById('processMapDetailPanel').classList.remove('active');
|
|
2115
|
+
}
|
|
2116
|
+
});
|
|
2117
|
+
|
|
2118
|
+
// Close button
|
|
2119
|
+
var closeBtn = document.getElementById('processMapDetailClose');
|
|
2120
|
+
if (closeBtn) {
|
|
2121
|
+
closeBtn.onclick = function() {
|
|
2122
|
+
document.getElementById('processMapDetailPanel').classList.remove('active');
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
1909
2126
|
}
|
|
1910
2127
|
|
|
1911
2128
|
// Initialize
|
package/public/index.html
CHANGED
|
@@ -1235,6 +1235,7 @@
|
|
|
1235
1235
|
<div class="tab" data-tab="state">State Machine</div>
|
|
1236
1236
|
<div class="tab" data-tab="summary">Summary</div>
|
|
1237
1237
|
<div class="tab" data-tab="transcript">Transcript</div>
|
|
1238
|
+
<div class="tab" data-tab="processmap">Process Map</div>
|
|
1238
1239
|
</div>
|
|
1239
1240
|
|
|
1240
1241
|
<div class="toolbar" id="toolbar">
|
|
@@ -1332,6 +1333,24 @@
|
|
|
1332
1333
|
</div>
|
|
1333
1334
|
</div>
|
|
1334
1335
|
</div>
|
|
1336
|
+
|
|
1337
|
+
<!-- Process Map tab -->
|
|
1338
|
+
<div class="tab-panel" id="panel-processmap">
|
|
1339
|
+
<div id="cyProcessMap" style="width:100%;height:100%;min-height:500px;">
|
|
1340
|
+
<div class="empty-state" id="processMapEmpty">
|
|
1341
|
+
<div class="empty-state-icon">⚙</div>
|
|
1342
|
+
<div class="empty-state-title">Process Map</div>
|
|
1343
|
+
<div class="empty-state-text">Select a trace to view the process mining graph for its agent.<br>Shows activity flows aggregated across all executions.</div>
|
|
1344
|
+
</div>
|
|
1345
|
+
</div>
|
|
1346
|
+
<div class="node-detail-panel" id="processMapDetailPanel">
|
|
1347
|
+
<div class="node-detail-header">
|
|
1348
|
+
<h4 id="processMapDetailTitle">Activity Details</h4>
|
|
1349
|
+
<button class="node-detail-close" id="processMapDetailClose">×</button>
|
|
1350
|
+
</div>
|
|
1351
|
+
<div class="node-detail-body" id="processMapDetailBody"></div>
|
|
1352
|
+
</div>
|
|
1353
|
+
</div>
|
|
1335
1354
|
</div>
|
|
1336
1355
|
</main>
|
|
1337
1356
|
</div>
|