@synergenius/flow-weaver-pack-weaver 0.9.20 → 0.9.21

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.
@@ -0,0 +1,249 @@
1
+ "use strict";
2
+
3
+ // src/ui/bot-activity.tsx
4
+ var import_jsx_runtime = require("react/jsx-runtime");
5
+ var React = require("react");
6
+ var { useState, useEffect, useCallback, useRef } = React;
7
+ var { Typography } = require("@fw/plugin-ui-kit");
8
+ var { Icon } = require("@fw/plugin-ui-kit");
9
+ var { IconButton } = require("@fw/plugin-ui-kit");
10
+ var { Badge } = require("@fw/plugin-ui-kit");
11
+ var { ScrollArea } = require("@fw/plugin-ui-kit");
12
+ var { LoadingSpinner } = require("@fw/plugin-ui-kit");
13
+ var { styled } = require("@fw/plugin-theme");
14
+ var REFRESH_INTERVAL = 3e4;
15
+ function packToolUrl(toolName) {
16
+ return `/api/pack-tool/@synergenius/flow-weaver-pack-weaver/${toolName}`;
17
+ }
18
+ async function callTool(toolName, body = {}) {
19
+ const res = await fetch(packToolUrl(toolName), {
20
+ method: "POST",
21
+ headers: { "Content-Type": "application/json" },
22
+ body: JSON.stringify(body),
23
+ credentials: "include"
24
+ });
25
+ if (!res.ok) throw new Error("Request failed");
26
+ const json = await res.json();
27
+ if (json.isError) throw new Error(json.result);
28
+ try {
29
+ return JSON.parse(json.result);
30
+ } catch {
31
+ return json.result;
32
+ }
33
+ }
34
+ var Container = styled.div({
35
+ display: "flex",
36
+ flexDirection: "column",
37
+ height: "100%",
38
+ overflow: "hidden"
39
+ });
40
+ var SectionHeader = styled.div({
41
+ display: "flex",
42
+ alignItems: "center",
43
+ gap: "6px",
44
+ padding: "10px 12px 4px"
45
+ });
46
+ var RunningCard = styled.div({
47
+ margin: "4px 12px 8px",
48
+ padding: "10px 12px",
49
+ borderRadius: "$border-radius-compact",
50
+ backgroundColor: "$color-surface-elevated",
51
+ borderLeft: "3px solid $color-brand-main",
52
+ cursor: "pointer",
53
+ "&:hover": { opacity: 0.85 }
54
+ });
55
+ var RunningHeader = styled.div({
56
+ display: "flex",
57
+ alignItems: "center",
58
+ gap: "8px",
59
+ marginBottom: "6px"
60
+ });
61
+ var RunningControls = styled.div({
62
+ display: "flex",
63
+ gap: "4px",
64
+ marginLeft: "auto"
65
+ });
66
+ var QueueItem = styled.div({
67
+ display: "flex",
68
+ alignItems: "center",
69
+ gap: "8px",
70
+ padding: "6px 12px",
71
+ borderBottom: "1px solid $color-border-default"
72
+ });
73
+ var RunRow = styled.div({
74
+ display: "flex",
75
+ alignItems: "center",
76
+ gap: "8px",
77
+ padding: "8px 12px",
78
+ cursor: "pointer",
79
+ borderBottom: "1px solid $color-border-default",
80
+ "&:hover": { backgroundColor: "$color-surface-elevated" }
81
+ });
82
+ var RunText = styled.div({
83
+ flex: 1,
84
+ overflow: "hidden",
85
+ textOverflow: "ellipsis",
86
+ whiteSpace: "nowrap"
87
+ });
88
+ var KpiBar = styled.div({
89
+ display: "flex",
90
+ justifyContent: "center",
91
+ gap: "12px",
92
+ padding: "8px 12px",
93
+ borderTop: "1px solid $color-border-default",
94
+ flexShrink: 0
95
+ });
96
+ var EmptyState = styled.div({
97
+ display: "flex",
98
+ flexDirection: "column",
99
+ alignItems: "center",
100
+ justifyContent: "center",
101
+ gap: "8px",
102
+ padding: "24px 16px",
103
+ flex: 1
104
+ });
105
+ var outcomeConfig = {
106
+ completed: { icon: "taskAlt", color: "color-status-positive" },
107
+ success: { icon: "taskAlt", color: "color-status-positive" },
108
+ failed: { icon: "error", color: "color-status-negative" },
109
+ error: { icon: "error", color: "color-status-negative" },
110
+ skipped: { icon: "minus", color: "color-text-subtle" }
111
+ };
112
+ function formatTime(iso) {
113
+ try {
114
+ const diffMs = Date.now() - new Date(iso).getTime();
115
+ if (diffMs < 0) return "just now";
116
+ const diffMin = Math.floor(diffMs / 6e4);
117
+ if (diffMin < 1) return "now";
118
+ if (diffMin < 60) return `${diffMin}m ago`;
119
+ const diffH = Math.floor(diffMin / 60);
120
+ if (diffH < 24) return `${diffH}h ago`;
121
+ return `${Math.floor(diffH / 24)}d ago`;
122
+ } catch {
123
+ return "";
124
+ }
125
+ }
126
+ function healthColor(score) {
127
+ if (score >= 80) return "var(--color-status-positive)";
128
+ if (score >= 50) return "var(--color-status-caution)";
129
+ return "var(--color-status-negative)";
130
+ }
131
+ function BotActivity({ packName, botId }) {
132
+ const [history, setHistory] = useState([]);
133
+ const [queue, setQueue] = useState([]);
134
+ const [insights, setInsights] = useState(null);
135
+ const [status, setStatus] = useState(null);
136
+ const [loading, setLoading] = useState(true);
137
+ const timerRef = useRef();
138
+ const fetchAll = useCallback(async () => {
139
+ const [hist, q, ins, st] = await Promise.allSettled([
140
+ callTool("fw_weaver_history", { limit: 15 }),
141
+ callTool("fw_weaver_queue", { action: "list" }),
142
+ callTool("fw_weaver_insights"),
143
+ callTool("fw_weaver_status")
144
+ ]);
145
+ if (hist.status === "fulfilled") setHistory(Array.isArray(hist.value) ? hist.value : []);
146
+ if (q.status === "fulfilled") setQueue(Array.isArray(q.value) ? q.value : []);
147
+ if (ins.status === "fulfilled") setInsights(ins.value);
148
+ if (st.status === "fulfilled") setStatus(st.value);
149
+ setLoading(false);
150
+ }, []);
151
+ useEffect(() => {
152
+ fetchAll();
153
+ timerRef.current = setInterval(fetchAll, REFRESH_INTERVAL);
154
+ return () => clearInterval(timerRef.current);
155
+ }, [fetchAll]);
156
+ const openWorkspace = useCallback((runId) => {
157
+ window.dispatchEvent(new CustomEvent("fw:open-bot-workspace", {
158
+ detail: { runId, botId }
159
+ }));
160
+ }, [botId]);
161
+ const handleRemoveQueueItem = useCallback(async (id) => {
162
+ await callTool("fw_weaver_queue", { action: "remove", id });
163
+ setQueue((prev) => prev.filter((q) => q.id !== id));
164
+ }, []);
165
+ const handlePause = useCallback(async (e) => {
166
+ e.stopPropagation();
167
+ await callTool("fw_weaver_steer", { command: "pause" });
168
+ }, []);
169
+ const handleStop = useCallback(async (e) => {
170
+ e.stopPropagation();
171
+ await callTool("fw_weaver_steer", { command: "cancel" });
172
+ }, []);
173
+ if (loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmptyState, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoadingSpinner, { size: "small" }) });
174
+ const isRunning = status?.state && status.state !== "idle" && status.state !== "no active session";
175
+ const recentRuns = history.slice(0, 15);
176
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Container, { children: [
177
+ isRunning && status && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
178
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SectionHeader, { children: [
179
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { name: "playArrow", size: 12, color: "color-brand-main" }),
180
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Typography, { variant: "smallCaption-bold", color: "color-text-subtle", style: { textTransform: "uppercase", letterSpacing: "0.04em" }, children: "Running" })
181
+ ] }),
182
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(RunningCard, { onClick: () => openWorkspace(), children: [
183
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(RunningHeader, { children: [
184
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, { variant: "info", children: status.state }),
185
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(RunningControls, { children: [
186
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconButton, { icon: "pause", size: "xs", variant: "clear", onClick: handlePause, title: "Pause" }),
187
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconButton, { icon: "stop", size: "xs", variant: "clear", onClick: handleStop, title: "Stop" })
188
+ ] })
189
+ ] }),
190
+ status.currentTask && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Typography, { variant: "caption-regular", color: "color-text-medium", style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: status.currentTask })
191
+ ] })
192
+ ] }),
193
+ queue.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
194
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SectionHeader, { children: [
195
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { name: "pendingActions", size: 12, color: "color-text-subtle" }),
196
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Typography, { variant: "smallCaption-bold", color: "color-text-subtle", style: { textTransform: "uppercase", letterSpacing: "0.04em" }, children: [
197
+ "Queued (",
198
+ queue.length,
199
+ ")"
200
+ ] })
201
+ ] }),
202
+ queue.map((q) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(QueueItem, { children: [
203
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { name: "pending", size: 14, color: "color-text-subtle" }),
204
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Typography, { variant: "caption-regular", color: "color-text-medium", style: { flex: 1, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: q.instruction }),
205
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconButton, { icon: "close", size: "xs", variant: "clear", onClick: () => handleRemoveQueueItem(q.id), title: "Remove" })
206
+ ] }, q.id))
207
+ ] }),
208
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(SectionHeader, { children: [
209
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { name: "scheduled", size: 12, color: "color-text-subtle" }),
210
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Typography, { variant: "smallCaption-bold", color: "color-text-subtle", style: { textTransform: "uppercase", letterSpacing: "0.04em" }, children: "Recent" }),
211
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { flex: 1 } }),
212
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconButton, { icon: "refresh", size: "xs", variant: "clear", onClick: fetchAll, title: "Refresh" })
213
+ ] }),
214
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ScrollArea, { style: { flex: 1 }, children: recentRuns.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(EmptyState, { children: [
215
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { name: "info", size: 20, color: "color-text-subtle" }),
216
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Typography, { variant: "caption-regular", color: "color-text-subtle", children: "No recent runs. Ask Weaver to run a bot." })
217
+ ] }) : recentRuns.map((run, i) => {
218
+ const oc = outcomeConfig[run.outcome] ?? { icon: "help", color: "color-text-subtle" };
219
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(RunRow, { onClick: () => openWorkspace(run.id), children: [
220
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, { name: oc.icon, size: 14, color: oc.color }),
221
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RunText, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Typography, { variant: "caption-regular", color: "color-text-medium", children: run.summary ?? run.workflowFile ?? run.outcome }) }),
222
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Typography, { variant: "smallCaption-regular", color: "color-text-subtle", style: { fontFamily: "var(--typography-family-mono)", flexShrink: 0 }, children: formatTime(run.startedAt) })
223
+ ] }, run.id ?? i);
224
+ }) }),
225
+ insights && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(KpiBar, { children: [
226
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Typography, { variant: "smallCaption-regular", color: "color-text-subtle", children: [
227
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: { color: healthColor(insights.health?.overall ?? 0), fontWeight: 600 }, children: insights.health?.overall ?? 0 }),
228
+ " ",
229
+ "health"
230
+ ] }),
231
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Typography, { variant: "smallCaption-regular", color: "color-text-subtle", children: [
232
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { fontWeight: 600 }, children: [
233
+ "P",
234
+ insights.trust?.phase ?? "?"
235
+ ] }),
236
+ " ",
237
+ "trust"
238
+ ] }),
239
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Typography, { variant: "smallCaption-regular", color: "color-text-subtle", children: [
240
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { fontWeight: 600 }, children: [
241
+ "$",
242
+ (insights.cost?.last7Days ?? 0).toFixed(2)
243
+ ] }),
244
+ "/7d"
245
+ ] })
246
+ ] })
247
+ ] });
248
+ }
249
+ module.exports = BotActivity;
@@ -5,12 +5,13 @@ var import_jsx_runtime = require("react/jsx-runtime");
5
5
  var React = require("react");
