@synergenius/flow-weaver-pack-weaver 0.9.13 → 0.9.15
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/ui/evolution-panel.js +129 -56
- package/dist/ui/insights-widget.js +69 -37
- package/flowweaver.manifest.json +2 -2
- package/package.json +1 -1
- package/src/ui/evolution-panel.tsx +155 -70
- package/src/ui/insights-widget.tsx +86 -33
|
@@ -26,71 +26,144 @@ __export(evolution_panel_exports, {
|
|
|
26
26
|
module.exports = __toCommonJS(evolution_panel_exports);
|
|
27
27
|
var import_react = require("react");
|
|
28
28
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
29
|
-
function WeaverEvolutionPanel(
|
|
30
|
-
const [
|
|
29
|
+
function WeaverEvolutionPanel() {
|
|
30
|
+
const [health, setHealth] = (0, import_react.useState)(null);
|
|
31
|
+
const [improve, setImprove] = (0, import_react.useState)(null);
|
|
31
32
|
const [error, setError] = (0, import_react.useState)(null);
|
|
33
|
+
const [loading, setLoading] = (0, import_react.useState)(true);
|
|
34
|
+
const [deviceId, setDeviceId] = (0, import_react.useState)(null);
|
|
35
|
+
const [deviceName, setDeviceName] = (0, import_react.useState)("");
|
|
36
|
+
const [improving, setImproving] = (0, import_react.useState)(false);
|
|
37
|
+
const fetchData = (0, import_react.useCallback)(async () => {
|
|
38
|
+
try {
|
|
39
|
+
const devRes = await fetch("/api/devices", { credentials: "include" });
|
|
40
|
+
if (!devRes.ok) {
|
|
41
|
+
setError("Not connected");
|
|
42
|
+
setLoading(false);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const devices = await devRes.json();
|
|
46
|
+
const device = devices.find((d) => d.capabilities?.includes("improve"));
|
|
47
|
+
if (!device) {
|
|
48
|
+
setError("No device with improve capability");
|
|
49
|
+
setLoading(false);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
setDeviceId(device.id);
|
|
53
|
+
setDeviceName(device.name);
|
|
54
|
+
const [healthRes, improveRes] = await Promise.allSettled([
|
|
55
|
+
fetch(`/api/devices/${device.id}/health`, { credentials: "include" }),
|
|
56
|
+
fetch(`/api/devices/${device.id}/improve/status`, { credentials: "include" })
|
|
57
|
+
]);
|
|
58
|
+
if (healthRes.status === "fulfilled" && healthRes.value.ok) {
|
|
59
|
+
setHealth(await healthRes.value.json());
|
|
60
|
+
}
|
|
61
|
+
if (improveRes.status === "fulfilled" && improveRes.value.ok) {
|
|
62
|
+
const data = await improveRes.value.json();
|
|
63
|
+
setImprove(data);
|
|
64
|
+
setImproving(data.running ?? false);
|
|
65
|
+
}
|
|
66
|
+
setError(null);
|
|
67
|
+
} catch (e) {
|
|
68
|
+
setError(e instanceof Error ? e.message : "Failed to load");
|
|
69
|
+
} finally {
|
|
70
|
+
setLoading(false);
|
|
71
|
+
}
|
|
72
|
+
}, []);
|
|
32
73
|
(0, import_react.useEffect)(() => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
74
|
+
fetchData();
|
|
75
|
+
const interval = setInterval(fetchData, 15e3);
|
|
76
|
+
return () => clearInterval(interval);
|
|
77
|
+
}, [fetchData]);
|
|
78
|
+
const startImprove = async () => {
|
|
79
|
+
if (!deviceId) return;
|
|
80
|
+
setImproving(true);
|
|
81
|
+
try {
|
|
82
|
+
await fetch(`/api/devices/${deviceId}/improve`, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: { "Content-Type": "application/json" },
|
|
85
|
+
credentials: "include",
|
|
86
|
+
body: JSON.stringify({ maxCycles: 5 })
|
|
87
|
+
});
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
48
90
|
};
|
|
49
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
91
|
+
if (loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { padding: 16, opacity: 0.5 }, children: "Loading..." });
|
|
92
|
+
if (error) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { padding: 16 }, children: [
|
|
93
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: "#ef4444", marginBottom: 8 }, children: error }),
|
|
94
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { onClick: fetchData, style: { fontSize: 12, color: "#6b7280", background: "none", border: "1px solid #333", borderRadius: 4, padding: "4px 8px", cursor: "pointer" }, children: "Retry" })
|
|
95
|
+
] });
|
|
96
|
+
const trustPhase = health?.trust?.phase ?? 1;
|
|
97
|
+
const trustScore = health?.trust?.score ?? 0;
|
|
98
|
+
const healthScore = health?.health?.overall ?? 0;
|
|
99
|
+
const outcomeIcon = { success: "\u2713", failure: "\u2717" };
|
|
100
|
+
const outcomeColor = { success: "#22c55e", failure: "#ef4444" };
|
|
101
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { padding: 16, fontFamily: "system-ui, sans-serif", fontSize: 13, color: "var(--color-text-high, #e5e5e5)" }, children: [
|
|
102
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 10, textTransform: "uppercase", letterSpacing: "0.05em", opacity: 0.5, marginBottom: 12 }, children: deviceName }),
|
|
103
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 16, marginBottom: 16 }, children: [
|
|
104
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { flex: 1, padding: 12, border: "1px solid rgba(128,128,128,0.2)", borderRadius: 6 }, children: [
|
|
105
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { fontSize: 20, fontWeight: 700 }, children: [
|
|
106
|
+
"P",
|
|
107
|
+
trustPhase
|
|
56
108
|
] }),
|
|
57
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("
|
|
58
|
-
"
|
|
59
|
-
|
|
109
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { fontSize: 10, opacity: 0.5, textTransform: "uppercase", marginTop: 2 }, children: [
|
|
110
|
+
"Trust \xB7 ",
|
|
111
|
+
trustScore,
|
|
60
112
|
"/100"
|
|
61
113
|
] })
|
|
114
|
+
] }),
|
|
115
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { flex: 1, padding: 12, border: "1px solid rgba(128,128,128,0.2)", borderRadius: 6 }, children: [
|
|
116
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 20, fontWeight: 700 }, children: healthScore }),
|
|
117
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 10, opacity: 0.5, textTransform: "uppercase", marginTop: 2 }, children: "Health" })
|
|
62
118
|
] })
|
|
63
119
|
] }),
|
|
64
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
65
|
-
"
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
/* @__PURE__ */ (0, import_jsx_runtime.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
120
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginBottom: 16 }, children: [
|
|
121
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 8 }, children: [
|
|
122
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 600, fontSize: 11, textTransform: "uppercase", letterSpacing: "0.04em", opacity: 0.6 }, children: "Improve" }),
|
|
123
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
124
|
+
"button",
|
|
125
|
+
{
|
|
126
|
+
onClick: startImprove,
|
|
127
|
+
disabled: improving,
|
|
128
|
+
style: {
|
|
129
|
+
fontSize: 11,
|
|
130
|
+
fontWeight: 500,
|
|
131
|
+
padding: "3px 10px",
|
|
132
|
+
borderRadius: 4,
|
|
133
|
+
border: "1px solid rgba(128,128,128,0.3)",
|
|
134
|
+
background: improving ? "rgba(34,197,94,0.1)" : "transparent",
|
|
135
|
+
color: improving ? "#22c55e" : "var(--color-text-medium, #999)",
|
|
136
|
+
cursor: improving ? "default" : "pointer",
|
|
137
|
+
opacity: improving ? 0.8 : 1
|
|
138
|
+
},
|
|
139
|
+
children: improving ? "Running..." : "Start Improve"
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
] }),
|
|
143
|
+
improve?.lastRun && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
144
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 8, marginBottom: 8 }, children: [
|
|
145
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { color: "#22c55e", fontWeight: 600 }, children: [
|
|
146
|
+
improve.lastRun.successes,
|
|
147
|
+
" committed"
|
|
148
|
+
] }),
|
|
149
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { color: "#ef4444", fontWeight: 600 }, children: [
|
|
150
|
+
improve.lastRun.failures,
|
|
151
|
+
" rolled back"
|
|
152
|
+
] }),
|
|
153
|
+
improve.lastRun.skips > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { opacity: 0.5 }, children: [
|
|
154
|
+
improve.lastRun.skips,
|
|
155
|
+
" skipped"
|
|
156
|
+
] })
|
|
157
|
+
] }),
|
|
158
|
+
improve.lastRun.cycles.slice(-5).reverse().map((cy) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { padding: "4px 0", borderBottom: "1px solid rgba(128,128,128,0.1)", display: "flex", alignItems: "flex-start", gap: 6 }, children: [
|
|
159
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: outcomeColor[cy.outcome] ?? "#6b7280", fontWeight: 700, fontSize: 12, width: 14, textAlign: "center", flexShrink: 0 }, children: outcomeIcon[cy.outcome] ?? "\u25CB" }),
|
|
160
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { flex: 1, opacity: 0.8 }, children: cy.description.slice(0, 80) }),
|
|
161
|
+
cy.commitHash && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontFamily: "monospace", fontSize: 10, opacity: 0.4 }, children: cy.commitHash })
|
|
162
|
+
] }, cy.cycle))
|
|
163
|
+
] }),
|
|
164
|
+
!improve?.lastRun && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { opacity: 0.5, textAlign: "center", padding: 16 }, children: 'No improve runs yet. Click "Start Improve" to begin.' })
|
|
92
165
|
] }),
|
|
93
|
-
|
|
166
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { textAlign: "right" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { onClick: fetchData, style: { fontSize: 11, color: "#6b7280", background: "none", border: "none", cursor: "pointer" }, children: "Refresh" }) })
|
|
94
167
|
] });
|
|
95
168
|
}
|
|
96
169
|
var evolution_panel_default = WeaverEvolutionPanel;
|
|
@@ -26,58 +26,90 @@ __export(insights_widget_exports, {
|
|
|
26
26
|
module.exports = __toCommonJS(insights_widget_exports);
|
|
27
27
|
var import_react = require("react");
|
|
28
28
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
29
|
-
function WeaverInsightsWidget(
|
|
29
|
+
function WeaverInsightsWidget() {
|
|
30
30
|
const [data, setData] = (0, import_react.useState)(null);
|
|
31
31
|
const [error, setError] = (0, import_react.useState)(null);
|
|
32
|
+
const [loading, setLoading] = (0, import_react.useState)(true);
|
|
33
|
+
const [deviceName, setDeviceName] = (0, import_react.useState)("");
|
|
34
|
+
const fetchData = (0, import_react.useCallback)(async () => {
|
|
35
|
+
try {
|
|
36
|
+
const devRes = await fetch("/api/devices", { credentials: "include" });
|
|
37
|
+
if (!devRes.ok) {
|
|
38
|
+
setError("Not connected");
|
|
39
|
+
setLoading(false);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const devices = await devRes.json();
|
|
43
|
+
const device = devices.find((d) => d.capabilities?.includes("insights"));
|
|
44
|
+
if (!device) {
|
|
45
|
+
setError("No device with insights capability");
|
|
46
|
+
setLoading(false);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
setDeviceName(device.name);
|
|
50
|
+
const [healthRes, insightsRes] = await Promise.allSettled([
|
|
51
|
+
fetch(`/api/devices/${device.id}/health`, { credentials: "include" }),
|
|
52
|
+
fetch(`/api/devices/${device.id}/insights`, { credentials: "include" })
|
|
53
|
+
]);
|
|
54
|
+
const health = healthRes.status === "fulfilled" && healthRes.value.ok ? await healthRes.value.json() : null;
|
|
55
|
+
const insights = insightsRes.status === "fulfilled" && insightsRes.value.ok ? await insightsRes.value.json() : [];
|
|
56
|
+
setData({
|
|
57
|
+
health: health?.health ?? { overall: 0, workflows: [] },
|
|
58
|
+
bots: health?.bots ?? [],
|
|
59
|
+
insights: Array.isArray(insights) ? insights : [],
|
|
60
|
+
cost: health?.cost ?? { last7Days: 0, trend: "stable" },
|
|
61
|
+
trust: health?.trust ?? { phase: 1, score: 0 }
|
|
62
|
+
});
|
|
63
|
+
setError(null);
|
|
64
|
+
} catch (e) {
|
|
65
|
+
setError(e instanceof Error ? e.message : "Failed to load");
|
|
66
|
+
} finally {
|
|
67
|
+
setLoading(false);
|
|
68
|
+
}
|
|
69
|
+
}, []);
|
|
32
70
|
(0, import_react.useEffect)(() => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
71
|
+
fetchData();
|
|
72
|
+
const interval = setInterval(fetchData, 3e4);
|
|
73
|
+
return () => clearInterval(interval);
|
|
74
|
+
}, [fetchData]);
|
|
75
|
+
if (loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { padding: 16, opacity: 0.5 }, children: "Loading insights..." });
|
|
76
|
+
if (error) return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { padding: 16 }, children: [
|
|
77
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: "#ef4444", marginBottom: 8 }, children: error }),
|
|
78
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { onClick: fetchData, style: { fontSize: 12, color: "#6b7280", background: "none", border: "1px solid #333", borderRadius: 4, padding: "4px 8px", cursor: "pointer" }, children: "Retry" })
|
|
39
79
|
] });
|
|
40
|
-
if (!data) return
|
|
41
|
-
const severityColor = {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
};
|
|
46
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { padding: 16, fontFamily: "system-ui, sans-serif", fontSize: 13 }, children: [
|
|
47
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "baseline", gap: 12, marginBottom: 12 }, children: [
|
|
80
|
+
if (!data) return null;
|
|
81
|
+
const severityColor = { critical: "#ef4444", warning: "#f59e0b", info: "#6b7280" };
|
|
82
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { padding: 16, fontFamily: "system-ui, sans-serif", fontSize: 13, color: "var(--color-text-high, #e5e5e5)" }, children: [
|
|
83
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 10, textTransform: "uppercase", letterSpacing: "0.05em", opacity: 0.5, marginBottom: 12 }, children: deviceName }),
|
|
84
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "baseline", gap: 12, marginBottom: 16 }, children: [
|
|
48
85
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { fontSize: 28, fontWeight: 700 }, children: data.health.overall }),
|
|
49
86
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { opacity: 0.6 }, children: "/100 health" }),
|
|
50
87
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { marginLeft: "auto", opacity: 0.5 }, children: [
|
|
51
|
-
"
|
|
88
|
+
"P",
|
|
52
89
|
data.trust.phase,
|
|
53
90
|
" \xB7 $",
|
|
54
91
|
data.cost.last7Days.toFixed(2),
|
|
55
|
-
"/7d
|
|
56
|
-
data.cost.trend,
|
|
57
|
-
")"
|
|
92
|
+
"/7d"
|
|
58
93
|
] })
|
|
59
94
|
] }),
|
|
60
|
-
data.insights.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginBottom:
|
|
61
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 600, marginBottom: 6 }, children: "Insights" }),
|
|
62
|
-
data.insights.slice(0,
|
|
63
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: severityColor[insight.severity] ?? "#6b7280", fontWeight: 600, marginRight: 8 }, children: insight.severity
|
|
64
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: insight.title })
|
|
65
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { opacity: 0.5, marginLeft: 8 }, children: [
|
|
66
|
-
Math.round(insight.confidence * 100),
|
|
67
|
-
"%"
|
|
68
|
-
] })
|
|
95
|
+
data.insights.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginBottom: 16 }, children: [
|
|
96
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 600, marginBottom: 6, fontSize: 11, textTransform: "uppercase", letterSpacing: "0.04em", opacity: 0.6 }, children: "Insights" }),
|
|
97
|
+
data.insights.slice(0, 5).map((insight, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { padding: "6px 0", borderBottom: "1px solid rgba(128,128,128,0.15)" }, children: [
|
|
98
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: severityColor[insight.severity] ?? "#6b7280", fontWeight: 600, marginRight: 8, fontSize: 10, textTransform: "uppercase" }, children: insight.severity }),
|
|
99
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: insight.title })
|
|
69
100
|
] }, i))
|
|
70
101
|
] }),
|
|
102
|
+
data.insights.length === 0 && data.health.overall === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { opacity: 0.5, textAlign: "center", padding: 20 }, children: "No data yet. Connect a device and run some workflows." }),
|
|
71
103
|
data.bots.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
|
|
72
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 600, marginBottom: 6 }, children: "Bots" }),
|
|
73
|
-
data.bots.map((bot, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { padding: "
|
|
74
|
-
bot.name,
|
|
75
|
-
":
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
104
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontWeight: 600, marginBottom: 6, fontSize: 11, textTransform: "uppercase", letterSpacing: "0.04em", opacity: 0.6 }, children: "Bots" }),
|
|
105
|
+
data.bots.map((bot, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { padding: "4px 0", display: "flex", justifyContent: "space-between" }, children: [
|
|
106
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: bot.name }),
|
|
107
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { opacity: 0.6 }, children: [
|
|
108
|
+
Math.round(bot.successRate * 100),
|
|
109
|
+
"% (",
|
|
110
|
+
bot.totalTasksRun,
|
|
111
|
+
")"
|
|
112
|
+
] })
|
|
81
113
|
] }, i))
|
|
82
114
|
] })
|
|
83
115
|
] });
|
package/flowweaver.manifest.json
CHANGED
|
@@ -1077,7 +1077,7 @@
|
|
|
1077
1077
|
"type": "dashboard-widget",
|
|
1078
1078
|
"id": "weaver-insights",
|
|
1079
1079
|
"name": "Project Insights",
|
|
1080
|
-
"component": "insights-widget.js",
|
|
1080
|
+
"component": "dist/ui/insights-widget.js",
|
|
1081
1081
|
"area": "dashboard",
|
|
1082
1082
|
"size": "large"
|
|
1083
1083
|
},
|
|
@@ -1085,7 +1085,7 @@
|
|
|
1085
1085
|
"type": "panel",
|
|
1086
1086
|
"id": "weaver-evolution",
|
|
1087
1087
|
"name": "Evolution History",
|
|
1088
|
-
"component": "evolution-panel.js"
|
|
1088
|
+
"component": "dist/ui/evolution-panel.js"
|
|
1089
1089
|
}
|
|
1090
1090
|
],
|
|
1091
1091
|
"sandboxCapabilities": [
|
package/package.json
CHANGED
|
@@ -1,94 +1,179 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Weaver Evolution Panel — shows Genesis cycle history
|
|
3
|
-
*
|
|
2
|
+
* Weaver Evolution Panel — shows Genesis cycle history, trust level, improve status.
|
|
3
|
+
* Fetches data from the connected device via platform relay endpoints.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React, { useEffect, useState } from 'react';
|
|
6
|
+
import React, { useEffect, useState, useCallback } from 'react';
|
|
7
7
|
|
|
8
|
-
interface
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
interface ImproveStatus {
|
|
9
|
+
running: boolean;
|
|
10
|
+
lastRun?: {
|
|
11
|
+
successes: number;
|
|
12
|
+
failures: number;
|
|
13
|
+
skips: number;
|
|
14
|
+
cycles: Array<{ cycle: number; outcome: string; description: string; commitHash?: string }>;
|
|
14
15
|
};
|
|
15
|
-
trust: { phase: number; score: number };
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
interface HealthData {
|
|
19
|
+
health?: { overall: number };
|
|
20
|
+
trust?: { phase: number; score: number };
|
|
21
|
+
cost?: { last7Days: number; trend: string };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface Device {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
capabilities: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function WeaverEvolutionPanel() {
|
|
31
|
+
const [health, setHealth] = useState<HealthData | null>(null);
|
|
32
|
+
const [improve, setImprove] = useState<ImproveStatus | null>(null);
|
|
20
33
|
const [error, setError] = useState<string | null>(null);
|
|
34
|
+
const [loading, setLoading] = useState(true);
|
|
35
|
+
const [deviceId, setDeviceId] = useState<string | null>(null);
|
|
36
|
+
const [deviceName, setDeviceName] = useState('');
|
|
37
|
+
const [improving, setImproving] = useState(false);
|
|
38
|
+
|
|
39
|
+
const fetchData = useCallback(async () => {
|
|
40
|
+
try {
|
|
41
|
+
const devRes = await fetch('/api/devices', { credentials: 'include' });
|
|
42
|
+
if (!devRes.ok) { setError('Not connected'); setLoading(false); return; }
|
|
43
|
+
const devices: Device[] = await devRes.json();
|
|
44
|
+
const device = devices.find(d => d.capabilities?.includes('improve'));
|
|
45
|
+
if (!device) { setError('No device with improve capability'); setLoading(false); return; }
|
|
46
|
+
setDeviceId(device.id);
|
|
47
|
+
setDeviceName(device.name);
|
|
48
|
+
|
|
49
|
+
const [healthRes, improveRes] = await Promise.allSettled([
|
|
50
|
+
fetch(`/api/devices/${device.id}/health`, { credentials: 'include' }),
|
|
51
|
+
fetch(`/api/devices/${device.id}/improve/status`, { credentials: 'include' }),
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
if (healthRes.status === 'fulfilled' && healthRes.value.ok) {
|
|
55
|
+
setHealth(await healthRes.value.json());
|
|
56
|
+
}
|
|
57
|
+
if (improveRes.status === 'fulfilled' && improveRes.value.ok) {
|
|
58
|
+
const data = await improveRes.value.json();
|
|
59
|
+
setImprove(data);
|
|
60
|
+
setImproving(data.running ?? false);
|
|
61
|
+
}
|
|
62
|
+
setError(null);
|
|
63
|
+
} catch (e) {
|
|
64
|
+
setError(e instanceof Error ? e.message : 'Failed to load');
|
|
65
|
+
} finally {
|
|
66
|
+
setLoading(false);
|
|
67
|
+
}
|
|
68
|
+
}, []);
|
|
21
69
|
|
|
22
70
|
useEffect(() => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
error: '#ef4444',
|
|
71
|
+
fetchData();
|
|
72
|
+
const interval = setInterval(fetchData, 15_000);
|
|
73
|
+
return () => clearInterval(interval);
|
|
74
|
+
}, [fetchData]);
|
|
75
|
+
|
|
76
|
+
const startImprove = async () => {
|
|
77
|
+
if (!deviceId) return;
|
|
78
|
+
setImproving(true);
|
|
79
|
+
try {
|
|
80
|
+
await fetch(`/api/devices/${deviceId}/improve`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: { 'Content-Type': 'application/json' },
|
|
83
|
+
credentials: 'include',
|
|
84
|
+
body: JSON.stringify({ maxCycles: 5 }),
|
|
85
|
+
});
|
|
86
|
+
} catch { /* ignore */ }
|
|
40
87
|
};
|
|
41
88
|
|
|
89
|
+
if (loading) return <div style={{ padding: 16, opacity: 0.5 }}>Loading...</div>;
|
|
90
|
+
if (error) return (
|
|
91
|
+
<div style={{ padding: 16 }}>
|
|
92
|
+
<div style={{ color: '#ef4444', marginBottom: 8 }}>{error}</div>
|
|
93
|
+
<button onClick={fetchData} style={{ fontSize: 12, color: '#6b7280', background: 'none', border: '1px solid #333', borderRadius: 4, padding: '4px 8px', cursor: 'pointer' }}>Retry</button>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const trustPhase = health?.trust?.phase ?? 1;
|
|
98
|
+
const trustScore = health?.trust?.score ?? 0;
|
|
99
|
+
const healthScore = health?.health?.overall ?? 0;
|
|
100
|
+
|
|
101
|
+
const outcomeIcon: Record<string, string> = { success: '\u2713', failure: '\u2717' };
|
|
102
|
+
const outcomeColor: Record<string, string> = { success: '#22c55e', failure: '#ef4444' };
|
|
103
|
+
|
|
42
104
|
return (
|
|
43
|
-
<div style={{ padding: 16, fontFamily: 'system-ui, sans-serif', fontSize: 13 }}>
|
|
44
|
-
<div style={{ marginBottom:
|
|
45
|
-
|
|
46
|
-
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
|
|
47
|
-
<span style={{ fontSize: 20, fontWeight: 700 }}>Phase {trust.phase}</span>
|
|
48
|
-
<span style={{ opacity: 0.5 }}>Score: {trust.score}/100</span>
|
|
49
|
-
</div>
|
|
105
|
+
<div style={{ padding: 16, fontFamily: 'system-ui, sans-serif', fontSize: 13, color: 'var(--color-text-high, #e5e5e5)' }}>
|
|
106
|
+
<div style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: '0.05em', opacity: 0.5, marginBottom: 12 }}>
|
|
107
|
+
{deviceName}
|
|
50
108
|
</div>
|
|
51
109
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
110
|
+
{/* Trust + Health */}
|
|
111
|
+
<div style={{ display: 'flex', gap: 16, marginBottom: 16 }}>
|
|
112
|
+
<div style={{ flex: 1, padding: 12, border: '1px solid rgba(128,128,128,0.2)', borderRadius: 6 }}>
|
|
113
|
+
<div style={{ fontSize: 20, fontWeight: 700 }}>P{trustPhase}</div>
|
|
114
|
+
<div style={{ fontSize: 10, opacity: 0.5, textTransform: 'uppercase', marginTop: 2 }}>Trust · {trustScore}/100</div>
|
|
115
|
+
</div>
|
|
116
|
+
<div style={{ flex: 1, padding: 12, border: '1px solid rgba(128,128,128,0.2)', borderRadius: 6 }}>
|
|
117
|
+
<div style={{ fontSize: 20, fontWeight: 700 }}>{healthScore}</div>
|
|
118
|
+
<div style={{ fontSize: 10, opacity: 0.5, textTransform: 'uppercase', marginTop: 2 }}>Health</div>
|
|
55
119
|
</div>
|
|
56
120
|
</div>
|
|
57
121
|
|
|
58
|
-
{
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
{
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
122
|
+
{/* Improve */}
|
|
123
|
+
<div style={{ marginBottom: 16 }}>
|
|
124
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 8 }}>
|
|
125
|
+
<div style={{ fontWeight: 600, fontSize: 11, textTransform: 'uppercase', letterSpacing: '0.04em', opacity: 0.6 }}>
|
|
126
|
+
Improve
|
|
127
|
+
</div>
|
|
128
|
+
<button
|
|
129
|
+
onClick={startImprove}
|
|
130
|
+
disabled={improving}
|
|
131
|
+
style={{
|
|
132
|
+
fontSize: 11,
|
|
133
|
+
fontWeight: 500,
|
|
134
|
+
padding: '3px 10px',
|
|
135
|
+
borderRadius: 4,
|
|
136
|
+
border: '1px solid rgba(128,128,128,0.3)',
|
|
137
|
+
background: improving ? 'rgba(34,197,94,0.1)' : 'transparent',
|
|
138
|
+
color: improving ? '#22c55e' : 'var(--color-text-medium, #999)',
|
|
139
|
+
cursor: improving ? 'default' : 'pointer',
|
|
140
|
+
opacity: improving ? 0.8 : 1,
|
|
141
|
+
}}
|
|
142
|
+
>
|
|
143
|
+
{improving ? 'Running...' : 'Start Improve'}
|
|
144
|
+
</button>
|
|
67
145
|
</div>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
<span style={{ color: outcomeColor[cycle.outcome] ?? '#6b7280', fontWeight: 600, marginRight: 8 }}>
|
|
76
|
-
{cycle.outcome}
|
|
77
|
-
</span>
|
|
78
|
-
<span style={{ opacity: 0.7 }}>{cycle.id}</span>
|
|
79
|
-
{cycle.proposal && (
|
|
80
|
-
<div style={{ opacity: 0.6, paddingLeft: 8 }}>{cycle.proposal.summary}</div>
|
|
81
|
-
)}
|
|
146
|
+
|
|
147
|
+
{improve?.lastRun && (
|
|
148
|
+
<>
|
|
149
|
+
<div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
|
|
150
|
+
<span style={{ color: '#22c55e', fontWeight: 600 }}>{improve.lastRun.successes} committed</span>
|
|
151
|
+
<span style={{ color: '#ef4444', fontWeight: 600 }}>{improve.lastRun.failures} rolled back</span>
|
|
152
|
+
{improve.lastRun.skips > 0 && <span style={{ opacity: 0.5 }}>{improve.lastRun.skips} skipped</span>}
|
|
82
153
|
</div>
|
|
83
|
-
))}
|
|
84
|
-
</div>
|
|
85
|
-
)}
|
|
86
154
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
155
|
+
{improve.lastRun.cycles.slice(-5).reverse().map(cy => (
|
|
156
|
+
<div key={cy.cycle} style={{ padding: '4px 0', borderBottom: '1px solid rgba(128,128,128,0.1)', display: 'flex', alignItems: 'flex-start', gap: 6 }}>
|
|
157
|
+
<span style={{ color: outcomeColor[cy.outcome] ?? '#6b7280', fontWeight: 700, fontSize: 12, width: 14, textAlign: 'center', flexShrink: 0 }}>
|
|
158
|
+
{outcomeIcon[cy.outcome] ?? '\u25CB'}
|
|
159
|
+
</span>
|
|
160
|
+
<span style={{ flex: 1, opacity: 0.8 }}>{cy.description.slice(0, 80)}</span>
|
|
161
|
+
{cy.commitHash && <span style={{ fontFamily: 'monospace', fontSize: 10, opacity: 0.4 }}>{cy.commitHash}</span>}
|
|
162
|
+
</div>
|
|
163
|
+
))}
|
|
164
|
+
</>
|
|
165
|
+
)}
|
|
166
|
+
|
|
167
|
+
{!improve?.lastRun && (
|
|
168
|
+
<div style={{ opacity: 0.5, textAlign: 'center', padding: 16 }}>
|
|
169
|
+
No improve runs yet. Click "Start Improve" to begin.
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<div style={{ textAlign: 'right' }}>
|
|
175
|
+
<button onClick={fetchData} style={{ fontSize: 11, color: '#6b7280', background: 'none', border: 'none', cursor: 'pointer' }}>Refresh</button>
|
|
176
|
+
</div>
|
|
92
177
|
</div>
|
|
93
178
|
);
|
|
94
179
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Weaver Insights
|
|
3
|
-
*
|
|
2
|
+
* Weaver Insights Widget — shows project health, insights, and cost summary.
|
|
3
|
+
* Fetches data from the connected device via platform relay endpoints.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import React, { useEffect, useState } from 'react';
|
|
6
|
+
import React, { useEffect, useState, useCallback } from 'react';
|
|
7
7
|
|
|
8
8
|
interface InsightsData {
|
|
9
9
|
health: { overall: number; workflows: Array<{ file: string; score: number; trend: string }> };
|
|
@@ -13,59 +13,112 @@ interface InsightsData {
|
|
|
13
13
|
trust: { phase: number; score: number };
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
interface Device {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
capabilities: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function WeaverInsightsWidget() {
|
|
17
23
|
const [data, setData] = useState<InsightsData | null>(null);
|
|
18
24
|
const [error, setError] = useState<string | null>(null);
|
|
25
|
+
const [loading, setLoading] = useState(true);
|
|
26
|
+
const [deviceName, setDeviceName] = useState('');
|
|
27
|
+
|
|
28
|
+
const fetchData = useCallback(async () => {
|
|
29
|
+
try {
|
|
30
|
+
// Find first connected device with insights capability
|
|
31
|
+
const devRes = await fetch('/api/devices', { credentials: 'include' });
|
|
32
|
+
if (!devRes.ok) { setError('Not connected'); setLoading(false); return; }
|
|
33
|
+
const devices: Device[] = await devRes.json();
|
|
34
|
+
const device = devices.find(d => d.capabilities?.includes('insights'));
|
|
35
|
+
if (!device) { setError('No device with insights capability'); setLoading(false); return; }
|
|
36
|
+
setDeviceName(device.name);
|
|
37
|
+
|
|
38
|
+
// Fetch health + insights from device
|
|
39
|
+
const [healthRes, insightsRes] = await Promise.allSettled([
|
|
40
|
+
fetch(`/api/devices/${device.id}/health`, { credentials: 'include' }),
|
|
41
|
+
fetch(`/api/devices/${device.id}/insights`, { credentials: 'include' }),
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const health = healthRes.status === 'fulfilled' && healthRes.value.ok
|
|
45
|
+
? await healthRes.value.json() : null;
|
|
46
|
+
const insights = insightsRes.status === 'fulfilled' && insightsRes.value.ok
|
|
47
|
+
? await insightsRes.value.json() : [];
|
|
48
|
+
|
|
49
|
+
setData({
|
|
50
|
+
health: health?.health ?? { overall: 0, workflows: [] },
|
|
51
|
+
bots: health?.bots ?? [],
|
|
52
|
+
insights: Array.isArray(insights) ? insights : [],
|
|
53
|
+
cost: health?.cost ?? { last7Days: 0, trend: 'stable' },
|
|
54
|
+
trust: health?.trust ?? { phase: 1, score: 0 },
|
|
55
|
+
});
|
|
56
|
+
setError(null);
|
|
57
|
+
} catch (e) {
|
|
58
|
+
setError(e instanceof Error ? e.message : 'Failed to load');
|
|
59
|
+
} finally {
|
|
60
|
+
setLoading(false);
|
|
61
|
+
}
|
|
62
|
+
}, []);
|
|
19
63
|
|
|
20
64
|
useEffect(() => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
};
|
|
65
|
+
fetchData();
|
|
66
|
+
const interval = setInterval(fetchData, 30_000);
|
|
67
|
+
return () => clearInterval(interval);
|
|
68
|
+
}, [fetchData]);
|
|
69
|
+
|
|
70
|
+
if (loading) return <div style={{ padding: 16, opacity: 0.5 }}>Loading insights...</div>;
|
|
71
|
+
if (error) return (
|
|
72
|
+
<div style={{ padding: 16 }}>
|
|
73
|
+
<div style={{ color: '#ef4444', marginBottom: 8 }}>{error}</div>
|
|
74
|
+
<button onClick={fetchData} style={{ fontSize: 12, color: '#6b7280', background: 'none', border: '1px solid #333', borderRadius: 4, padding: '4px 8px', cursor: 'pointer' }}>Retry</button>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
if (!data) return null;
|
|
78
|
+
|
|
79
|
+
const severityColor: Record<string, string> = { critical: '#ef4444', warning: '#f59e0b', info: '#6b7280' };
|
|
36
80
|
|
|
37
81
|
return (
|
|
38
|
-
<div style={{ padding: 16, fontFamily: 'system-ui, sans-serif', fontSize: 13 }}>
|
|
39
|
-
<div style={{
|
|
82
|
+
<div style={{ padding: 16, fontFamily: 'system-ui, sans-serif', fontSize: 13, color: 'var(--color-text-high, #e5e5e5)' }}>
|
|
83
|
+
<div style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: '0.05em', opacity: 0.5, marginBottom: 12 }}>
|
|
84
|
+
{deviceName}
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div style={{ display: 'flex', alignItems: 'baseline', gap: 12, marginBottom: 16 }}>
|
|
40
88
|
<span style={{ fontSize: 28, fontWeight: 700 }}>{data.health.overall}</span>
|
|
41
89
|
<span style={{ opacity: 0.6 }}>/100 health</span>
|
|
42
90
|
<span style={{ marginLeft: 'auto', opacity: 0.5 }}>
|
|
43
|
-
|
|
91
|
+
P{data.trust.phase} · ${data.cost.last7Days.toFixed(2)}/7d
|
|
44
92
|
</span>
|
|
45
93
|
</div>
|
|
46
94
|
|
|
47
95
|
{data.insights.length > 0 && (
|
|
48
|
-
<div style={{ marginBottom:
|
|
49
|
-
<div style={{ fontWeight: 600, marginBottom: 6 }}>Insights</div>
|
|
50
|
-
{data.insights.slice(0,
|
|
51
|
-
<div key={i} style={{ padding: '
|
|
52
|
-
<span style={{ color: severityColor[insight.severity] ?? '#6b7280', fontWeight: 600, marginRight: 8 }}>
|
|
53
|
-
{insight.severity
|
|
96
|
+
<div style={{ marginBottom: 16 }}>
|
|
97
|
+
<div style={{ fontWeight: 600, marginBottom: 6, fontSize: 11, textTransform: 'uppercase', letterSpacing: '0.04em', opacity: 0.6 }}>Insights</div>
|
|
98
|
+
{data.insights.slice(0, 5).map((insight, i) => (
|
|
99
|
+
<div key={i} style={{ padding: '6px 0', borderBottom: '1px solid rgba(128,128,128,0.15)' }}>
|
|
100
|
+
<span style={{ color: severityColor[insight.severity] ?? '#6b7280', fontWeight: 600, marginRight: 8, fontSize: 10, textTransform: 'uppercase' }}>
|
|
101
|
+
{insight.severity}
|
|
54
102
|
</span>
|
|
55
103
|
<span>{insight.title}</span>
|
|
56
|
-
<span style={{ opacity: 0.5, marginLeft: 8 }}>{Math.round(insight.confidence * 100)}%</span>
|
|
57
104
|
</div>
|
|
58
105
|
))}
|
|
59
106
|
</div>
|
|
60
107
|
)}
|
|
61
108
|
|
|
109
|
+
{data.insights.length === 0 && data.health.overall === 0 && (
|
|
110
|
+
<div style={{ opacity: 0.5, textAlign: 'center', padding: 20 }}>
|
|
111
|
+
No data yet. Connect a device and run some workflows.
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
|
|
62
115
|
{data.bots.length > 0 && (
|
|
63
116
|
<div>
|
|
64
|
-
<div style={{ fontWeight: 600, marginBottom: 6 }}>Bots</div>
|
|
117
|
+
<div style={{ fontWeight: 600, marginBottom: 6, fontSize: 11, textTransform: 'uppercase', letterSpacing: '0.04em', opacity: 0.6 }}>Bots</div>
|
|
65
118
|
{data.bots.map((bot, i) => (
|
|
66
|
-
<div key={i} style={{ padding: '
|
|
67
|
-
{bot.name}
|
|
68
|
-
|
|
119
|
+
<div key={i} style={{ padding: '4px 0', display: 'flex', justifyContent: 'space-between' }}>
|
|
120
|
+
<span>{bot.name}</span>
|
|
121
|
+
<span style={{ opacity: 0.6 }}>{Math.round(bot.successRate * 100)}% ({bot.totalTasksRun})</span>
|
|
69
122
|
</div>
|
|
70
123
|
))}
|
|
71
124
|
</div>
|