6
6
  var { useState, useEffect, useCallback } = React;
7
7
  var { Typography } = require("@fw/plugin-ui-kit");
8
- var { Icon } = require("@fw/plugin-ui-kit");
9
8
  var { CollapsibleSection } = require("@fw/plugin-ui-kit");
10
9
  var { LoadingSpinner } = require("@fw/plugin-ui-kit");
11
10
  var { Banner } = require("@fw/plugin-ui-kit");
12
11
  var { IconButton } = require("@fw/plugin-ui-kit");
12
+ var { Badge } = require("@fw/plugin-ui-kit");
13
13
  var { styled } = require("@fw/plugin-theme");
14
+ var BASE = "/api/pack-tool/@synergenius/flow-weaver-pack-weaver";
14
15
  var ConfigRow = styled.div({
15
16
  display: "flex",
16
17
  justifyContent: "space-between",
@@ -19,31 +20,44 @@ var ConfigRow = styled.div({
19
20
  borderBottom: "1px solid $color-border-default",
20
21
  "&:last-of-type": { borderBottom: "none" }
21
22
  });
22
- var TOOL_URL = "/api/pack-tool/@synergenius/flow-weaver-pack-weaver/fw_weaver_insights";
23
+ var Footer = styled.div({
24
+ display: "flex",
25
+ justifyContent: "space-between",
26
+ alignItems: "center",
27
+ marginTop: "10px",
28
+ paddingTop: "6px",
29
+ borderTop: "1px solid $color-border-default"
30
+ });
31
+ function callTool(tool, body = {}) {
32
+ return fetch(`${BASE}/${tool}`, {
33
+ method: "POST",
34
+ headers: { "Content-Type": "application/json" },
35
+ body: JSON.stringify(body),
36
+ credentials: "include"
37
+ }).then(async (res) => {
38
+ if (!res.ok) throw new Error("Request failed");
39
+ const json = await res.json();
40
+ if (json.isError) throw new Error(json.result);
41
+ try {
42
+ return JSON.parse(json.result);
43
+ } catch {
44
+ return json.result;
45
+ }
46
+ });
47
+ }
23
48
  function BotConfig({ packName, botId }) {
24
- const [data, setData] = useState(null);
49
+ const [insights, setInsights] = useState(null);
50
+ const [providers, setProviders] = useState([]);
25
51
  const [error, setError] = useState(null);
26
52
  const [loading, setLoading] = useState(true);
27
53
  const fetchData = useCallback(async () => {
28
54
  try {
29
- const res = await fetch(TOOL_URL, {
30
- method: "POST",
31
- headers: { "Content-Type": "application/json" },
32
- body: JSON.stringify({}),
33
- credentials: "include"
34
- });
35
- if (!res.ok) {
36
- setError("Failed to fetch");
37
- setLoading(false);
38
- return;
39
- }
40
- const json = await res.json();
41
- if (json.isError) {
42
- setError(json.result);
43
- setLoading(false);
44
- return;
45
- }
46
- setData(JSON.parse(json.result));
55
+ const [ins, provs] = await Promise.allSettled([
56
+ callTool("fw_weaver_insights"),
57
+ callTool("fw_weaver_providers")
58
+ ]);
59
+ if (ins.status === "fulfilled") setInsights(ins.value);
60
+ if (provs.status === "fulfilled" && Array.isArray(provs.value)) setProviders(provs.value);
47
61
  setError(null);
48
62
  } catch (e) {
49
63
  setError(e.message ?? "Failed to load");
@@ -54,36 +68,44 @@ function BotConfig({ packName, botId }) {
54
68
  useEffect(() => {
55
69
  fetchData();
56
70
  }, [fetchData]);
57
- if (loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoadingSpinner, { size: "small" });
58
- if (error) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { padding: "8px 12px" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Banner, { status: "danger", size: "small", children: error }) });
59
- const bot = data?.bots?.find((b) => b.name === botId) ?? data?.bots?.[0];
60
- const provider = bot?.provider ?? "Not configured";
71
+ if (loading) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { padding: 16, display: "flex", justifyContent: "center" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(LoadingSpinner, { size: "small" }) });
72
+ if (error && !insights) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { padding: "8px 12px" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Banner, { status: "danger", size: "small", children: error }) });
73
+ const bot = insights?.bots?.find((b) => b.name === botId) ?? insights?.bots?.[0];
74
+ const configuredProvider = bot?.provider;
75
+ const availableProvider = providers.find((p) => p.envVarsSet);
76
+ const activeProvider = configuredProvider || availableProvider?.name;
77
+ const isAutoDetected = !configuredProvider && !!availableProvider;
61
78
  const approval = bot?.approvalMode ?? "auto";
62
- const hasBots = (data?.bots?.length ?? 0) > 0;
79
+ const trustPhase = insights?.trust?.phase ?? 1;
80
+ const trustScore = insights?.trust?.score ?? 0;
81
+ const noProvider = !activeProvider;
63
82
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CollapsibleSection, { title: "Configuration", defaultExpanded: true, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { padding: "4px 12px 12px" }, children: [
64
83
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ConfigRow, { children: [
65
84
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Typography, { variant: "smallCaption-bold", color: "color-text-subtle", children: "Provider" }),
66
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Typography, { variant: "caption-regular", color: "color-text-high", children: provider })
85
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
86
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Typography, { variant: "caption-regular", color: "color-text-high", children: activeProvider ?? "None detected" }),
87
+ isAutoDetected && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Badge, { variant: "default", children: "auto" })
88
+ ] })
67
89
  ] }),
68
90
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ConfigRow, { children: [
69
91
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Typography, { variant: "smallCaption-bold", color: "color-text-subtle", children: "Approval" }),
70
92
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Typography, { variant: "caption-regular", color: "color-text-high", children: approval })
71
93
  ] }),
72
94
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ConfigRow, { children: [
73
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Typography, { variant: "smallCaption-bold", color: "color-text-subtle", children: "Trust Phase" }),
95
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Typography, { variant: "smallCaption-bold", color: "color-text-subtle", children: "Trust" }),
74
96
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Typography, { variant: "caption-regular", color: "color-text-high", children: [
75
97
  "P",
76
- data?.trust?.phase ?? "?",
98
+ trustPhase,
77
99
  " (",
78
- data?.trust?.score ?? 0,
100
+ trustScore,
79
101
  "/100)"
80
102
  ] })
81
103
  ] }),
82
- !hasBots && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { marginTop: 12 }, children: [
83
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Banner, { status: "info", size: "small", children: "No .weaver.json detected. Create one to configure providers and approval modes." }),
84
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Typography, { variant: "caption-regular", color: "color-text-subtle", style: { marginTop: 6, fontFamily: "var(--typography-family-mono)" }, children: "npx flow-weaver weaver init" })
85
- ] }),
86
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { marginTop: 10, display: "flex", justifyContent: "flex-end" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconButton, { icon: "refresh", size: "xs", variant: "clear", onClick: fetchData, title: "Refresh" }) })
104
+ noProvider && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { marginTop: 10 }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Banner, { status: "warning", size: "small", children: "No AI provider detected. Set ANTHROPIC_API_KEY or install Claude CLI." }) }),
105
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Footer, { children: [
106
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Typography, { variant: "smallCaption-regular", color: "color-text-subtle", children: "Customize via .weaver.json" }),
107
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(IconButton, { icon: "refresh", size: "xs", variant: "clear", onClick: fetchData, title: "Refresh" })
108
+ ] })
87
109
  ] }) });
88
110
  }
89
111
  module.exports = BotConfig;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifestVersion": 2,
3
3
  "name": "@synergenius/flow-weaver-pack-weaver",
4
- "version": "0.9.20",
4
+ "version": "0.9.21",
5
5
  "description": "AI bot for Flow Weaver. Execute tasks, run workflows, evolve autonomously.",
6
6
  "engineVersion": ">=0.22.10",
7
7
  "categories": [
@@ -1074,6 +1074,7 @@
1074
1074
  ],
1075
1075
  "uiContributions": [],
1076
1076
  "botUI": {
1077
+ "activity": "dist/ui/bot-activity.js",
1077
1078
  "config": "dist/ui/bot-config.js",
1078
1079
  "status": "dist/ui/bot-status.js",
1079
1080
  "dashboard": "dist/ui/bot-dashboard.js"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synergenius/flow-weaver-pack-weaver",
3
- "version": "0.9.20",
3
+ "version": "0.9.21",
4
4
  "description": "AI bot for Flow Weaver. Execute tasks, run workflows, evolve autonomously.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -0,0 +1,343 @@
1
+ /**
2
+ * Weaver Bot Activity Panel — sidebar activity feed.
3
+ * Shows running bot, queued tasks, recent runs, and KPI bar.
4
+ * Runs in the platform bot-panel sandbox.
5
+ */
6
+ const React = require('react');
7
+ const { useState, useEffect, useCallback, useRef } = React;
8
+ const { Typography } = require('@fw/plugin-ui-kit');
9
+ const { Icon } = require('@fw/plugin-ui-kit');
10
+ const { IconButton } = require('@fw/plugin-ui-kit');
11
+ const { Badge } = require('@fw/plugin-ui-kit');
12
+ const { ScrollArea } = require('@fw/plugin-ui-kit');
13
+ const { LoadingSpinner } = require('@fw/plugin-ui-kit');
14
+ const { styled } = require('@fw/plugin-theme');
15
+
16
+ const REFRESH_INTERVAL = 30_000;
17
+
18
+ function packToolUrl(toolName: string) {
19
+ // packName is injected via props — but for now the component knows its own pack
20
+ return `/api/pack-tool/@synergenius/flow-weaver-pack-weaver/${toolName}`;
21
+ }
22
+
23
+ async function callTool(toolName: string, body: Record<string, unknown> = {}) {
24
+ const res = await fetch(packToolUrl(toolName), {
25
+ method: 'POST',
26
+ headers: { 'Content-Type': 'application/json' },
27
+ body: JSON.stringify(body),
28
+ credentials: 'include',
29
+ });
30
+ if (!res.ok) throw new Error('Request failed');
31
+ const json = await res.json();
32
+ if (json.isError) throw new Error(json.result);
33
+ try { return JSON.parse(json.result); } catch { return json.result; }
34
+ }
35
+
36
+ // --- Types ---
37
+
38
+ interface HistoryEntry {
39
+ id?: string;
40
+ outcome: string;
41
+ summary?: string;
42
+ workflowFile?: string;
43
+ startedAt: string;
44
+ duration?: number;
45
+ }
46
+
47
+ interface InsightsData {
48
+ health: { overall: number };
49
+ trust: { phase: number; score: number };
50
+ cost: { last7Days: number };
51
+ }
52
+
53
+ interface QueueEntry {
54
+ id: string;
55
+ instruction: string;
56
+ priority: number;
57
+ }
58
+
59
+ // --- Styled Components ---
60
+
61
+ const Container = styled.div({
62
+ display: 'flex',
63
+ flexDirection: 'column',
64
+ height: '100%',
65
+ overflow: 'hidden',
66
+ });
67
+
68
+ const SectionHeader = styled.div({
69
+ display: 'flex',
70
+ alignItems: 'center',
71
+ gap: '6px',
72
+ padding: '10px 12px 4px',
73
+ });
74
+
75
+ const RunningCard = styled.div({
76
+ margin: '4px 12px 8px',
77
+ padding: '10px 12px',
78
+ borderRadius: '$border-radius-compact',
79
+ backgroundColor: '$color-surface-elevated',
80
+ borderLeft: '3px solid $color-brand-main',
81
+ cursor: 'pointer',
82
+ '&:hover': { opacity: 0.85 },
83
+ });
84
+
85
+ const RunningHeader = styled.div({
86
+ display: 'flex',
87
+ alignItems: 'center',
88
+ gap: '8px',
89
+ marginBottom: '6px',
90
+ });
91
+
92
+ const RunningControls = styled.div({
93
+ display: 'flex',
94
+ gap: '4px',
95
+ marginLeft: 'auto',
96
+ });
97
+
98
+ const QueueItem = styled.div({
99
+ display: 'flex',
100
+ alignItems: 'center',
101
+ gap: '8px',
102
+ padding: '6px 12px',
103
+ borderBottom: '1px solid $color-border-default',
104
+ });
105
+
106
+ const RunRow = styled.div({
107
+ display: 'flex',
108
+ alignItems: 'center',
109
+ gap: '8px',
110
+ padding: '8px 12px',
111
+ cursor: 'pointer',
112
+ borderBottom: '1px solid $color-border-default',
113
+ '&:hover': { backgroundColor: '$color-surface-elevated' },
114
+ });
115
+
116
+ const RunText = styled.div({
117
+ flex: 1,
118
+ overflow: 'hidden',
119
+ textOverflow: 'ellipsis',
120
+ whiteSpace: 'nowrap',
121
+ });
122
+
123
+ const KpiBar = styled.div({
124
+ display: 'flex',
125
+ justifyContent: 'center',
126
+ gap: '12px',
127
+ padding: '8px 12px',
128
+ borderTop: '1px solid $color-border-default',
129
+ flexShrink: 0,
130
+ });
131
+
132
+ const EmptyState = styled.div({
133
+ display: 'flex',
134
+ flexDirection: 'column',
135
+ alignItems: 'center',
136
+ justifyContent: 'center',
137
+ gap: '8px',
138
+ padding: '24px 16px',
139
+ flex: 1,
140
+ });
141
+
142
+ // --- Helpers ---
143
+
144
+ const outcomeConfig: Record<string, { icon: string; color: string }> = {
145
+ completed: { icon: 'taskAlt', color: 'color-status-positive' },
146
+ success: { icon: 'taskAlt', color: 'color-status-positive' },
147
+ failed: { icon: 'error', color: 'color-status-negative' },
148
+ error: { icon: 'error', color: 'color-status-negative' },
149
+ skipped: { icon: 'minus', color: 'color-text-subtle' },
150
+ };
151
+
152
+ function formatTime(iso: string): string {
153
+ try {
154
+ const diffMs = Date.now() - new Date(iso).getTime();
155
+ if (diffMs < 0) return 'just now';
156
+ const diffMin = Math.floor(diffMs / 60_000);
157
+ if (diffMin < 1) return 'now';
158
+ if (diffMin < 60) return `${diffMin}m ago`;
159
+ const diffH = Math.floor(diffMin / 60);
160
+ if (diffH < 24) return `${diffH}h ago`;
161
+ return `${Math.floor(diffH / 24)}d ago`;
162
+ } catch { return ''; }
163
+ }
164
+
165
+ function healthColor(score: number): string {
166
+ if (score >= 80) return 'var(--color-status-positive)';
167
+ if (score >= 50) return 'var(--color-status-caution)';
168
+ return 'var(--color-status-negative)';
169
+ }
170
+
171
+ // --- Component ---
172
+
173
+ function BotActivity({ packName, botId }: { packName: string; botId: string }) {
174
+ const [history, setHistory] = useState<HistoryEntry[]>([]);
175
+ const [queue, setQueue] = useState<QueueEntry[]>([]);
176
+ const [insights, setInsights] = useState<InsightsData | null>(null);
177
+ const [status, setStatus] = useState<{ state: string; currentTask?: string } | null>(null);
178
+ const [loading, setLoading] = useState(true);
179
+ const timerRef = useRef<ReturnType<typeof setInterval>>();
180
+
181
+ const fetchAll = useCallback(async () => {
182
+ const [hist, q, ins, st] = await Promise.allSettled([
183
+ callTool('fw_weaver_history', { limit: 15 }),
184
+ callTool('fw_weaver_queue', { action: 'list' }),
185
+ callTool('fw_weaver_insights'),
186
+ callTool('fw_weaver_status'),
187
+ ]);
188
+ if (hist.status === 'fulfilled') setHistory(Array.isArray(hist.value) ? hist.value : []);
189
+ if (q.status === 'fulfilled') setQueue(Array.isArray(q.value) ? q.value : []);
190
+ if (ins.status === 'fulfilled') setInsights(ins.value);
191
+ if (st.status === 'fulfilled') setStatus(st.value);
192
+ setLoading(false);
193
+ }, []);
194
+
195
+ useEffect(() => {
196
+ fetchAll();
197
+ timerRef.current = setInterval(fetchAll, REFRESH_INTERVAL);
198
+ return () => clearInterval(timerRef.current);
199
+ }, [fetchAll]);
200
+
201
+ // Open center workspace via platform's window system
202
+ const openWorkspace = useCallback((runId?: string) => {
203
+ // Dispatch custom event that the platform's BotPanel listens for
204
+ window.dispatchEvent(new CustomEvent('fw:open-bot-workspace', {
205
+ detail: { runId, botId },
206
+ }));
207
+ }, [botId]);
208
+
209
+ const handleRemoveQueueItem = useCallback(async (id: string) => {
210
+ await callTool('fw_weaver_queue', { action: 'remove', id });
211
+ setQueue((prev) => prev.filter((q) => q.id !== id));
212
+ }, []);
213
+
214
+ const handlePause = useCallback(async (e: React.MouseEvent) => {
215
+ e.stopPropagation();
216
+ await callTool('fw_weaver_steer', { command: 'pause' });
217
+ }, []);
218
+
219
+ const handleStop = useCallback(async (e: React.MouseEvent) => {
220
+ e.stopPropagation();
221
+ await callTool('fw_weaver_steer', { command: 'cancel' });
222
+ }, []);
223
+
224
+ if (loading) return (
225
+ <EmptyState>
226
+ <LoadingSpinner size="small" />
227
+ </EmptyState>
228
+ );
229
+
230
+ const isRunning = status?.state && status.state !== 'idle' && status.state !== 'no active session';
231
+ const recentRuns = history.slice(0, 15);
232
+
233
+ return (
234
+ <Container>
235
+ {/* RUNNING */}
236
+ {isRunning && status && (
237
+ <>
238
+ <SectionHeader>
239
+ <Icon name="playArrow" size={12} color="color-brand-main" />
240
+ <Typography variant="smallCaption-bold" color="color-text-subtle" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>
241
+ Running
242
+ </Typography>
243
+ </SectionHeader>
244
+ <RunningCard onClick={() => openWorkspace()}>
245
+ <RunningHeader>
246
+ <Badge variant="info">{status.state}</Badge>
247
+ <RunningControls>
248
+ <IconButton icon="pause" size="xs" variant="clear" onClick={handlePause} title="Pause" />
249
+ <IconButton icon="stop" size="xs" variant="clear" onClick={handleStop} title="Stop" />
250
+ </RunningControls>
251
+ </RunningHeader>
252
+ {status.currentTask && (
253
+ <Typography variant="caption-regular" color="color-text-medium" style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
254
+ {status.currentTask}
255
+ </Typography>
256
+ )}
257
+ </RunningCard>
258
+ </>
259
+ )}
260
+
261
+ {/* QUEUED */}
262
+ {queue.length > 0 && (
263
+ <>
264
+ <SectionHeader>
265
+ <Icon name="pendingActions" size={12} color="color-text-subtle" />
266
+ <Typography variant="smallCaption-bold" color="color-text-subtle" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>
267
+ Queued ({queue.length})
268
+ </Typography>
269
+ </SectionHeader>
270
+ {queue.map((q: QueueEntry) => (
271
+ <QueueItem key={q.id}>
272
+ <Icon name="pending" size={14} color="color-text-subtle" />
273
+ <Typography variant="caption-regular" color="color-text-medium" style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
274
+ {q.instruction}
275
+ </Typography>
276
+ <IconButton icon="close" size="xs" variant="clear" onClick={() => handleRemoveQueueItem(q.id)} title="Remove" />
277
+ </QueueItem>
278
+ ))}
279
+ </>
280
+ )}
281
+
282
+ {/* RECENT */}
283
+ <SectionHeader>
284
+ <Icon name="scheduled" size={12} color="color-text-subtle" />
285
+ <Typography variant="smallCaption-bold" color="color-text-subtle" style={{ textTransform: 'uppercase', letterSpacing: '0.04em' }}>
286
+ Recent
287
+ </Typography>
288
+ <div style={{ flex: 1 }} />
289
+ <IconButton icon="refresh" size="xs" variant="clear" onClick={fetchAll} title="Refresh" />
290
+ </SectionHeader>
291
+
292
+ <ScrollArea style={{ flex: 1 }}>
293
+ {recentRuns.length === 0 ? (
294
+ <EmptyState>
295
+ <Icon name="info" size={20} color="color-text-subtle" />
296
+ <Typography variant="caption-regular" color="color-text-subtle">
297
+ No recent runs. Ask Weaver to run a bot.
298
+ </Typography>
299
+ </EmptyState>
300
+ ) : (
301
+ recentRuns.map((run: HistoryEntry, i: number) => {
302
+ const oc = outcomeConfig[run.outcome] ?? { icon: 'help', color: 'color-text-subtle' };
303
+ return (
304
+ <RunRow key={run.id ?? i} onClick={() => openWorkspace(run.id)}>
305
+ <Icon name={oc.icon as any} size={14} color={oc.color} />
306
+ <RunText>
307
+ <Typography variant="caption-regular" color="color-text-medium">
308
+ {run.summary ?? run.workflowFile ?? run.outcome}
309
+ </Typography>
310
+ </RunText>
311
+ <Typography variant="smallCaption-regular" color="color-text-subtle" style={{ fontFamily: 'var(--typography-family-mono)', flexShrink: 0 }}>
312
+ {formatTime(run.startedAt)}
313
+ </Typography>
314
+ </RunRow>
315
+ );
316
+ })
317
+ )}
318
+ </ScrollArea>
319
+
320
+ {/* KPI BAR */}
321
+ {insights && (
322
+ <KpiBar>
323
+ <Typography variant="smallCaption-regular" color="color-text-subtle">
324
+ <span style={{ color: healthColor(insights.health?.overall ?? 0), fontWeight: 600 }}>
325
+ {insights.health?.overall ?? 0}
326
+ </span>
327
+ {' '}health
328
+ </Typography>
329
+ <Typography variant="smallCaption-regular" color="color-text-subtle">
330
+ <span style={{ fontWeight: 600 }}>P{insights.trust?.phase ?? '?'}</span>
331
+ {' '}trust
332
+ </Typography>
333
+ <Typography variant="smallCaption-regular" color="color-text-subtle">
334
+ <span style={{ fontWeight: 600 }}>${(insights.cost?.last7Days ?? 0).toFixed(2)}</span>
335
+ /7d
336
+ </Typography>
337
+ </KpiBar>
338
+ )}
339
+ </Container>
340
+ );
341
+ }
342
+
343
+ module.exports = BotActivity;
@@ -1,17 +1,19 @@
1
1
  /**
2
- * Weaver Bot Config Panel — read-only configuration summary.
2
+ * Weaver Bot Config Panel — shows active configuration (detected + configured).
3
3
  * Runs in the platform bot-panel sandbox.
4
4
  */
5
5
  const React = require('react');
6
6
  const { useState, useEffect, useCallback } = React;
7
7
  const { Typography } = require('@fw/plugin-ui-kit');
8
- const { Icon } = require('@fw/plugin-ui-kit');
9
8
  const { CollapsibleSection } = require('@fw/plugin-ui-kit');
10
9
  const { LoadingSpinner } = require('@fw/plugin-ui-kit');
11
10
  const { Banner } = require('@fw/plugin-ui-kit');
12
11
  const { IconButton } = require('@fw/plugin-ui-kit');
12
+ const { Badge } = require('@fw/plugin-ui-kit');
13
13
  const { styled } = require('@fw/plugin-theme');
14
14
 
15
+ const BASE = '/api/pack-tool/@synergenius/flow-weaver-pack-weaver';
16
+
15
17
  const ConfigRow = styled.div({
16
18
  display: 'flex',
17
19
  justifyContent: 'space-between',
@@ -21,7 +23,20 @@ const ConfigRow = styled.div({
21
23
  '&:last-of-type': { borderBottom: 'none' },
22
24
  });
23
25
 
24
- const TOOL_URL = '/api/pack-tool/@synergenius/flow-weaver-pack-weaver/fw_weaver_insights';
26
+ const Footer = styled.div({
27
+ display: 'flex',
28
+ justifyContent: 'space-between',
29
+ alignItems: 'center',
30
+ marginTop: '10px',
31
+ paddingTop: '6px',
32
+ borderTop: '1px solid $color-border-default',
33
+ });
34
+
35
+ interface ProviderInfo {
36
+ name: string;
37
+ source: string;
38
+ envVarsSet: boolean;
39
+ }
25
40
 
26
41
  interface InsightsResult {
27
42
  health: { overall: number };
@@ -29,23 +44,34 @@ interface InsightsResult {
29
44
  trust: { phase: number; score: number };
30
45
  }
31
46
 
47
+ function callTool(tool: string, body: Record<string, unknown> = {}) {
48
+ return fetch(`${BASE}/${tool}`, {
49
+ method: 'POST',
50
+ headers: { 'Content-Type': 'application/json' },
51
+ body: JSON.stringify(body),
52
+ credentials: 'include',
53
+ }).then(async res => {
54
+ if (!res.ok) throw new Error('Request failed');
55
+ const json = await res.json();
56
+ if (json.isError) throw new Error(json.result);
57
+ try { return JSON.parse(json.result); } catch { return json.result; }
58
+ });
59
+ }
60
+
32
61
  function BotConfig({ packName, botId }: { packName: string; botId: string }) {
33
- const [data, setData] = useState<InsightsResult | null>(null);
62
+ const [insights, setInsights] = useState<InsightsResult | null>(null);
63
+ const [providers, setProviders] = useState<ProviderInfo[]>([]);
34
64
  const [error, setError] = useState<string | null>(null);
35
65
  const [loading, setLoading] = useState(true);
36
66
 
37
67
  const fetchData = useCallback(async () => {
38
68
  try {
39
- const res = await fetch(TOOL_URL, {
40
- method: 'POST',
41
- headers: { 'Content-Type': 'application/json' },
42
- body: JSON.stringify({}),
43
- credentials: 'include',
44
- });
45
- if (!res.ok) { setError('Failed to fetch'); setLoading(false); return; }
46
- const json = await res.json();
47
- if (json.isError) { setError(json.result); setLoading(false); return; }
48
- setData(JSON.parse(json.result));
69
+ const [ins, provs] = await Promise.allSettled([
70
+ callTool('fw_weaver_insights'),
71
+ callTool('fw_weaver_providers'),
72
+ ]);
73
+ if (ins.status === 'fulfilled') setInsights(ins.value);
74
+ if (provs.status === 'fulfilled' && Array.isArray(provs.value)) setProviders(provs.value);
49
75
  setError(null);
50
76
  } catch (e: any) {
51
77
  setError(e.message ?? 'Failed to load');
@@ -56,49 +82,66 @@ function BotConfig({ packName, botId }: { packName: string; botId: string }) {
56
82
 
57
83
  useEffect(() => { fetchData(); }, [fetchData]);
58
84
 
59
- if (loading) return <LoadingSpinner size="small" />;
85
+ if (loading) return (
86
+ <div style={{ padding: 16, display: 'flex', justifyContent: 'center' }}>
87
+ <LoadingSpinner size="small" />
88
+ </div>
89
+ );
60
90
 
61
- if (error) return (
91
+ if (error && !insights) return (
62
92
  <div style={{ padding: '8px 12px' }}>
63
93
  <Banner status="danger" size="small">{error}</Banner>
64
94
  </div>
65
95
  );
66
96
 
67
- const bot = data?.bots?.find((b: any) => b.name === botId) ?? data?.bots?.[0];
68
- const provider = bot?.provider ?? 'Not configured';
97
+ // Determine active provider prefer configured, fall back to first available
98
+ const bot = insights?.bots?.find((b: any) => b.name === botId) ?? insights?.bots?.[0];
99
+ const configuredProvider = bot?.provider;
100
+ const availableProvider = providers.find((p: ProviderInfo) => p.envVarsSet);
101
+ const activeProvider = configuredProvider || availableProvider?.name;
102
+ const isAutoDetected = !configuredProvider && !!availableProvider;
69
103
  const approval = bot?.approvalMode ?? 'auto';
70
- const hasBots = (data?.bots?.length ?? 0) > 0;
104
+ const trustPhase = insights?.trust?.phase ?? 1;
105
+ const trustScore = insights?.trust?.score ?? 0;
106
+ const noProvider = !activeProvider;
71
107
 
72
108
  return (
73
109
  <CollapsibleSection title="Configuration" defaultExpanded>
74
110
  <div style={{ padding: '4px 12px 12px' }}>
75
111
  <ConfigRow>
76
112
  <Typography variant="smallCaption-bold" color="color-text-subtle">Provider</Typography>
77
- <Typography variant="caption-regular" color="color-text-high">{provider}</Typography>
113
+ <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
114
+ <Typography variant="caption-regular" color="color-text-high">
115
+ {activeProvider ?? 'None detected'}
116
+ </Typography>
117
+ {isAutoDetected && <Badge variant="default">auto</Badge>}
118
+ </span>
78
119
  </ConfigRow>
79
120
  <ConfigRow>
80
121
  <Typography variant="smallCaption-bold" color="color-text-subtle">Approval</Typography>
81
122
  <Typography variant="caption-regular" color="color-text-high">{approval}</Typography>
82
123
  </ConfigRow>
83
124
  <ConfigRow>
84
- <Typography variant="smallCaption-bold" color="color-text-subtle">Trust Phase</Typography>
85
- <Typography variant="caption-regular" color="color-text-high">P{data?.trust?.phase ?? '?'} ({data?.trust?.score ?? 0}/100)</Typography>
125
+ <Typography variant="smallCaption-bold" color="color-text-subtle">Trust</Typography>
126
+ <Typography variant="caption-regular" color="color-text-high">
127
+ P{trustPhase} ({trustScore}/100)
128
+ </Typography>
86
129
  </ConfigRow>
87
130
 
88
- {!hasBots && (
89
- <div style={{ marginTop: 12 }}>
90
- <Banner status="info" size="small">
91
- No .weaver.json detected. Create one to configure providers and approval modes.
131
+ {noProvider && (
132
+ <div style={{ marginTop: 10 }}>
133
+ <Banner status="warning" size="small">
134
+ No AI provider detected. Set ANTHROPIC_API_KEY or install Claude CLI.
92
135
  </Banner>
93
- <Typography variant="caption-regular" color="color-text-subtle" style={{ marginTop: 6, fontFamily: 'var(--typography-family-mono)' }}>
94
- npx flow-weaver weaver init
95
- </Typography>
96
136
  </div>
97
137
  )}
98
138
 
99
- <div style={{ marginTop: 10, display: 'flex', justifyContent: 'flex-end' }}>
139
+ <Footer>
140
+ <Typography variant="smallCaption-regular" color="color-text-subtle">
141
+ Customize via .weaver.json
142
+ </Typography>
100
143
  <IconButton icon="refresh" size="xs" variant="clear" onClick={fetchData} title="Refresh" />
101
- </div>
144
+ </Footer>
102
145
  </div>
103
146
  </CollapsibleSection>
104
147
  );