@contractspec/lib.example-shared-ui 0.0.0-canary-20260113170453
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/.turbo/turbo-build$colon$bundle.log +9 -0
- package/.turbo/turbo-build.log +11 -0
- package/CHANGELOG.md +34 -0
- package/dist/index.mjs +3121 -0
- package/package.json +43 -0
- package/src/EvolutionDashboard.tsx +480 -0
- package/src/EvolutionSidebar.tsx +282 -0
- package/src/LocalDataIndicator.tsx +39 -0
- package/src/MarkdownView.tsx +389 -0
- package/src/OverlayContextProvider.tsx +341 -0
- package/src/PersonalizationInsights.tsx +293 -0
- package/src/SaveToStudioButton.tsx +64 -0
- package/src/SpecEditorPanel.tsx +165 -0
- package/src/TemplateShell.tsx +63 -0
- package/src/hooks/index.ts +5 -0
- package/src/hooks/useBehaviorTracking.ts +327 -0
- package/src/hooks/useEvolution.ts +501 -0
- package/src/hooks/useRegistryTemplates.ts +49 -0
- package/src/hooks/useSpecContent.ts +243 -0
- package/src/hooks/useWorkflowComposer.ts +670 -0
- package/src/index.ts +15 -0
- package/src/lib/component-registry.tsx +64 -0
- package/src/lib/runtime-context.tsx +54 -0
- package/src/lib/types.ts +84 -0
- package/src/overlay-types.ts +25 -0
- package/src/utils/fetchPresentationData.ts +48 -0
- package/src/utils/generateSpecFromTemplate.ts +458 -0
- package/src/utils/index.ts +2 -0
- package/tsconfig.json +10 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,3121 @@
|
|
|
1
|
+
import { RefreshCw, Shield, Sparkles } from "lucide-react";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { Button, ErrorState, LoaderBlock } from "@contractspec/lib.design-system";
|
|
6
|
+
import { Card } from "@contractspec/lib.ui-kit-web/ui/card";
|
|
7
|
+
import { Badge } from "@contractspec/lib.ui-kit-web/ui/badge";
|
|
8
|
+
import { useQuery } from "@tanstack/react-query";
|
|
9
|
+
|
|
10
|
+
//#region src/lib/runtime-context.tsx
|
|
11
|
+
const TemplateRuntimeContext = createContext(null);
|
|
12
|
+
function useTemplateRuntime() {
|
|
13
|
+
const context = useContext(TemplateRuntimeContext);
|
|
14
|
+
if (!context) throw new Error("useTemplateRuntime must be used within a TemplateRuntimeProvider");
|
|
15
|
+
return context;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/LocalDataIndicator.tsx
|
|
20
|
+
function LocalDataIndicator() {
|
|
21
|
+
const { projectId, templateId, template, installer } = useTemplateRuntime();
|
|
22
|
+
const [isResetting, setIsResetting] = useState(false);
|
|
23
|
+
const handleReset = async () => {
|
|
24
|
+
setIsResetting(true);
|
|
25
|
+
try {
|
|
26
|
+
await installer.install(templateId, { projectId });
|
|
27
|
+
} finally {
|
|
28
|
+
setIsResetting(false);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
32
|
+
className: "border-border bg-muted/40 text-muted-foreground inline-flex items-center gap-2 rounded-full border px-3 py-1 text-xs",
|
|
33
|
+
children: [
|
|
34
|
+
/* @__PURE__ */ jsx(Shield, { className: "h-3.5 w-3.5 text-violet-400" }),
|
|
35
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
36
|
+
"Local runtime ·",
|
|
37
|
+
" ",
|
|
38
|
+
/* @__PURE__ */ jsx("span", {
|
|
39
|
+
className: "text-foreground font-semibold",
|
|
40
|
+
children: template.name
|
|
41
|
+
})
|
|
42
|
+
] }),
|
|
43
|
+
/* @__PURE__ */ jsxs("button", {
|
|
44
|
+
type: "button",
|
|
45
|
+
className: "border-border text-muted-foreground hover:text-foreground inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-[11px] font-semibold",
|
|
46
|
+
onClick: handleReset,
|
|
47
|
+
disabled: isResetting,
|
|
48
|
+
children: [/* @__PURE__ */ jsx(RefreshCw, { className: "h-3 w-3" }), isResetting ? "Resetting…" : "Reset data"]
|
|
49
|
+
})
|
|
50
|
+
]
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/SaveToStudioButton.tsx
|
|
56
|
+
function SaveToStudioButton({ organizationId = "demo-org", projectName, endpoint, token }) {
|
|
57
|
+
const { installer, templateId, template } = useTemplateRuntime();
|
|
58
|
+
const [status, setStatus] = useState("idle");
|
|
59
|
+
const [error, setError] = useState(null);
|
|
60
|
+
const handleSave = async () => {
|
|
61
|
+
setStatus("saving");
|
|
62
|
+
setError(null);
|
|
63
|
+
try {
|
|
64
|
+
await installer.saveToStudio({
|
|
65
|
+
templateId,
|
|
66
|
+
projectName: projectName ?? `${template.name} demo`,
|
|
67
|
+
organizationId,
|
|
68
|
+
endpoint,
|
|
69
|
+
token
|
|
70
|
+
});
|
|
71
|
+
setStatus("saved");
|
|
72
|
+
setTimeout(() => setStatus("idle"), 3e3);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
setStatus("error");
|
|
75
|
+
setError(err instanceof Error ? err.message : "Unknown error");
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
79
|
+
className: "flex flex-col items-end gap-1",
|
|
80
|
+
children: [
|
|
81
|
+
/* @__PURE__ */ jsxs("button", {
|
|
82
|
+
type: "button",
|
|
83
|
+
className: "btn-primary inline-flex items-center gap-2 text-sm",
|
|
84
|
+
onClick: handleSave,
|
|
85
|
+
disabled: status === "saving",
|
|
86
|
+
children: [/* @__PURE__ */ jsx(Sparkles, { className: "h-4 w-4" }), status === "saving" ? "Publishing…" : "Save to Studio"]
|
|
87
|
+
}),
|
|
88
|
+
status === "error" && error ? /* @__PURE__ */ jsx("p", {
|
|
89
|
+
className: "text-destructive text-xs",
|
|
90
|
+
children: error
|
|
91
|
+
}) : null,
|
|
92
|
+
status === "saved" ? /* @__PURE__ */ jsx("p", {
|
|
93
|
+
className: "text-xs text-emerald-400",
|
|
94
|
+
children: "Template sent to Studio."
|
|
95
|
+
}) : null
|
|
96
|
+
]
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/TemplateShell.tsx
|
|
102
|
+
const TemplateShell = ({ title, description, sidebar, actions, showSaveAction = true, saveProps, children }) => /* @__PURE__ */ jsxs("div", {
|
|
103
|
+
className: "space-y-6",
|
|
104
|
+
children: [/* @__PURE__ */ jsxs("header", {
|
|
105
|
+
className: "border-border bg-card rounded-2xl border p-6 shadow-sm",
|
|
106
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
107
|
+
className: "flex flex-wrap items-center justify-between gap-4",
|
|
108
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [
|
|
109
|
+
/* @__PURE__ */ jsx("p", {
|
|
110
|
+
className: "text-muted-foreground text-sm font-semibold tracking-wide uppercase",
|
|
111
|
+
children: "ContractSpec Templates"
|
|
112
|
+
}),
|
|
113
|
+
/* @__PURE__ */ jsx("h1", {
|
|
114
|
+
className: "text-3xl font-bold",
|
|
115
|
+
children: title
|
|
116
|
+
}),
|
|
117
|
+
description ? /* @__PURE__ */ jsx("p", {
|
|
118
|
+
className: "text-muted-foreground mt-2 max-w-2xl text-sm",
|
|
119
|
+
children: description
|
|
120
|
+
}) : null
|
|
121
|
+
] }), /* @__PURE__ */ jsxs("div", {
|
|
122
|
+
className: "flex flex-col items-end gap-2",
|
|
123
|
+
children: [/* @__PURE__ */ jsx(LocalDataIndicator, {}), showSaveAction ? /* @__PURE__ */ jsx(SaveToStudioButton, { ...saveProps }) : null]
|
|
124
|
+
})]
|
|
125
|
+
}), actions ? /* @__PURE__ */ jsx("div", {
|
|
126
|
+
className: "mt-4",
|
|
127
|
+
children: actions
|
|
128
|
+
}) : null]
|
|
129
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
130
|
+
className: sidebar ? "grid gap-6 lg:grid-cols-[minmax(0,1fr)_320px]" : "w-full",
|
|
131
|
+
children: [/* @__PURE__ */ jsx("main", {
|
|
132
|
+
className: "space-y-4 p-2",
|
|
133
|
+
children
|
|
134
|
+
}), sidebar ? /* @__PURE__ */ jsx("aside", {
|
|
135
|
+
className: "border-border bg-card rounded-2xl border p-4",
|
|
136
|
+
children: sidebar
|
|
137
|
+
}) : null]
|
|
138
|
+
})]
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/MarkdownView.tsx
|
|
143
|
+
/**
|
|
144
|
+
* MarkdownView renders template presentations as markdown using TransformEngine.
|
|
145
|
+
* It allows switching between available presentations for the template.
|
|
146
|
+
*/
|
|
147
|
+
function MarkdownView({ templateId: propTemplateId, presentationId, className }) {
|
|
148
|
+
const { engine, template, templateId: contextTemplateId, resolvePresentation, fetchData } = useTemplateRuntime();
|
|
149
|
+
const templateId = propTemplateId ?? contextTemplateId;
|
|
150
|
+
const presentations = template?.presentations ?? [];
|
|
151
|
+
const [selectedPresentation, setSelectedPresentation] = useState("");
|
|
152
|
+
const [markdownContent, setMarkdownContent] = useState("");
|
|
153
|
+
const [loading, setLoading] = useState(false);
|
|
154
|
+
const [error, setError] = useState(null);
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
if (presentationId && presentations.includes(presentationId)) setSelectedPresentation(presentationId);
|
|
157
|
+
else if (presentations.length > 0 && !selectedPresentation) setSelectedPresentation(presentations[0] ?? "");
|
|
158
|
+
}, [
|
|
159
|
+
presentationId,
|
|
160
|
+
presentations,
|
|
161
|
+
selectedPresentation
|
|
162
|
+
]);
|
|
163
|
+
const renderMarkdown = useCallback(async () => {
|
|
164
|
+
if (!selectedPresentation || !engine) return;
|
|
165
|
+
setLoading(true);
|
|
166
|
+
setError(null);
|
|
167
|
+
try {
|
|
168
|
+
if (!resolvePresentation) throw new Error("resolvePresentation not available in runtime context");
|
|
169
|
+
const descriptor = resolvePresentation(selectedPresentation);
|
|
170
|
+
if (!descriptor) throw new Error(`Presentation descriptor not found: ${selectedPresentation}`);
|
|
171
|
+
const dataResult = await fetchData(selectedPresentation);
|
|
172
|
+
setMarkdownContent((await engine.render("markdown", descriptor, { data: dataResult.data })).body);
|
|
173
|
+
} catch (err) {
|
|
174
|
+
setError(err instanceof Error ? err : /* @__PURE__ */ new Error("Failed to render markdown"));
|
|
175
|
+
} finally {
|
|
176
|
+
setLoading(false);
|
|
177
|
+
}
|
|
178
|
+
}, [
|
|
179
|
+
selectedPresentation,
|
|
180
|
+
templateId,
|
|
181
|
+
engine,
|
|
182
|
+
resolvePresentation,
|
|
183
|
+
fetchData
|
|
184
|
+
]);
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
renderMarkdown();
|
|
187
|
+
}, [renderMarkdown]);
|
|
188
|
+
if (!presentations.length) return /* @__PURE__ */ jsx(Card, {
|
|
189
|
+
className,
|
|
190
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
191
|
+
className: "p-6 text-center",
|
|
192
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
193
|
+
className: "text-muted-foreground",
|
|
194
|
+
children: "No presentations available for this template."
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
});
|
|
198
|
+
const handleCopy = useCallback(() => {
|
|
199
|
+
if (markdownContent) navigator.clipboard.writeText(markdownContent);
|
|
200
|
+
}, [markdownContent]);
|
|
201
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
202
|
+
className,
|
|
203
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
204
|
+
className: "mb-4 flex flex-wrap items-center gap-2",
|
|
205
|
+
children: [
|
|
206
|
+
/* @__PURE__ */ jsx("span", {
|
|
207
|
+
className: "text-muted-foreground text-sm font-medium",
|
|
208
|
+
children: "Presentation:"
|
|
209
|
+
}),
|
|
210
|
+
presentations.map((name) => /* @__PURE__ */ jsx(Button, {
|
|
211
|
+
variant: selectedPresentation === name ? "default" : "outline",
|
|
212
|
+
size: "sm",
|
|
213
|
+
onPress: () => setSelectedPresentation(name),
|
|
214
|
+
children: formatPresentationName(name)
|
|
215
|
+
}, name)),
|
|
216
|
+
/* @__PURE__ */ jsxs("div", {
|
|
217
|
+
className: "ml-auto flex items-center gap-2",
|
|
218
|
+
children: [/* @__PURE__ */ jsx(Badge, {
|
|
219
|
+
variant: "secondary",
|
|
220
|
+
children: "LLM-friendly"
|
|
221
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
222
|
+
variant: "outline",
|
|
223
|
+
size: "sm",
|
|
224
|
+
onPress: handleCopy,
|
|
225
|
+
disabled: !markdownContent || loading,
|
|
226
|
+
children: "Copy"
|
|
227
|
+
})]
|
|
228
|
+
})
|
|
229
|
+
]
|
|
230
|
+
}), /* @__PURE__ */ jsxs(Card, {
|
|
231
|
+
className: "overflow-hidden",
|
|
232
|
+
children: [
|
|
233
|
+
loading && /* @__PURE__ */ jsx(LoaderBlock, { label: "Rendering markdown..." }),
|
|
234
|
+
error && /* @__PURE__ */ jsx(ErrorState, {
|
|
235
|
+
title: "Render failed",
|
|
236
|
+
description: error.message,
|
|
237
|
+
onRetry: renderMarkdown,
|
|
238
|
+
retryLabel: "Retry"
|
|
239
|
+
}),
|
|
240
|
+
!loading && !error && markdownContent && /* @__PURE__ */ jsx("div", {
|
|
241
|
+
className: "p-6",
|
|
242
|
+
children: /* @__PURE__ */ jsx(MarkdownRenderer, { content: markdownContent })
|
|
243
|
+
}),
|
|
244
|
+
!loading && !error && !markdownContent && /* @__PURE__ */ jsx("div", {
|
|
245
|
+
className: "p-6 text-center",
|
|
246
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
247
|
+
className: "text-muted-foreground",
|
|
248
|
+
children: "Select a presentation to view its markdown output."
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
]
|
|
252
|
+
})]
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Simple markdown renderer using pre-formatted display
|
|
257
|
+
* For production, consider using react-markdown or similar
|
|
258
|
+
*/
|
|
259
|
+
function MarkdownRenderer({ content }) {
|
|
260
|
+
const lines = content.split("\n");
|
|
261
|
+
const rendered = [];
|
|
262
|
+
let i = 0;
|
|
263
|
+
while (i < lines.length) {
|
|
264
|
+
const line = lines[i] ?? "";
|
|
265
|
+
if (line.startsWith("|") && lines[i + 1]?.match(/^\|[\s-|]+\|$/)) {
|
|
266
|
+
const tableLines = [line];
|
|
267
|
+
i++;
|
|
268
|
+
while (i < lines.length && (lines[i]?.startsWith("|") ?? false)) {
|
|
269
|
+
tableLines.push(lines[i] ?? "");
|
|
270
|
+
i++;
|
|
271
|
+
}
|
|
272
|
+
rendered.push(renderTable(tableLines, rendered.length));
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (line.startsWith("# ")) rendered.push(/* @__PURE__ */ jsx("h1", {
|
|
276
|
+
className: "mb-4 text-2xl font-bold",
|
|
277
|
+
children: line.slice(2)
|
|
278
|
+
}, i));
|
|
279
|
+
else if (line.startsWith("## ")) rendered.push(/* @__PURE__ */ jsx("h2", {
|
|
280
|
+
className: "mt-6 mb-3 text-xl font-semibold",
|
|
281
|
+
children: line.slice(3)
|
|
282
|
+
}, i));
|
|
283
|
+
else if (line.startsWith("### ")) rendered.push(/* @__PURE__ */ jsx("h3", {
|
|
284
|
+
className: "mt-4 mb-2 text-lg font-medium",
|
|
285
|
+
children: line.slice(4)
|
|
286
|
+
}, i));
|
|
287
|
+
else if (line.startsWith("> ")) rendered.push(/* @__PURE__ */ jsx("blockquote", {
|
|
288
|
+
className: "text-muted-foreground my-2 border-l-4 border-violet-500/50 pl-4 italic",
|
|
289
|
+
children: line.slice(2)
|
|
290
|
+
}, i));
|
|
291
|
+
else if (line.startsWith("- ")) rendered.push(/* @__PURE__ */ jsx("li", {
|
|
292
|
+
className: "ml-4 list-disc",
|
|
293
|
+
children: formatInlineMarkdown(line.slice(2))
|
|
294
|
+
}, i));
|
|
295
|
+
else if (line.startsWith("**") && line.includes(":**")) {
|
|
296
|
+
const [label, ...rest] = line.split(":**");
|
|
297
|
+
rendered.push(/* @__PURE__ */ jsxs("p", {
|
|
298
|
+
className: "my-1",
|
|
299
|
+
children: [/* @__PURE__ */ jsxs("strong", { children: [label?.slice(2), ":"] }), rest.join(":**")]
|
|
300
|
+
}, i));
|
|
301
|
+
} else if (line.startsWith("_") && line.endsWith("_")) rendered.push(/* @__PURE__ */ jsx("p", {
|
|
302
|
+
className: "text-muted-foreground my-1 italic",
|
|
303
|
+
children: line.slice(1, -1)
|
|
304
|
+
}, i));
|
|
305
|
+
else if (!line.trim()) rendered.push(/* @__PURE__ */ jsx("div", { className: "h-2" }, i));
|
|
306
|
+
else rendered.push(/* @__PURE__ */ jsx("p", {
|
|
307
|
+
className: "my-1",
|
|
308
|
+
children: formatInlineMarkdown(line)
|
|
309
|
+
}, i));
|
|
310
|
+
i++;
|
|
311
|
+
}
|
|
312
|
+
return /* @__PURE__ */ jsx("div", {
|
|
313
|
+
className: "prose prose-sm dark:prose-invert max-w-none",
|
|
314
|
+
children: rendered
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Render a markdown table
|
|
319
|
+
*/
|
|
320
|
+
function renderTable(lines, keyPrefix) {
|
|
321
|
+
if (lines.length < 2) return null;
|
|
322
|
+
const parseRow = (row) => row.split("|").slice(1, -1).map((cell) => cell.trim());
|
|
323
|
+
const headers = parseRow(lines[0] ?? "");
|
|
324
|
+
const dataRows = lines.slice(2).map(parseRow);
|
|
325
|
+
return /* @__PURE__ */ jsx("div", {
|
|
326
|
+
className: "my-4 overflow-x-auto",
|
|
327
|
+
children: /* @__PURE__ */ jsxs("table", {
|
|
328
|
+
className: "border-border min-w-full border-collapse border text-sm",
|
|
329
|
+
children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", {
|
|
330
|
+
className: "bg-muted/50",
|
|
331
|
+
children: headers.map((header, idx) => /* @__PURE__ */ jsx("th", {
|
|
332
|
+
className: "border-border border px-3 py-2 text-left font-semibold",
|
|
333
|
+
children: header
|
|
334
|
+
}, idx))
|
|
335
|
+
}) }), /* @__PURE__ */ jsx("tbody", { children: dataRows.map((row, rowIdx) => /* @__PURE__ */ jsx("tr", {
|
|
336
|
+
className: "hover:bg-muted/30",
|
|
337
|
+
children: row.map((cell, cellIdx) => /* @__PURE__ */ jsx("td", {
|
|
338
|
+
className: "border-border border px-3 py-2",
|
|
339
|
+
children: formatInlineMarkdown(cell)
|
|
340
|
+
}, cellIdx))
|
|
341
|
+
}, rowIdx)) })]
|
|
342
|
+
})
|
|
343
|
+
}, `table-${keyPrefix}`);
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Format inline markdown (bold, code)
|
|
347
|
+
*/
|
|
348
|
+
function formatInlineMarkdown(text) {
|
|
349
|
+
return text.split(/(\*\*[^*]+\*\*)/g).map((part, i) => {
|
|
350
|
+
if (part.startsWith("**") && part.endsWith("**")) return /* @__PURE__ */ jsx("strong", { children: part.slice(2, -2) }, i);
|
|
351
|
+
if (part.includes("`")) return part.split(/(`[^`]+`)/g).map((cp, j) => {
|
|
352
|
+
if (cp.startsWith("`") && cp.endsWith("`")) return /* @__PURE__ */ jsx("code", {
|
|
353
|
+
className: "rounded bg-violet-500/10 px-1.5 py-0.5 font-mono text-sm",
|
|
354
|
+
children: cp.slice(1, -1)
|
|
355
|
+
}, `${i}-${j}`);
|
|
356
|
+
return cp;
|
|
357
|
+
});
|
|
358
|
+
return part;
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Format presentation name for display
|
|
363
|
+
*/
|
|
364
|
+
function formatPresentationName(name) {
|
|
365
|
+
const parts = name.split(".");
|
|
366
|
+
return (parts[parts.length - 1] ?? name).split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
//#endregion
|
|
370
|
+
//#region src/utils/generateSpecFromTemplate.ts
|
|
371
|
+
/**
|
|
372
|
+
* Generate TypeScript spec code from a template's definition.
|
|
373
|
+
* Converts FeatureModuleSpec contracts to TypeScript spec code.
|
|
374
|
+
*/
|
|
375
|
+
function generateSpecFromTemplate(template) {
|
|
376
|
+
const templateId = template?.id ?? "unknown";
|
|
377
|
+
if (!template) return generateDefaultSpec(templateId);
|
|
378
|
+
switch (templateId) {
|
|
379
|
+
case "crm-pipeline": return generateCrmPipelineSpec(template.schema.contracts);
|
|
380
|
+
case "saas-boilerplate": return generateSaasBoilerplateSpec(template.schema.contracts);
|
|
381
|
+
case "agent-console": return generateAgentConsoleSpec(template.schema.contracts);
|
|
382
|
+
case "todos-app": return generateTodosSpec(template.schema.contracts);
|
|
383
|
+
case "messaging-app": return generateMessagingSpec(template.schema.contracts);
|
|
384
|
+
case "recipe-app-i18n": return generateRecipeSpec(template.schema.contracts);
|
|
385
|
+
default: return generateDefaultSpec(templateId);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* CRM Pipeline spec
|
|
390
|
+
*/
|
|
391
|
+
function generateCrmPipelineSpec(contracts) {
|
|
392
|
+
return `// CRM Pipeline Specs
|
|
393
|
+
// Contracts: ${contracts.join(", ")}
|
|
394
|
+
|
|
395
|
+
contractSpec("crm.deal.updateStage.v1", {
|
|
396
|
+
goal: "Move a deal to a different pipeline stage",
|
|
397
|
+
transport: { gql: { mutation: "updateDealStage" } },
|
|
398
|
+
io: {
|
|
399
|
+
input: {
|
|
400
|
+
dealId: "string",
|
|
401
|
+
stageId: "string",
|
|
402
|
+
notes: "string?"
|
|
403
|
+
},
|
|
404
|
+
output: {
|
|
405
|
+
deal: {
|
|
406
|
+
id: "string",
|
|
407
|
+
stage: "string",
|
|
408
|
+
probability: "number",
|
|
409
|
+
value: "number"
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
events: ["deal.stage.changed"],
|
|
414
|
+
policy: { auth: "user", rbac: "org:sales" }
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
contractSpec("crm.deal.create.v1", {
|
|
418
|
+
goal: "Create a new deal in the pipeline",
|
|
419
|
+
transport: { gql: { mutation: "createDeal" } },
|
|
420
|
+
io: {
|
|
421
|
+
input: {
|
|
422
|
+
title: "string",
|
|
423
|
+
value: "number",
|
|
424
|
+
contactId: "string",
|
|
425
|
+
stageId: "string",
|
|
426
|
+
ownerId: "string?"
|
|
427
|
+
},
|
|
428
|
+
output: {
|
|
429
|
+
deal: {
|
|
430
|
+
id: "string",
|
|
431
|
+
title: "string",
|
|
432
|
+
value: "number",
|
|
433
|
+
stage: "string",
|
|
434
|
+
createdAt: "ISO8601"
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
events: ["deal.created"]
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
contractSpec("crm.contact.list.v1", {
|
|
442
|
+
goal: "List contacts with filtering and pagination",
|
|
443
|
+
transport: { gql: { query: "listContacts" } },
|
|
444
|
+
io: {
|
|
445
|
+
input: {
|
|
446
|
+
filter: {
|
|
447
|
+
search: "string?",
|
|
448
|
+
companyId: "string?",
|
|
449
|
+
tags: "string[]?"
|
|
450
|
+
},
|
|
451
|
+
pagination: {
|
|
452
|
+
page: "number",
|
|
453
|
+
limit: "number"
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
output: {
|
|
457
|
+
contacts: "array<Contact>",
|
|
458
|
+
total: "number",
|
|
459
|
+
hasMore: "boolean"
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
});`;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* SaaS Boilerplate spec
|
|
466
|
+
*/
|
|
467
|
+
function generateSaasBoilerplateSpec(contracts) {
|
|
468
|
+
return `// SaaS Boilerplate Specs
|
|
469
|
+
// Contracts: ${contracts.join(", ")}
|
|
470
|
+
|
|
471
|
+
contractSpec("saas.project.create.v1", {
|
|
472
|
+
goal: "Create a new project in an organization",
|
|
473
|
+
transport: { gql: { mutation: "createProject" } },
|
|
474
|
+
io: {
|
|
475
|
+
input: {
|
|
476
|
+
orgId: "string",
|
|
477
|
+
name: "string",
|
|
478
|
+
description: "string?"
|
|
479
|
+
},
|
|
480
|
+
output: {
|
|
481
|
+
project: {
|
|
482
|
+
id: "string",
|
|
483
|
+
name: "string",
|
|
484
|
+
description: "string?",
|
|
485
|
+
createdAt: "ISO8601"
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
policy: { auth: "user", rbac: "org:member" }
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
contractSpec("saas.billing.recordUsage.v1", {
|
|
493
|
+
goal: "Record usage for billing purposes",
|
|
494
|
+
transport: { gql: { mutation: "recordUsage" } },
|
|
495
|
+
io: {
|
|
496
|
+
input: {
|
|
497
|
+
orgId: "string",
|
|
498
|
+
metric: "enum<'api_calls'|'storage_gb'|'seats'>",
|
|
499
|
+
quantity: "number",
|
|
500
|
+
timestamp: "ISO8601?"
|
|
501
|
+
},
|
|
502
|
+
output: {
|
|
503
|
+
usage: {
|
|
504
|
+
id: "string",
|
|
505
|
+
metric: "string",
|
|
506
|
+
quantity: "number",
|
|
507
|
+
recordedAt: "ISO8601"
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
events: ["billing.usage.recorded"]
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
contractSpec("saas.settings.update.v1", {
|
|
515
|
+
goal: "Update organization or user settings",
|
|
516
|
+
transport: { gql: { mutation: "updateSettings" } },
|
|
517
|
+
io: {
|
|
518
|
+
input: {
|
|
519
|
+
scope: "enum<'org'|'user'>",
|
|
520
|
+
targetId: "string",
|
|
521
|
+
settings: "Record<string, unknown>"
|
|
522
|
+
},
|
|
523
|
+
output: {
|
|
524
|
+
settings: {
|
|
525
|
+
scope: "string",
|
|
526
|
+
values: "Record<string, unknown>",
|
|
527
|
+
updatedAt: "ISO8601"
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
},
|
|
531
|
+
events: ["settings.updated"]
|
|
532
|
+
});`;
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Agent Console spec
|
|
536
|
+
*/
|
|
537
|
+
function generateAgentConsoleSpec(contracts) {
|
|
538
|
+
return `// Agent Console Specs
|
|
539
|
+
// Contracts: ${contracts.join(", ")}
|
|
540
|
+
|
|
541
|
+
contractSpec("agent.run.execute.v1", {
|
|
542
|
+
goal: "Execute an agent run with specified tools",
|
|
543
|
+
transport: { gql: { mutation: "executeAgentRun" } },
|
|
544
|
+
io: {
|
|
545
|
+
input: {
|
|
546
|
+
agentId: "string",
|
|
547
|
+
input: "string",
|
|
548
|
+
tools: "string[]?",
|
|
549
|
+
maxSteps: "number?"
|
|
550
|
+
},
|
|
551
|
+
output: {
|
|
552
|
+
runId: "string",
|
|
553
|
+
status: "enum<'running'|'completed'|'failed'>",
|
|
554
|
+
steps: "number"
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
events: ["run.started", "run.completed", "run.failed"]
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
contractSpec("agent.tool.create.v1", {
|
|
561
|
+
goal: "Register a new tool in the tool registry",
|
|
562
|
+
transport: { gql: { mutation: "createTool" } },
|
|
563
|
+
io: {
|
|
564
|
+
input: {
|
|
565
|
+
name: "string",
|
|
566
|
+
description: "string",
|
|
567
|
+
category: "enum<'code'|'data'|'api'|'file'|'custom'>",
|
|
568
|
+
schema: "JSONSchema",
|
|
569
|
+
handler: "string"
|
|
570
|
+
},
|
|
571
|
+
output: {
|
|
572
|
+
tool: {
|
|
573
|
+
id: "string",
|
|
574
|
+
name: "string",
|
|
575
|
+
category: "string",
|
|
576
|
+
createdAt: "ISO8601"
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
events: ["tool.created"]
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
contractSpec("agent.agent.create.v1", {
|
|
584
|
+
goal: "Create a new AI agent configuration",
|
|
585
|
+
transport: { gql: { mutation: "createAgent" } },
|
|
586
|
+
io: {
|
|
587
|
+
input: {
|
|
588
|
+
name: "string",
|
|
589
|
+
description: "string",
|
|
590
|
+
model: "string",
|
|
591
|
+
systemPrompt: "string?",
|
|
592
|
+
tools: "string[]?"
|
|
593
|
+
},
|
|
594
|
+
output: {
|
|
595
|
+
agent: {
|
|
596
|
+
id: "string",
|
|
597
|
+
name: "string",
|
|
598
|
+
model: "string",
|
|
599
|
+
toolCount: "number",
|
|
600
|
+
createdAt: "ISO8601"
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
events: ["agent.created"]
|
|
605
|
+
});`;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Todos App spec
|
|
609
|
+
*/
|
|
610
|
+
function generateTodosSpec(contracts) {
|
|
611
|
+
return `// To-dos App Specs
|
|
612
|
+
// Contracts: ${contracts.join(", ")}
|
|
613
|
+
|
|
614
|
+
contractSpec("tasks.board.v1", {
|
|
615
|
+
goal: "Assign and approve craft work",
|
|
616
|
+
transport: { gql: { field: "tasksBoard" } },
|
|
617
|
+
io: {
|
|
618
|
+
input: {
|
|
619
|
+
tenantId: "string",
|
|
620
|
+
assignee: "string?",
|
|
621
|
+
status: "enum<'pending'|'in_progress'|'completed'>?"
|
|
622
|
+
},
|
|
623
|
+
output: {
|
|
624
|
+
tasks: "array<Task>",
|
|
625
|
+
summary: {
|
|
626
|
+
total: "number",
|
|
627
|
+
completed: "number",
|
|
628
|
+
overdue: "number"
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
contractSpec("tasks.create.v1", {
|
|
635
|
+
goal: "Create a new task",
|
|
636
|
+
transport: { gql: { mutation: "createTask" } },
|
|
637
|
+
io: {
|
|
638
|
+
input: {
|
|
639
|
+
title: "string",
|
|
640
|
+
description: "string?",
|
|
641
|
+
assignee: "string?",
|
|
642
|
+
priority: "enum<'low'|'medium'|'high'>",
|
|
643
|
+
dueDate: "ISO8601?"
|
|
644
|
+
},
|
|
645
|
+
output: {
|
|
646
|
+
task: {
|
|
647
|
+
id: "string",
|
|
648
|
+
title: "string",
|
|
649
|
+
status: "string",
|
|
650
|
+
createdAt: "ISO8601"
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
events: ["task.created"]
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
contractSpec("tasks.complete.v1", {
|
|
658
|
+
goal: "Mark a task as completed",
|
|
659
|
+
transport: { gql: { mutation: "completeTask" } },
|
|
660
|
+
io: {
|
|
661
|
+
input: { taskId: "string" },
|
|
662
|
+
output: {
|
|
663
|
+
task: {
|
|
664
|
+
id: "string",
|
|
665
|
+
status: "string",
|
|
666
|
+
completedAt: "ISO8601"
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
},
|
|
670
|
+
events: ["task.completed"]
|
|
671
|
+
});`;
|
|
672
|
+
}
|
|
673
|
+
/**
|
|
674
|
+
* Messaging App spec
|
|
675
|
+
*/
|
|
676
|
+
function generateMessagingSpec(contracts) {
|
|
677
|
+
return `// Messaging App Specs
|
|
678
|
+
// Contracts: ${contracts.join(", ")}
|
|
679
|
+
|
|
680
|
+
contractSpec("messaging.send.v1", {
|
|
681
|
+
goal: "Deliver intent-rich updates",
|
|
682
|
+
io: {
|
|
683
|
+
input: {
|
|
684
|
+
conversationId: "string",
|
|
685
|
+
body: "richtext",
|
|
686
|
+
attachments: "array<Attachment>?"
|
|
687
|
+
},
|
|
688
|
+
output: {
|
|
689
|
+
messageId: "string",
|
|
690
|
+
deliveredAt: "ISO8601"
|
|
691
|
+
}
|
|
692
|
+
},
|
|
693
|
+
events: ["message.sent", "message.delivered"]
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
contractSpec("messaging.conversation.create.v1", {
|
|
697
|
+
goal: "Start a new conversation",
|
|
698
|
+
transport: { gql: { mutation: "createConversation" } },
|
|
699
|
+
io: {
|
|
700
|
+
input: {
|
|
701
|
+
participants: "string[]",
|
|
702
|
+
title: "string?",
|
|
703
|
+
type: "enum<'direct'|'group'>"
|
|
704
|
+
},
|
|
705
|
+
output: {
|
|
706
|
+
conversation: {
|
|
707
|
+
id: "string",
|
|
708
|
+
title: "string?",
|
|
709
|
+
participantCount: "number",
|
|
710
|
+
createdAt: "ISO8601"
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
},
|
|
714
|
+
events: ["conversation.created"]
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
contractSpec("messaging.read.v1", {
|
|
718
|
+
goal: "Mark messages as read",
|
|
719
|
+
transport: { gql: { mutation: "markRead" } },
|
|
720
|
+
io: {
|
|
721
|
+
input: {
|
|
722
|
+
conversationId: "string",
|
|
723
|
+
messageIds: "string[]"
|
|
724
|
+
},
|
|
725
|
+
output: {
|
|
726
|
+
readCount: "number",
|
|
727
|
+
readAt: "ISO8601"
|
|
728
|
+
}
|
|
729
|
+
},
|
|
730
|
+
events: ["message.read"]
|
|
731
|
+
});`;
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Recipe App spec
|
|
735
|
+
*/
|
|
736
|
+
function generateRecipeSpec(contracts) {
|
|
737
|
+
return `// Recipe App (i18n) Specs
|
|
738
|
+
// Contracts: ${contracts.join(", ")}
|
|
739
|
+
|
|
740
|
+
contractSpec("recipes.lookup.v1", {
|
|
741
|
+
goal: "Serve bilingual rituals",
|
|
742
|
+
io: {
|
|
743
|
+
input: {
|
|
744
|
+
locale: "enum<'EN'|'FR'>",
|
|
745
|
+
slug: "string"
|
|
746
|
+
},
|
|
747
|
+
output: {
|
|
748
|
+
title: "string",
|
|
749
|
+
content: "markdown",
|
|
750
|
+
ingredients: "array<Ingredient>",
|
|
751
|
+
instructions: "array<Instruction>"
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
contractSpec("recipes.list.v1", {
|
|
757
|
+
goal: "Browse recipes with filtering",
|
|
758
|
+
transport: { gql: { query: "listRecipes" } },
|
|
759
|
+
io: {
|
|
760
|
+
input: {
|
|
761
|
+
locale: "enum<'EN'|'FR'>",
|
|
762
|
+
category: "string?",
|
|
763
|
+
search: "string?",
|
|
764
|
+
favorites: "boolean?"
|
|
765
|
+
},
|
|
766
|
+
output: {
|
|
767
|
+
recipes: "array<RecipeSummary>",
|
|
768
|
+
categories: "array<Category>",
|
|
769
|
+
total: "number"
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
contractSpec("recipes.favorite.toggle.v1", {
|
|
775
|
+
goal: "Toggle recipe favorite status",
|
|
776
|
+
transport: { gql: { mutation: "toggleFavorite" } },
|
|
777
|
+
io: {
|
|
778
|
+
input: { recipeId: "string" },
|
|
779
|
+
output: {
|
|
780
|
+
isFavorite: "boolean",
|
|
781
|
+
totalFavorites: "number"
|
|
782
|
+
}
|
|
783
|
+
},
|
|
784
|
+
events: ["recipe.favorited", "recipe.unfavorited"]
|
|
785
|
+
});`;
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Default spec for unknown templates
|
|
789
|
+
*/
|
|
790
|
+
function generateDefaultSpec(templateId) {
|
|
791
|
+
return `// ${templateId} Specs
|
|
792
|
+
|
|
793
|
+
contractSpec("${templateId}.main.v1", {
|
|
794
|
+
goal: "Main operation for ${templateId}",
|
|
795
|
+
transport: { gql: { query: "main" } },
|
|
796
|
+
io: {
|
|
797
|
+
input: {
|
|
798
|
+
id: "string"
|
|
799
|
+
},
|
|
800
|
+
output: {
|
|
801
|
+
result: "unknown"
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
});`;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
//#endregion
|
|
808
|
+
//#region src/hooks/useSpecContent.ts
|
|
809
|
+
/**
|
|
810
|
+
* Storage key prefix for spec content persistence
|
|
811
|
+
*/
|
|
812
|
+
const SPEC_STORAGE_KEY = "contractspec-spec-content";
|
|
813
|
+
/**
|
|
814
|
+
* Hook for managing spec content with persistence for a template.
|
|
815
|
+
* Uses localStorage for persistence in the sandbox environment.
|
|
816
|
+
*/
|
|
817
|
+
function useSpecContent(templateId) {
|
|
818
|
+
const { template } = useTemplateRuntime();
|
|
819
|
+
const [content, setContentState] = useState("");
|
|
820
|
+
const [savedContent, setSavedContent] = useState("");
|
|
821
|
+
const [loading, setLoading] = useState(true);
|
|
822
|
+
const [validation, setValidation] = useState(null);
|
|
823
|
+
const [lastSaved, setLastSaved] = useState(null);
|
|
824
|
+
useEffect(() => {
|
|
825
|
+
setLoading(true);
|
|
826
|
+
try {
|
|
827
|
+
const stored = localStorage.getItem(`${SPEC_STORAGE_KEY}-${templateId}`);
|
|
828
|
+
if (stored) {
|
|
829
|
+
const parsed = JSON.parse(stored);
|
|
830
|
+
if (parsed.content) {
|
|
831
|
+
setContentState(parsed.content);
|
|
832
|
+
setSavedContent(parsed.content);
|
|
833
|
+
setLastSaved(parsed.savedAt);
|
|
834
|
+
} else {
|
|
835
|
+
const generated = generateSpecFromTemplate(template);
|
|
836
|
+
setContentState(generated);
|
|
837
|
+
setSavedContent(generated);
|
|
838
|
+
}
|
|
839
|
+
} else {
|
|
840
|
+
const generated = generateSpecFromTemplate(template);
|
|
841
|
+
setContentState(generated);
|
|
842
|
+
setSavedContent(generated);
|
|
843
|
+
}
|
|
844
|
+
} catch {
|
|
845
|
+
const generated = generateSpecFromTemplate(template);
|
|
846
|
+
setContentState(generated);
|
|
847
|
+
setSavedContent(generated);
|
|
848
|
+
}
|
|
849
|
+
setLoading(false);
|
|
850
|
+
}, [templateId]);
|
|
851
|
+
/**
|
|
852
|
+
* Update spec content (in-memory only until save)
|
|
853
|
+
*/
|
|
854
|
+
const setContent = useCallback((newContent) => {
|
|
855
|
+
setContentState(newContent);
|
|
856
|
+
setValidation(null);
|
|
857
|
+
}, []);
|
|
858
|
+
/**
|
|
859
|
+
* Save spec content to storage
|
|
860
|
+
*/
|
|
861
|
+
const save = useCallback(() => {
|
|
862
|
+
try {
|
|
863
|
+
const savedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
864
|
+
localStorage.setItem(`${SPEC_STORAGE_KEY}-${templateId}`, JSON.stringify({
|
|
865
|
+
content,
|
|
866
|
+
savedAt
|
|
867
|
+
}));
|
|
868
|
+
setSavedContent(content);
|
|
869
|
+
setLastSaved(savedAt);
|
|
870
|
+
} catch {}
|
|
871
|
+
}, [content, templateId]);
|
|
872
|
+
/**
|
|
873
|
+
* Validate spec content
|
|
874
|
+
* Performs basic syntax validation
|
|
875
|
+
*/
|
|
876
|
+
const validate = useCallback(() => {
|
|
877
|
+
const errors = [];
|
|
878
|
+
const lines = content.split("\n");
|
|
879
|
+
if (!content.includes("contractSpec(")) errors.push({
|
|
880
|
+
line: 1,
|
|
881
|
+
message: "Spec must contain a contractSpec() definition",
|
|
882
|
+
severity: "error"
|
|
883
|
+
});
|
|
884
|
+
if (!content.includes("goal:")) errors.push({
|
|
885
|
+
line: 1,
|
|
886
|
+
message: "Spec should have a goal field",
|
|
887
|
+
severity: "warning"
|
|
888
|
+
});
|
|
889
|
+
if (!content.includes("io:")) errors.push({
|
|
890
|
+
line: 1,
|
|
891
|
+
message: "Spec should define io (input/output)",
|
|
892
|
+
severity: "warning"
|
|
893
|
+
});
|
|
894
|
+
const openBraces = (content.match(/{/g) ?? []).length;
|
|
895
|
+
const closeBraces = (content.match(/}/g) ?? []).length;
|
|
896
|
+
if (openBraces !== closeBraces) errors.push({
|
|
897
|
+
line: lines.length,
|
|
898
|
+
message: `Unbalanced braces: ${openBraces} opening, ${closeBraces} closing`,
|
|
899
|
+
severity: "error"
|
|
900
|
+
});
|
|
901
|
+
const openParens = (content.match(/\(/g) ?? []).length;
|
|
902
|
+
const closeParens = (content.match(/\)/g) ?? []).length;
|
|
903
|
+
if (openParens !== closeParens) errors.push({
|
|
904
|
+
line: lines.length,
|
|
905
|
+
message: `Unbalanced parentheses: ${openParens} opening, ${closeParens} closing`,
|
|
906
|
+
severity: "error"
|
|
907
|
+
});
|
|
908
|
+
lines.forEach((line, index) => {
|
|
909
|
+
const singleQuotes = (line.match(/'/g) ?? []).length;
|
|
910
|
+
const doubleQuotes = (line.match(/"/g) ?? []).length;
|
|
911
|
+
if (singleQuotes % 2 !== 0) errors.push({
|
|
912
|
+
line: index + 1,
|
|
913
|
+
message: "Unclosed single quote",
|
|
914
|
+
severity: "error"
|
|
915
|
+
});
|
|
916
|
+
if (doubleQuotes % 2 !== 0) errors.push({
|
|
917
|
+
line: index + 1,
|
|
918
|
+
message: "Unclosed double quote",
|
|
919
|
+
severity: "error"
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
const result = {
|
|
923
|
+
valid: errors.filter((e) => e.severity === "error").length === 0,
|
|
924
|
+
errors
|
|
925
|
+
};
|
|
926
|
+
setValidation(result);
|
|
927
|
+
return result;
|
|
928
|
+
}, [content]);
|
|
929
|
+
/**
|
|
930
|
+
* Reset to template default
|
|
931
|
+
*/
|
|
932
|
+
const reset = useCallback(() => {
|
|
933
|
+
const generated = generateSpecFromTemplate(template);
|
|
934
|
+
setContentState(generated);
|
|
935
|
+
setSavedContent(generated);
|
|
936
|
+
setValidation(null);
|
|
937
|
+
setLastSaved(null);
|
|
938
|
+
try {
|
|
939
|
+
localStorage.removeItem(`${SPEC_STORAGE_KEY}-${templateId}`);
|
|
940
|
+
} catch {}
|
|
941
|
+
}, [templateId]);
|
|
942
|
+
return {
|
|
943
|
+
content,
|
|
944
|
+
loading,
|
|
945
|
+
isDirty: content !== savedContent,
|
|
946
|
+
validation,
|
|
947
|
+
setContent,
|
|
948
|
+
save,
|
|
949
|
+
validate,
|
|
950
|
+
reset,
|
|
951
|
+
lastSaved
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
//#endregion
|
|
956
|
+
//#region src/SpecEditorPanel.tsx
|
|
957
|
+
/**
|
|
958
|
+
* Spec editor panel that wraps SpecEditor with persisted spec content.
|
|
959
|
+
* Uses useSpecContent hook to manage spec persistence and validation.
|
|
960
|
+
*/
|
|
961
|
+
function SpecEditorPanel({ templateId, SpecEditor, onLog }) {
|
|
962
|
+
const { content, loading, isDirty, validation, setContent, save, validate, reset, lastSaved } = useSpecContent(templateId);
|
|
963
|
+
useEffect(() => {
|
|
964
|
+
if (!loading && content) onLog?.(`Spec loaded for ${templateId}`);
|
|
965
|
+
}, [
|
|
966
|
+
loading,
|
|
967
|
+
content,
|
|
968
|
+
templateId,
|
|
969
|
+
onLog
|
|
970
|
+
]);
|
|
971
|
+
const handleSave = useCallback(() => {
|
|
972
|
+
save();
|
|
973
|
+
onLog?.("Spec saved locally");
|
|
974
|
+
}, [save, onLog]);
|
|
975
|
+
const handleValidate = useCallback(() => {
|
|
976
|
+
const result = validate();
|
|
977
|
+
if (result.valid) onLog?.("Spec validation passed");
|
|
978
|
+
else {
|
|
979
|
+
const errorCount = result.errors.filter((e) => e.severity === "error").length;
|
|
980
|
+
const warnCount = result.errors.filter((e) => e.severity === "warning").length;
|
|
981
|
+
onLog?.(`Spec validation: ${errorCount} errors, ${warnCount} warnings`);
|
|
982
|
+
}
|
|
983
|
+
}, [validate, onLog]);
|
|
984
|
+
const handleReset = useCallback(() => {
|
|
985
|
+
reset();
|
|
986
|
+
onLog?.("Spec reset to template defaults");
|
|
987
|
+
}, [reset, onLog]);
|
|
988
|
+
if (loading) return /* @__PURE__ */ jsx(LoaderBlock, { label: "Loading spec..." });
|
|
989
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
990
|
+
className: "space-y-4",
|
|
991
|
+
children: [
|
|
992
|
+
/* @__PURE__ */ jsxs("div", {
|
|
993
|
+
className: "flex items-center justify-between",
|
|
994
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
995
|
+
className: "flex items-center gap-2",
|
|
996
|
+
children: [
|
|
997
|
+
/* @__PURE__ */ jsx(Button, {
|
|
998
|
+
variant: "default",
|
|
999
|
+
size: "sm",
|
|
1000
|
+
onClick: handleSave,
|
|
1001
|
+
children: "Save"
|
|
1002
|
+
}),
|
|
1003
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1004
|
+
variant: "outline",
|
|
1005
|
+
size: "sm",
|
|
1006
|
+
onClick: handleValidate,
|
|
1007
|
+
children: "Validate"
|
|
1008
|
+
}),
|
|
1009
|
+
isDirty && /* @__PURE__ */ jsx(Badge, {
|
|
1010
|
+
variant: "secondary",
|
|
1011
|
+
className: "border-amber-500/30 bg-amber-500/20 text-amber-400",
|
|
1012
|
+
children: "Unsaved changes"
|
|
1013
|
+
}),
|
|
1014
|
+
validation && /* @__PURE__ */ jsx(Badge, {
|
|
1015
|
+
variant: validation.valid ? "default" : "destructive",
|
|
1016
|
+
className: validation.valid ? "border-green-500/30 bg-green-500/20 text-green-400" : "",
|
|
1017
|
+
children: validation.valid ? "Valid" : `${validation.errors.filter((e) => e.severity === "error").length} errors`
|
|
1018
|
+
})
|
|
1019
|
+
]
|
|
1020
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1021
|
+
className: "flex items-center gap-2",
|
|
1022
|
+
children: [lastSaved && /* @__PURE__ */ jsxs("span", {
|
|
1023
|
+
className: "text-muted-foreground text-xs",
|
|
1024
|
+
children: ["Last saved: ", new Date(lastSaved).toLocaleTimeString()]
|
|
1025
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
1026
|
+
variant: "ghost",
|
|
1027
|
+
size: "sm",
|
|
1028
|
+
onPress: handleReset,
|
|
1029
|
+
children: "Reset"
|
|
1030
|
+
})]
|
|
1031
|
+
})]
|
|
1032
|
+
}),
|
|
1033
|
+
validation && validation.errors.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
1034
|
+
className: "rounded-lg border border-amber-500/50 bg-amber-500/10 p-3",
|
|
1035
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
1036
|
+
className: "mb-2 text-xs font-semibold text-amber-400 uppercase",
|
|
1037
|
+
children: "Validation Issues"
|
|
1038
|
+
}), /* @__PURE__ */ jsx("ul", {
|
|
1039
|
+
className: "space-y-1",
|
|
1040
|
+
children: validation.errors.map((error, index) => /* @__PURE__ */ jsxs("li", {
|
|
1041
|
+
className: `text-xs ${error.severity === "error" ? "text-red-400" : "text-amber-400"}`,
|
|
1042
|
+
children: [
|
|
1043
|
+
"Line ",
|
|
1044
|
+
error.line,
|
|
1045
|
+
": ",
|
|
1046
|
+
error.message
|
|
1047
|
+
]
|
|
1048
|
+
}, `${error.line}-${error.message}-${index}`))
|
|
1049
|
+
})]
|
|
1050
|
+
}),
|
|
1051
|
+
/* @__PURE__ */ jsx("div", {
|
|
1052
|
+
className: "border-border bg-card rounded-2xl border p-4",
|
|
1053
|
+
children: /* @__PURE__ */ jsx(SpecEditor, {
|
|
1054
|
+
projectId: "sandbox",
|
|
1055
|
+
type: "CAPABILITY",
|
|
1056
|
+
content,
|
|
1057
|
+
onChange: setContent,
|
|
1058
|
+
metadata: { template: templateId },
|
|
1059
|
+
onSave: handleSave,
|
|
1060
|
+
onValidate: handleValidate
|
|
1061
|
+
})
|
|
1062
|
+
})
|
|
1063
|
+
]
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
//#endregion
|
|
1068
|
+
//#region src/hooks/useEvolution.ts
|
|
1069
|
+
/**
|
|
1070
|
+
* Storage key for evolution data persistence
|
|
1071
|
+
*/
|
|
1072
|
+
const EVOLUTION_STORAGE_KEY = "contractspec-evolution-data";
|
|
1073
|
+
/**
|
|
1074
|
+
* Hook for AI-powered spec evolution analysis and suggestions.
|
|
1075
|
+
* Tracks sandbox operations, detects anomalies, and generates improvement suggestions.
|
|
1076
|
+
*/
|
|
1077
|
+
function useEvolution(templateId) {
|
|
1078
|
+
const [usageStats, setUsageStats] = useState([]);
|
|
1079
|
+
const [anomalies, setAnomalies] = useState([]);
|
|
1080
|
+
const [suggestions, setSuggestions] = useState([]);
|
|
1081
|
+
const [hints, setHints] = useState([]);
|
|
1082
|
+
const [loading, setLoading] = useState(false);
|
|
1083
|
+
const metricsRef = useRef([]);
|
|
1084
|
+
const [operationCount, setOperationCount] = useState(0);
|
|
1085
|
+
useEffect(() => {
|
|
1086
|
+
try {
|
|
1087
|
+
const stored = localStorage.getItem(`${EVOLUTION_STORAGE_KEY}-${templateId}`);
|
|
1088
|
+
if (stored) setSuggestions(JSON.parse(stored).suggestions.map((s) => ({
|
|
1089
|
+
...s,
|
|
1090
|
+
createdAt: new Date(s.createdAt)
|
|
1091
|
+
})));
|
|
1092
|
+
} catch {}
|
|
1093
|
+
}, [templateId]);
|
|
1094
|
+
useEffect(() => {
|
|
1095
|
+
try {
|
|
1096
|
+
localStorage.setItem(`${EVOLUTION_STORAGE_KEY}-${templateId}`, JSON.stringify({ suggestions }));
|
|
1097
|
+
} catch {}
|
|
1098
|
+
}, [suggestions, templateId]);
|
|
1099
|
+
/**
|
|
1100
|
+
* Track a new operation metric
|
|
1101
|
+
*/
|
|
1102
|
+
const trackOperation = useCallback((operationName, durationMs, success, errorCode) => {
|
|
1103
|
+
const sample = {
|
|
1104
|
+
operation: {
|
|
1105
|
+
name: operationName,
|
|
1106
|
+
version: "1.0.0",
|
|
1107
|
+
tenantId: "sandbox"
|
|
1108
|
+
},
|
|
1109
|
+
durationMs,
|
|
1110
|
+
success,
|
|
1111
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
1112
|
+
errorCode
|
|
1113
|
+
};
|
|
1114
|
+
metricsRef.current.push(sample);
|
|
1115
|
+
setOperationCount((prev) => prev + 1);
|
|
1116
|
+
}, []);
|
|
1117
|
+
/**
|
|
1118
|
+
* Analyze tracked operations to generate usage stats and anomalies
|
|
1119
|
+
*/
|
|
1120
|
+
const analyzeUsage = useCallback(() => {
|
|
1121
|
+
const samples = metricsRef.current;
|
|
1122
|
+
if (samples.length < 5) return;
|
|
1123
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1124
|
+
for (const sample of samples) {
|
|
1125
|
+
const key = `${sample.operation.name}.v${sample.operation.version}`;
|
|
1126
|
+
const arr = groups.get(key) ?? [];
|
|
1127
|
+
arr.push(sample);
|
|
1128
|
+
groups.set(key, arr);
|
|
1129
|
+
}
|
|
1130
|
+
const stats = [];
|
|
1131
|
+
const detectedAnomalies = [];
|
|
1132
|
+
groups.forEach((opSamples) => {
|
|
1133
|
+
if (opSamples.length < 3) return;
|
|
1134
|
+
const durations = opSamples.map((s) => s.durationMs).sort((a, b) => a - b);
|
|
1135
|
+
const errors = opSamples.filter((s) => !s.success);
|
|
1136
|
+
const totalCalls = opSamples.length;
|
|
1137
|
+
const errorRate = errors.length / totalCalls;
|
|
1138
|
+
const averageLatencyMs = durations.reduce((sum, value) => sum + value, 0) / totalCalls;
|
|
1139
|
+
const timestamps = opSamples.map((s) => s.timestamp.getTime());
|
|
1140
|
+
const firstSample = opSamples[0];
|
|
1141
|
+
if (!firstSample) return;
|
|
1142
|
+
const stat = {
|
|
1143
|
+
operation: firstSample.operation,
|
|
1144
|
+
totalCalls,
|
|
1145
|
+
successRate: 1 - errorRate,
|
|
1146
|
+
errorRate,
|
|
1147
|
+
averageLatencyMs,
|
|
1148
|
+
p95LatencyMs: percentile(durations, .95),
|
|
1149
|
+
p99LatencyMs: percentile(durations, .99),
|
|
1150
|
+
maxLatencyMs: Math.max(...durations),
|
|
1151
|
+
lastSeenAt: new Date(Math.max(...timestamps)),
|
|
1152
|
+
windowStart: new Date(Math.min(...timestamps)),
|
|
1153
|
+
windowEnd: new Date(Math.max(...timestamps)),
|
|
1154
|
+
topErrors: errors.reduce((acc, s) => {
|
|
1155
|
+
if (s.errorCode) acc[s.errorCode] = (acc[s.errorCode] ?? 0) + 1;
|
|
1156
|
+
return acc;
|
|
1157
|
+
}, {})
|
|
1158
|
+
};
|
|
1159
|
+
stats.push(stat);
|
|
1160
|
+
if (errorRate > .1) detectedAnomalies.push({
|
|
1161
|
+
operation: stat.operation,
|
|
1162
|
+
severity: errorRate > .3 ? "high" : errorRate > .2 ? "medium" : "low",
|
|
1163
|
+
metric: "error-rate",
|
|
1164
|
+
description: `Error rate ${(errorRate * 100).toFixed(1)}% exceeds threshold`,
|
|
1165
|
+
detectedAt: /* @__PURE__ */ new Date(),
|
|
1166
|
+
threshold: .1,
|
|
1167
|
+
observedValue: errorRate
|
|
1168
|
+
});
|
|
1169
|
+
if (stat.p99LatencyMs > 500) detectedAnomalies.push({
|
|
1170
|
+
operation: stat.operation,
|
|
1171
|
+
severity: stat.p99LatencyMs > 1e3 ? "high" : stat.p99LatencyMs > 750 ? "medium" : "low",
|
|
1172
|
+
metric: "latency",
|
|
1173
|
+
description: `P99 latency ${stat.p99LatencyMs.toFixed(0)}ms exceeds threshold`,
|
|
1174
|
+
detectedAt: /* @__PURE__ */ new Date(),
|
|
1175
|
+
threshold: 500,
|
|
1176
|
+
observedValue: stat.p99LatencyMs
|
|
1177
|
+
});
|
|
1178
|
+
});
|
|
1179
|
+
setUsageStats(stats);
|
|
1180
|
+
setAnomalies(detectedAnomalies);
|
|
1181
|
+
setHints(detectedAnomalies.map((anomaly) => ({
|
|
1182
|
+
operation: anomaly.operation,
|
|
1183
|
+
category: anomaly.metric === "latency" ? "performance" : "error-handling",
|
|
1184
|
+
summary: anomaly.metric === "latency" ? "Latency regression detected" : "Error spike detected",
|
|
1185
|
+
justification: anomaly.description,
|
|
1186
|
+
recommendedActions: anomaly.metric === "latency" ? [
|
|
1187
|
+
"Add caching layer",
|
|
1188
|
+
"Optimize database queries",
|
|
1189
|
+
"Consider pagination"
|
|
1190
|
+
] : [
|
|
1191
|
+
"Add retry logic",
|
|
1192
|
+
"Improve error handling",
|
|
1193
|
+
"Add circuit breaker"
|
|
1194
|
+
]
|
|
1195
|
+
})));
|
|
1196
|
+
}, []);
|
|
1197
|
+
/**
|
|
1198
|
+
* Generate AI suggestions from anomalies (mock for sandbox)
|
|
1199
|
+
*/
|
|
1200
|
+
const generateSuggestions = useCallback(async () => {
|
|
1201
|
+
if (anomalies.length === 0) return;
|
|
1202
|
+
setLoading(true);
|
|
1203
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
1204
|
+
const newSuggestions = anomalies.map((anomaly) => ({
|
|
1205
|
+
id: `suggestion-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
|
1206
|
+
intent: {
|
|
1207
|
+
id: `intent-${anomaly.operation.name}`,
|
|
1208
|
+
type: anomaly.metric === "latency" ? "latency-regression" : anomaly.metric === "error-rate" ? "error-spike" : "throughput-drop",
|
|
1209
|
+
description: anomaly.description,
|
|
1210
|
+
operation: anomaly.operation,
|
|
1211
|
+
confidence: {
|
|
1212
|
+
score: anomaly.severity === "high" ? .9 : anomaly.severity === "medium" ? .7 : .5,
|
|
1213
|
+
sampleSize: usageStats.find((s) => s.operation.name === anomaly.operation.name)?.totalCalls ?? 0
|
|
1214
|
+
}
|
|
1215
|
+
},
|
|
1216
|
+
target: anomaly.operation,
|
|
1217
|
+
proposal: {
|
|
1218
|
+
summary: generateSuggestionSummary(anomaly),
|
|
1219
|
+
rationale: generateSuggestionRationale(anomaly),
|
|
1220
|
+
changeType: anomaly.metric === "error-rate" ? "policy-update" : "revision",
|
|
1221
|
+
recommendedActions: generateRecommendedActions(anomaly)
|
|
1222
|
+
},
|
|
1223
|
+
confidence: anomaly.severity === "high" ? .85 : anomaly.severity === "medium" ? .7 : .55,
|
|
1224
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1225
|
+
createdBy: "ai-evolution-agent",
|
|
1226
|
+
status: "pending",
|
|
1227
|
+
priority: anomaly.severity
|
|
1228
|
+
}));
|
|
1229
|
+
setSuggestions((prev) => [...prev, ...newSuggestions]);
|
|
1230
|
+
setLoading(false);
|
|
1231
|
+
}, [anomalies, usageStats]);
|
|
1232
|
+
/**
|
|
1233
|
+
* Approve a suggestion
|
|
1234
|
+
*/
|
|
1235
|
+
const approveSuggestion = useCallback((id, _notes) => {
|
|
1236
|
+
setSuggestions((prev) => prev.map((s) => s.id === id ? {
|
|
1237
|
+
...s,
|
|
1238
|
+
status: "approved"
|
|
1239
|
+
} : s));
|
|
1240
|
+
}, []);
|
|
1241
|
+
/**
|
|
1242
|
+
* Reject a suggestion
|
|
1243
|
+
*/
|
|
1244
|
+
const rejectSuggestion = useCallback((id, _notes) => {
|
|
1245
|
+
setSuggestions((prev) => prev.map((s) => s.id === id ? {
|
|
1246
|
+
...s,
|
|
1247
|
+
status: "rejected"
|
|
1248
|
+
} : s));
|
|
1249
|
+
}, []);
|
|
1250
|
+
/**
|
|
1251
|
+
* Clear all evolution data
|
|
1252
|
+
*/
|
|
1253
|
+
const clear = useCallback(() => {
|
|
1254
|
+
metricsRef.current = [];
|
|
1255
|
+
setOperationCount(0);
|
|
1256
|
+
setUsageStats([]);
|
|
1257
|
+
setAnomalies([]);
|
|
1258
|
+
setSuggestions([]);
|
|
1259
|
+
setHints([]);
|
|
1260
|
+
localStorage.removeItem(`${EVOLUTION_STORAGE_KEY}-${templateId}`);
|
|
1261
|
+
}, [templateId]);
|
|
1262
|
+
return useMemo(() => ({
|
|
1263
|
+
usageStats,
|
|
1264
|
+
anomalies,
|
|
1265
|
+
suggestions,
|
|
1266
|
+
hints,
|
|
1267
|
+
loading,
|
|
1268
|
+
trackOperation,
|
|
1269
|
+
analyzeUsage,
|
|
1270
|
+
generateSuggestions,
|
|
1271
|
+
approveSuggestion,
|
|
1272
|
+
rejectSuggestion,
|
|
1273
|
+
clear,
|
|
1274
|
+
operationCount
|
|
1275
|
+
}), [
|
|
1276
|
+
usageStats,
|
|
1277
|
+
anomalies,
|
|
1278
|
+
suggestions,
|
|
1279
|
+
hints,
|
|
1280
|
+
loading,
|
|
1281
|
+
trackOperation,
|
|
1282
|
+
analyzeUsage,
|
|
1283
|
+
generateSuggestions,
|
|
1284
|
+
approveSuggestion,
|
|
1285
|
+
rejectSuggestion,
|
|
1286
|
+
clear,
|
|
1287
|
+
operationCount
|
|
1288
|
+
]);
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Utility functions for generating mock AI content
|
|
1292
|
+
*/
|
|
1293
|
+
function percentile(values, p) {
|
|
1294
|
+
if (!values.length) return 0;
|
|
1295
|
+
if (values.length === 1) return values[0] ?? 0;
|
|
1296
|
+
return values[Math.min(values.length - 1, Math.floor(p * values.length))] ?? 0;
|
|
1297
|
+
}
|
|
1298
|
+
function generateSuggestionSummary(anomaly) {
|
|
1299
|
+
if (anomaly.metric === "latency") return `Add caching and pagination to ${anomaly.operation.name} to reduce latency`;
|
|
1300
|
+
if (anomaly.metric === "error-rate") return `Add retry policy and circuit breaker to ${anomaly.operation.name}`;
|
|
1301
|
+
return `Optimize ${anomaly.operation.name} for improved throughput`;
|
|
1302
|
+
}
|
|
1303
|
+
function generateSuggestionRationale(anomaly) {
|
|
1304
|
+
if (anomaly.metric === "latency") return `The operation ${anomaly.operation.name} is experiencing P99 latency of ${anomaly.observedValue?.toFixed(0)}ms, which is above the recommended threshold of ${anomaly.threshold}ms. This can impact user experience and downstream operations.`;
|
|
1305
|
+
if (anomaly.metric === "error-rate") return `The error rate for ${anomaly.operation.name} is ${((anomaly.observedValue ?? 0) * 100).toFixed(1)}%, indicating potential issues with input validation, external dependencies, or resource limits.`;
|
|
1306
|
+
return `Throughput for ${anomaly.operation.name} has dropped significantly, suggesting potential bottlenecks or reduced demand that should be investigated.`;
|
|
1307
|
+
}
|
|
1308
|
+
function generateRecommendedActions(anomaly) {
|
|
1309
|
+
if (anomaly.metric === "latency") return [
|
|
1310
|
+
"Add response caching for frequently accessed data",
|
|
1311
|
+
"Implement pagination for large result sets",
|
|
1312
|
+
"Optimize database queries with proper indexing",
|
|
1313
|
+
"Consider adding a GraphQL DataLoader for batching"
|
|
1314
|
+
];
|
|
1315
|
+
if (anomaly.metric === "error-rate") return [
|
|
1316
|
+
"Add input validation at the contract level",
|
|
1317
|
+
"Implement retry policy with exponential backoff",
|
|
1318
|
+
"Add circuit breaker for external dependencies",
|
|
1319
|
+
"Enhance error logging for better debugging"
|
|
1320
|
+
];
|
|
1321
|
+
return [
|
|
1322
|
+
"Review resource allocation and scaling policies",
|
|
1323
|
+
"Check for upstream routing or load balancer issues",
|
|
1324
|
+
"Validate feature flag configurations",
|
|
1325
|
+
"Monitor dependency health metrics"
|
|
1326
|
+
];
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
//#endregion
|
|
1330
|
+
//#region src/EvolutionDashboard.tsx
|
|
1331
|
+
/**
|
|
1332
|
+
* Dashboard for AI-powered spec evolution.
|
|
1333
|
+
* Shows usage statistics, anomalies, AI suggestions, and optimization hints.
|
|
1334
|
+
*/
|
|
1335
|
+
function EvolutionDashboard({ templateId, onLog }) {
|
|
1336
|
+
const { usageStats, anomalies, suggestions, hints, loading, trackOperation, analyzeUsage, generateSuggestions, approveSuggestion, rejectSuggestion, clear, operationCount } = useEvolution(templateId);
|
|
1337
|
+
const handleSimulateOperations = useCallback(() => {
|
|
1338
|
+
const operations = [
|
|
1339
|
+
{
|
|
1340
|
+
name: `${templateId}.list`,
|
|
1341
|
+
duration: 150,
|
|
1342
|
+
success: true
|
|
1343
|
+
},
|
|
1344
|
+
{
|
|
1345
|
+
name: `${templateId}.list`,
|
|
1346
|
+
duration: 180,
|
|
1347
|
+
success: true
|
|
1348
|
+
},
|
|
1349
|
+
{
|
|
1350
|
+
name: `${templateId}.create`,
|
|
1351
|
+
duration: 350,
|
|
1352
|
+
success: true
|
|
1353
|
+
},
|
|
1354
|
+
{
|
|
1355
|
+
name: `${templateId}.create`,
|
|
1356
|
+
duration: 420,
|
|
1357
|
+
success: false,
|
|
1358
|
+
error: "VALIDATION_ERROR"
|
|
1359
|
+
},
|
|
1360
|
+
{
|
|
1361
|
+
name: `${templateId}.list`,
|
|
1362
|
+
duration: 200,
|
|
1363
|
+
success: true
|
|
1364
|
+
},
|
|
1365
|
+
{
|
|
1366
|
+
name: `${templateId}.get`,
|
|
1367
|
+
duration: 80,
|
|
1368
|
+
success: true
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
name: `${templateId}.update`,
|
|
1372
|
+
duration: 280,
|
|
1373
|
+
success: true
|
|
1374
|
+
},
|
|
1375
|
+
{
|
|
1376
|
+
name: `${templateId}.list`,
|
|
1377
|
+
duration: 950,
|
|
1378
|
+
success: true
|
|
1379
|
+
},
|
|
1380
|
+
{
|
|
1381
|
+
name: `${templateId}.delete`,
|
|
1382
|
+
duration: 150,
|
|
1383
|
+
success: false,
|
|
1384
|
+
error: "NOT_FOUND"
|
|
1385
|
+
},
|
|
1386
|
+
{
|
|
1387
|
+
name: `${templateId}.create`,
|
|
1388
|
+
duration: 380,
|
|
1389
|
+
success: true
|
|
1390
|
+
}
|
|
1391
|
+
];
|
|
1392
|
+
for (const op of operations) trackOperation(op.name, op.duration, op.success, op.error);
|
|
1393
|
+
onLog?.(`Simulated ${operations.length} operations`);
|
|
1394
|
+
setTimeout(() => {
|
|
1395
|
+
analyzeUsage();
|
|
1396
|
+
onLog?.("Analysis complete");
|
|
1397
|
+
}, 100);
|
|
1398
|
+
}, [
|
|
1399
|
+
templateId,
|
|
1400
|
+
trackOperation,
|
|
1401
|
+
analyzeUsage,
|
|
1402
|
+
onLog
|
|
1403
|
+
]);
|
|
1404
|
+
const handleGenerateSuggestions = useCallback(async () => {
|
|
1405
|
+
await generateSuggestions();
|
|
1406
|
+
onLog?.("AI suggestions generated");
|
|
1407
|
+
}, [generateSuggestions, onLog]);
|
|
1408
|
+
const handleApproveSuggestion = useCallback((id) => {
|
|
1409
|
+
approveSuggestion(id);
|
|
1410
|
+
onLog?.(`Suggestion ${id.slice(0, 8)} approved`);
|
|
1411
|
+
}, [approveSuggestion, onLog]);
|
|
1412
|
+
const handleRejectSuggestion = useCallback((id) => {
|
|
1413
|
+
rejectSuggestion(id);
|
|
1414
|
+
onLog?.(`Suggestion ${id.slice(0, 8)} rejected`);
|
|
1415
|
+
}, [rejectSuggestion, onLog]);
|
|
1416
|
+
const handleClear = useCallback(() => {
|
|
1417
|
+
clear();
|
|
1418
|
+
onLog?.("Evolution data cleared");
|
|
1419
|
+
}, [clear, onLog]);
|
|
1420
|
+
const pendingSuggestions = useMemo(() => suggestions.filter((s) => s.status === "pending"), [suggestions]);
|
|
1421
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1422
|
+
className: "space-y-6",
|
|
1423
|
+
children: [
|
|
1424
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1425
|
+
className: "flex items-center justify-between",
|
|
1426
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h2", {
|
|
1427
|
+
className: "text-xl font-semibold",
|
|
1428
|
+
children: "AI Evolution Engine"
|
|
1429
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1430
|
+
className: "text-muted-foreground text-sm",
|
|
1431
|
+
children: "Analyze usage patterns and get AI-powered suggestions"
|
|
1432
|
+
})] }), /* @__PURE__ */ jsxs("div", {
|
|
1433
|
+
className: "flex items-center gap-2",
|
|
1434
|
+
children: [/* @__PURE__ */ jsxs(Badge, {
|
|
1435
|
+
variant: "secondary",
|
|
1436
|
+
children: [operationCount, " ops tracked"]
|
|
1437
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
1438
|
+
variant: "ghost",
|
|
1439
|
+
size: "sm",
|
|
1440
|
+
onPress: handleClear,
|
|
1441
|
+
children: "Clear"
|
|
1442
|
+
})]
|
|
1443
|
+
})]
|
|
1444
|
+
}),
|
|
1445
|
+
/* @__PURE__ */ jsxs(Card, {
|
|
1446
|
+
className: "p-4",
|
|
1447
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1448
|
+
className: "flex flex-wrap items-center gap-3",
|
|
1449
|
+
children: [
|
|
1450
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1451
|
+
variant: "default",
|
|
1452
|
+
size: "sm",
|
|
1453
|
+
onPress: handleSimulateOperations,
|
|
1454
|
+
children: "Simulate Operations"
|
|
1455
|
+
}),
|
|
1456
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1457
|
+
variant: "outline",
|
|
1458
|
+
size: "sm",
|
|
1459
|
+
onPress: analyzeUsage,
|
|
1460
|
+
disabled: operationCount < 5,
|
|
1461
|
+
children: "Analyze Usage"
|
|
1462
|
+
}),
|
|
1463
|
+
/* @__PURE__ */ jsx(Button, {
|
|
1464
|
+
variant: "outline",
|
|
1465
|
+
size: "sm",
|
|
1466
|
+
onPress: handleGenerateSuggestions,
|
|
1467
|
+
disabled: anomalies.length === 0 || loading,
|
|
1468
|
+
children: loading ? "Generating..." : "Generate AI Suggestions"
|
|
1469
|
+
})
|
|
1470
|
+
]
|
|
1471
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1472
|
+
className: "text-muted-foreground mt-2 text-xs",
|
|
1473
|
+
children: "Simulate sandbox operations, analyze patterns, and generate AI improvement suggestions."
|
|
1474
|
+
})]
|
|
1475
|
+
}),
|
|
1476
|
+
loading && /* @__PURE__ */ jsx(LoaderBlock, { label: "Generating AI suggestions..." }),
|
|
1477
|
+
usageStats.length > 0 && /* @__PURE__ */ jsxs(Card, {
|
|
1478
|
+
className: "p-4",
|
|
1479
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
1480
|
+
className: "mb-3 font-semibold",
|
|
1481
|
+
children: "Usage Statistics"
|
|
1482
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1483
|
+
className: "grid gap-3 md:grid-cols-2 lg:grid-cols-3",
|
|
1484
|
+
children: usageStats.map((stat) => /* @__PURE__ */ jsx(UsageStatCard, { stat }, stat.operation.name))
|
|
1485
|
+
})]
|
|
1486
|
+
}),
|
|
1487
|
+
anomalies.length > 0 && /* @__PURE__ */ jsxs(Card, {
|
|
1488
|
+
className: "p-4",
|
|
1489
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1490
|
+
className: "mb-3 flex items-center justify-between",
|
|
1491
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
1492
|
+
className: "font-semibold",
|
|
1493
|
+
children: "Detected Anomalies"
|
|
1494
|
+
}), /* @__PURE__ */ jsxs(Badge, {
|
|
1495
|
+
variant: "secondary",
|
|
1496
|
+
className: "border-amber-500/30 bg-amber-500/20 text-amber-400",
|
|
1497
|
+
children: [anomalies.length, " issues"]
|
|
1498
|
+
})]
|
|
1499
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1500
|
+
className: "space-y-2",
|
|
1501
|
+
children: anomalies.map((anomaly, index) => /* @__PURE__ */ jsx(AnomalyCard, { anomaly }, `${anomaly.operation.name}-${index}`))
|
|
1502
|
+
})]
|
|
1503
|
+
}),
|
|
1504
|
+
suggestions.length > 0 && /* @__PURE__ */ jsxs(Card, {
|
|
1505
|
+
className: "p-4",
|
|
1506
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1507
|
+
className: "mb-3 flex items-center justify-between",
|
|
1508
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
1509
|
+
className: "font-semibold",
|
|
1510
|
+
children: "AI Suggestions"
|
|
1511
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1512
|
+
className: "flex items-center gap-2",
|
|
1513
|
+
children: pendingSuggestions.length > 0 && /* @__PURE__ */ jsxs(Badge, {
|
|
1514
|
+
variant: "secondary",
|
|
1515
|
+
className: "border-amber-500/30 bg-amber-500/20 text-amber-400",
|
|
1516
|
+
children: [pendingSuggestions.length, " pending"]
|
|
1517
|
+
})
|
|
1518
|
+
})]
|
|
1519
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1520
|
+
className: "space-y-3",
|
|
1521
|
+
children: suggestions.map((suggestion) => /* @__PURE__ */ jsx(SuggestionCard, {
|
|
1522
|
+
suggestion,
|
|
1523
|
+
onApprove: handleApproveSuggestion,
|
|
1524
|
+
onReject: handleRejectSuggestion
|
|
1525
|
+
}, suggestion.id))
|
|
1526
|
+
})]
|
|
1527
|
+
}),
|
|
1528
|
+
hints.length > 0 && /* @__PURE__ */ jsxs(Card, {
|
|
1529
|
+
className: "p-4",
|
|
1530
|
+
children: [/* @__PURE__ */ jsx("h3", {
|
|
1531
|
+
className: "mb-3 font-semibold",
|
|
1532
|
+
children: "Optimization Hints"
|
|
1533
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1534
|
+
className: "space-y-2",
|
|
1535
|
+
children: hints.map((hint, index) => /* @__PURE__ */ jsx(HintCard, { hint }, `${hint.operation.name}-${index}`))
|
|
1536
|
+
})]
|
|
1537
|
+
}),
|
|
1538
|
+
usageStats.length === 0 && anomalies.length === 0 && suggestions.length === 0 && /* @__PURE__ */ jsx(Card, {
|
|
1539
|
+
className: "p-8 text-center",
|
|
1540
|
+
children: /* @__PURE__ */ jsx("p", {
|
|
1541
|
+
className: "text-muted-foreground",
|
|
1542
|
+
children: "Click \"Simulate Operations\" to generate sample data for analysis."
|
|
1543
|
+
})
|
|
1544
|
+
})
|
|
1545
|
+
]
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
/**
|
|
1549
|
+
* Usage statistics card component
|
|
1550
|
+
*/
|
|
1551
|
+
function UsageStatCard({ stat }) {
|
|
1552
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1553
|
+
className: "rounded-lg border border-violet-500/20 bg-violet-500/5 p-3",
|
|
1554
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1555
|
+
className: "mb-2 flex items-center justify-between",
|
|
1556
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1557
|
+
className: "font-mono text-sm font-medium",
|
|
1558
|
+
children: stat.operation.name
|
|
1559
|
+
}), /* @__PURE__ */ jsxs(Badge, {
|
|
1560
|
+
variant: stat.errorRate > .1 ? "destructive" : "default",
|
|
1561
|
+
className: stat.errorRate > .1 ? "" : "border-green-500/30 bg-green-500/20 text-green-400",
|
|
1562
|
+
children: [((1 - stat.errorRate) * 100).toFixed(0), "% success"]
|
|
1563
|
+
})]
|
|
1564
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1565
|
+
className: "grid grid-cols-2 gap-2 text-xs",
|
|
1566
|
+
children: [
|
|
1567
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1568
|
+
/* @__PURE__ */ jsx("span", {
|
|
1569
|
+
className: "text-muted-foreground",
|
|
1570
|
+
children: "Total Calls:"
|
|
1571
|
+
}),
|
|
1572
|
+
" ",
|
|
1573
|
+
/* @__PURE__ */ jsx("span", {
|
|
1574
|
+
className: "font-medium",
|
|
1575
|
+
children: stat.totalCalls
|
|
1576
|
+
})
|
|
1577
|
+
] }),
|
|
1578
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1579
|
+
/* @__PURE__ */ jsx("span", {
|
|
1580
|
+
className: "text-muted-foreground",
|
|
1581
|
+
children: "Avg Latency:"
|
|
1582
|
+
}),
|
|
1583
|
+
" ",
|
|
1584
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1585
|
+
className: "font-medium",
|
|
1586
|
+
children: [stat.averageLatencyMs.toFixed(0), "ms"]
|
|
1587
|
+
})
|
|
1588
|
+
] }),
|
|
1589
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1590
|
+
/* @__PURE__ */ jsx("span", {
|
|
1591
|
+
className: "text-muted-foreground",
|
|
1592
|
+
children: "P95:"
|
|
1593
|
+
}),
|
|
1594
|
+
" ",
|
|
1595
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1596
|
+
className: "font-medium",
|
|
1597
|
+
children: [stat.p95LatencyMs.toFixed(0), "ms"]
|
|
1598
|
+
})
|
|
1599
|
+
] }),
|
|
1600
|
+
/* @__PURE__ */ jsxs("div", { children: [
|
|
1601
|
+
/* @__PURE__ */ jsx("span", {
|
|
1602
|
+
className: "text-muted-foreground",
|
|
1603
|
+
children: "P99:"
|
|
1604
|
+
}),
|
|
1605
|
+
" ",
|
|
1606
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1607
|
+
className: "font-medium",
|
|
1608
|
+
children: [stat.p99LatencyMs.toFixed(0), "ms"]
|
|
1609
|
+
})
|
|
1610
|
+
] })
|
|
1611
|
+
]
|
|
1612
|
+
})]
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Anomaly card component
|
|
1617
|
+
*/
|
|
1618
|
+
function AnomalyCard({ anomaly }) {
|
|
1619
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1620
|
+
className: "flex items-center justify-between rounded-lg border border-amber-500/30 bg-amber-500/5 p-3",
|
|
1621
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1622
|
+
className: "flex items-center gap-3",
|
|
1623
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1624
|
+
className: `text-lg ${{
|
|
1625
|
+
low: "text-amber-400",
|
|
1626
|
+
medium: "text-orange-400",
|
|
1627
|
+
high: "text-red-400"
|
|
1628
|
+
}[anomaly.severity]}`,
|
|
1629
|
+
title: `${anomaly.severity} severity`,
|
|
1630
|
+
children: anomaly.severity === "high" ? "🔴" : anomaly.severity === "medium" ? "🟠" : "🟡"
|
|
1631
|
+
}), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
|
|
1632
|
+
className: "text-sm font-medium",
|
|
1633
|
+
children: anomaly.description
|
|
1634
|
+
}), /* @__PURE__ */ jsxs("p", {
|
|
1635
|
+
className: "text-muted-foreground text-xs",
|
|
1636
|
+
children: [
|
|
1637
|
+
anomaly.operation.name,
|
|
1638
|
+
" • ",
|
|
1639
|
+
anomaly.metric
|
|
1640
|
+
]
|
|
1641
|
+
})] })]
|
|
1642
|
+
}), /* @__PURE__ */ jsx(Badge, {
|
|
1643
|
+
variant: anomaly.severity === "high" ? "destructive" : "secondary",
|
|
1644
|
+
className: anomaly.severity === "medium" ? "border-amber-500/30 bg-amber-500/20 text-amber-400" : "",
|
|
1645
|
+
children: anomaly.severity
|
|
1646
|
+
})]
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Suggestion card component
|
|
1651
|
+
*/
|
|
1652
|
+
function SuggestionCard({ suggestion, onApprove, onReject }) {
|
|
1653
|
+
const getStatusStyles = (status) => {
|
|
1654
|
+
switch (status) {
|
|
1655
|
+
case "pending": return {
|
|
1656
|
+
variant: "secondary",
|
|
1657
|
+
className: "bg-amber-500/20 text-amber-400 border-amber-500/30"
|
|
1658
|
+
};
|
|
1659
|
+
case "approved": return {
|
|
1660
|
+
variant: "default",
|
|
1661
|
+
className: "bg-green-500/20 text-green-400 border-green-500/30"
|
|
1662
|
+
};
|
|
1663
|
+
case "rejected": return {
|
|
1664
|
+
variant: "destructive",
|
|
1665
|
+
className: ""
|
|
1666
|
+
};
|
|
1667
|
+
default: return {
|
|
1668
|
+
variant: "secondary",
|
|
1669
|
+
className: ""
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
};
|
|
1673
|
+
const statusStyle = getStatusStyles(suggestion.status);
|
|
1674
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1675
|
+
className: "rounded-lg border border-violet-500/30 bg-violet-500/5 p-4",
|
|
1676
|
+
children: [
|
|
1677
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1678
|
+
className: "mb-2 flex items-start justify-between",
|
|
1679
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1680
|
+
className: "flex-1",
|
|
1681
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1682
|
+
className: "flex items-center gap-2",
|
|
1683
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1684
|
+
className: "text-lg",
|
|
1685
|
+
children: suggestion.intent.type === "latency-regression" ? "⚡" : suggestion.intent.type === "error-spike" ? "🔥" : "📉"
|
|
1686
|
+
}), /* @__PURE__ */ jsx("h4", {
|
|
1687
|
+
className: "font-medium",
|
|
1688
|
+
children: suggestion.proposal.summary
|
|
1689
|
+
})]
|
|
1690
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1691
|
+
className: "text-muted-foreground mt-1 text-sm",
|
|
1692
|
+
children: suggestion.proposal.rationale
|
|
1693
|
+
})]
|
|
1694
|
+
}), /* @__PURE__ */ jsx(Badge, {
|
|
1695
|
+
variant: statusStyle.variant,
|
|
1696
|
+
className: statusStyle.className,
|
|
1697
|
+
children: suggestion.status
|
|
1698
|
+
})]
|
|
1699
|
+
}),
|
|
1700
|
+
suggestion.proposal.recommendedActions && suggestion.proposal.recommendedActions.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
1701
|
+
className: "mt-3",
|
|
1702
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
1703
|
+
className: "mb-1 text-xs font-semibold text-violet-400 uppercase",
|
|
1704
|
+
children: "Recommended Actions"
|
|
1705
|
+
}), /* @__PURE__ */ jsx("ul", {
|
|
1706
|
+
className: "list-inside list-disc space-y-1 text-xs",
|
|
1707
|
+
children: suggestion.proposal.recommendedActions.slice(0, 3).map((action, i) => /* @__PURE__ */ jsx("li", { children: action }, i))
|
|
1708
|
+
})]
|
|
1709
|
+
}),
|
|
1710
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1711
|
+
className: "mt-3 flex items-center justify-between",
|
|
1712
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1713
|
+
className: "flex items-center gap-2 text-xs",
|
|
1714
|
+
children: [
|
|
1715
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1716
|
+
className: "text-muted-foreground",
|
|
1717
|
+
children: [
|
|
1718
|
+
"Confidence: ",
|
|
1719
|
+
(suggestion.confidence * 100).toFixed(0),
|
|
1720
|
+
"%"
|
|
1721
|
+
]
|
|
1722
|
+
}),
|
|
1723
|
+
/* @__PURE__ */ jsx("span", {
|
|
1724
|
+
className: "text-muted-foreground",
|
|
1725
|
+
children: "•"
|
|
1726
|
+
}),
|
|
1727
|
+
/* @__PURE__ */ jsx(Badge, {
|
|
1728
|
+
variant: "secondary",
|
|
1729
|
+
children: suggestion.priority
|
|
1730
|
+
})
|
|
1731
|
+
]
|
|
1732
|
+
}), suggestion.status === "pending" && /* @__PURE__ */ jsxs("div", {
|
|
1733
|
+
className: "flex items-center gap-2",
|
|
1734
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
1735
|
+
variant: "outline",
|
|
1736
|
+
size: "sm",
|
|
1737
|
+
onPress: () => onReject(suggestion.id),
|
|
1738
|
+
children: "Reject"
|
|
1739
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
1740
|
+
variant: "default",
|
|
1741
|
+
size: "sm",
|
|
1742
|
+
onPress: () => onApprove(suggestion.id),
|
|
1743
|
+
children: "Approve"
|
|
1744
|
+
})]
|
|
1745
|
+
})]
|
|
1746
|
+
})
|
|
1747
|
+
]
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
/**
|
|
1751
|
+
* Optimization hint card component
|
|
1752
|
+
*/
|
|
1753
|
+
function HintCard({ hint }) {
|
|
1754
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1755
|
+
className: "rounded-lg border border-blue-500/20 bg-blue-500/5 p-3",
|
|
1756
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1757
|
+
className: "flex items-start gap-3",
|
|
1758
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1759
|
+
className: "text-lg",
|
|
1760
|
+
children: {
|
|
1761
|
+
schema: "📐",
|
|
1762
|
+
policy: "🔒",
|
|
1763
|
+
performance: "⚡",
|
|
1764
|
+
"error-handling": "🛡️"
|
|
1765
|
+
}[hint.category]
|
|
1766
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1767
|
+
className: "flex-1",
|
|
1768
|
+
children: [
|
|
1769
|
+
/* @__PURE__ */ jsx("p", {
|
|
1770
|
+
className: "font-medium",
|
|
1771
|
+
children: hint.summary
|
|
1772
|
+
}),
|
|
1773
|
+
/* @__PURE__ */ jsx("p", {
|
|
1774
|
+
className: "text-muted-foreground mt-1 text-xs",
|
|
1775
|
+
children: hint.justification
|
|
1776
|
+
}),
|
|
1777
|
+
hint.recommendedActions.length > 0 && /* @__PURE__ */ jsx("ul", {
|
|
1778
|
+
className: "mt-2 list-inside list-disc text-xs",
|
|
1779
|
+
children: hint.recommendedActions.slice(0, 2).map((action, i) => /* @__PURE__ */ jsx("li", { children: action }, i))
|
|
1780
|
+
})
|
|
1781
|
+
]
|
|
1782
|
+
})]
|
|
1783
|
+
})
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
//#endregion
|
|
1788
|
+
//#region src/EvolutionSidebar.tsx
|
|
1789
|
+
/**
|
|
1790
|
+
* Compact sidebar for Evolution Engine.
|
|
1791
|
+
* Shows top anomalies, pending suggestions, and quick actions.
|
|
1792
|
+
* Collapsible by default.
|
|
1793
|
+
*/
|
|
1794
|
+
function EvolutionSidebar({ templateId, expanded = false, onToggle, onLog, onOpenEvolution }) {
|
|
1795
|
+
const { anomalies, suggestions, loading, approveSuggestion, rejectSuggestion, operationCount } = useEvolution(templateId);
|
|
1796
|
+
const pendingSuggestions = useMemo(() => suggestions.filter((s) => s.status === "pending"), [suggestions]);
|
|
1797
|
+
const topAnomalies = useMemo(() => anomalies.sort((a, b) => {
|
|
1798
|
+
const severityOrder = {
|
|
1799
|
+
high: 0,
|
|
1800
|
+
medium: 1,
|
|
1801
|
+
low: 2
|
|
1802
|
+
};
|
|
1803
|
+
return severityOrder[a.severity] - severityOrder[b.severity];
|
|
1804
|
+
}).slice(0, 3), [anomalies]);
|
|
1805
|
+
const handleApprove = useCallback((id) => {
|
|
1806
|
+
approveSuggestion(id);
|
|
1807
|
+
onLog?.(`Approved suggestion ${id.slice(0, 8)}`);
|
|
1808
|
+
}, [approveSuggestion, onLog]);
|
|
1809
|
+
const handleReject = useCallback((id) => {
|
|
1810
|
+
rejectSuggestion(id);
|
|
1811
|
+
onLog?.(`Rejected suggestion ${id.slice(0, 8)}`);
|
|
1812
|
+
}, [rejectSuggestion, onLog]);
|
|
1813
|
+
if (!expanded) return /* @__PURE__ */ jsxs("button", {
|
|
1814
|
+
onClick: onToggle,
|
|
1815
|
+
className: "flex items-center gap-2 rounded-lg border border-violet-500/30 bg-violet-500/10 px-3 py-2 text-sm transition hover:bg-violet-500/20",
|
|
1816
|
+
type: "button",
|
|
1817
|
+
children: [
|
|
1818
|
+
/* @__PURE__ */ jsx("span", { children: "🤖" }),
|
|
1819
|
+
/* @__PURE__ */ jsx("span", { children: "Evolution" }),
|
|
1820
|
+
pendingSuggestions.length > 0 && /* @__PURE__ */ jsx(Badge, {
|
|
1821
|
+
variant: "secondary",
|
|
1822
|
+
className: "border-amber-500/30 bg-amber-500/20 text-amber-400",
|
|
1823
|
+
children: pendingSuggestions.length
|
|
1824
|
+
}),
|
|
1825
|
+
anomalies.length > 0 && pendingSuggestions.length === 0 && /* @__PURE__ */ jsx(Badge, {
|
|
1826
|
+
variant: "destructive",
|
|
1827
|
+
children: anomalies.length
|
|
1828
|
+
})
|
|
1829
|
+
]
|
|
1830
|
+
});
|
|
1831
|
+
return /* @__PURE__ */ jsxs(Card, {
|
|
1832
|
+
className: "w-80 overflow-hidden",
|
|
1833
|
+
children: [
|
|
1834
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1835
|
+
className: "flex items-center justify-between border-b border-violet-500/20 bg-violet-500/5 px-3 py-2",
|
|
1836
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1837
|
+
className: "flex items-center gap-2",
|
|
1838
|
+
children: [/* @__PURE__ */ jsx("span", { children: "🤖" }), /* @__PURE__ */ jsx("span", {
|
|
1839
|
+
className: "text-sm font-semibold",
|
|
1840
|
+
children: "Evolution"
|
|
1841
|
+
})]
|
|
1842
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1843
|
+
className: "flex items-center gap-1",
|
|
1844
|
+
children: [onOpenEvolution && /* @__PURE__ */ jsx(Button, {
|
|
1845
|
+
variant: "ghost",
|
|
1846
|
+
size: "sm",
|
|
1847
|
+
onPress: onOpenEvolution,
|
|
1848
|
+
children: "Expand"
|
|
1849
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
1850
|
+
onClick: onToggle,
|
|
1851
|
+
className: "text-muted-foreground hover:text-foreground p-1",
|
|
1852
|
+
type: "button",
|
|
1853
|
+
title: "Collapse",
|
|
1854
|
+
children: "✕"
|
|
1855
|
+
})]
|
|
1856
|
+
})]
|
|
1857
|
+
}),
|
|
1858
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1859
|
+
className: "max-h-96 overflow-y-auto p-3",
|
|
1860
|
+
children: [
|
|
1861
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1862
|
+
className: "mb-3 flex items-center justify-between text-xs",
|
|
1863
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
1864
|
+
className: "text-muted-foreground",
|
|
1865
|
+
children: [operationCount, " ops tracked"]
|
|
1866
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1867
|
+
className: "flex items-center gap-2",
|
|
1868
|
+
children: [anomalies.length > 0 && /* @__PURE__ */ jsxs(Badge, {
|
|
1869
|
+
variant: "destructive",
|
|
1870
|
+
children: [anomalies.length, " anomalies"]
|
|
1871
|
+
}), pendingSuggestions.length > 0 && /* @__PURE__ */ jsxs(Badge, {
|
|
1872
|
+
variant: "secondary",
|
|
1873
|
+
className: "border-amber-500/30 bg-amber-500/20 text-amber-400",
|
|
1874
|
+
children: [pendingSuggestions.length, " pending"]
|
|
1875
|
+
})]
|
|
1876
|
+
})]
|
|
1877
|
+
}),
|
|
1878
|
+
loading && /* @__PURE__ */ jsx("div", {
|
|
1879
|
+
className: "text-muted-foreground py-4 text-center text-sm",
|
|
1880
|
+
children: "Generating suggestions..."
|
|
1881
|
+
}),
|
|
1882
|
+
topAnomalies.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
1883
|
+
className: "mb-4",
|
|
1884
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
1885
|
+
className: "mb-2 text-xs font-semibold text-violet-400 uppercase",
|
|
1886
|
+
children: "Top Issues"
|
|
1887
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1888
|
+
className: "space-y-2",
|
|
1889
|
+
children: topAnomalies.map((anomaly, index) => /* @__PURE__ */ jsxs("div", {
|
|
1890
|
+
className: "rounded border border-amber-500/20 bg-amber-500/5 p-2 text-xs",
|
|
1891
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1892
|
+
className: "flex items-center gap-2",
|
|
1893
|
+
children: [/* @__PURE__ */ jsx("span", { children: anomaly.severity === "high" ? "🔴" : anomaly.severity === "medium" ? "🟠" : "🟡" }), /* @__PURE__ */ jsx("span", {
|
|
1894
|
+
className: "truncate font-medium",
|
|
1895
|
+
children: anomaly.operation.name
|
|
1896
|
+
})]
|
|
1897
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
1898
|
+
className: "text-muted-foreground mt-1 truncate",
|
|
1899
|
+
children: anomaly.description
|
|
1900
|
+
})]
|
|
1901
|
+
}, `${anomaly.operation.name}-${index}`))
|
|
1902
|
+
})]
|
|
1903
|
+
}),
|
|
1904
|
+
pendingSuggestions.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("p", {
|
|
1905
|
+
className: "mb-2 text-xs font-semibold text-violet-400 uppercase",
|
|
1906
|
+
children: "Pending Suggestions"
|
|
1907
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1908
|
+
className: "space-y-2",
|
|
1909
|
+
children: [pendingSuggestions.slice(0, 3).map((suggestion) => /* @__PURE__ */ jsx(CompactSuggestionCard, {
|
|
1910
|
+
suggestion,
|
|
1911
|
+
onApprove: handleApprove,
|
|
1912
|
+
onReject: handleReject
|
|
1913
|
+
}, suggestion.id)), pendingSuggestions.length > 3 && /* @__PURE__ */ jsxs("p", {
|
|
1914
|
+
className: "text-muted-foreground text-center text-xs",
|
|
1915
|
+
children: [
|
|
1916
|
+
"+",
|
|
1917
|
+
pendingSuggestions.length - 3,
|
|
1918
|
+
" more suggestions"
|
|
1919
|
+
]
|
|
1920
|
+
})]
|
|
1921
|
+
})] }),
|
|
1922
|
+
anomalies.length === 0 && pendingSuggestions.length === 0 && !loading && /* @__PURE__ */ jsx("div", {
|
|
1923
|
+
className: "text-muted-foreground py-4 text-center text-xs",
|
|
1924
|
+
children: "No issues detected. Keep coding!"
|
|
1925
|
+
})
|
|
1926
|
+
]
|
|
1927
|
+
}),
|
|
1928
|
+
onOpenEvolution && /* @__PURE__ */ jsx("div", {
|
|
1929
|
+
className: "border-t border-violet-500/20 p-2",
|
|
1930
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
1931
|
+
variant: "ghost",
|
|
1932
|
+
size: "sm",
|
|
1933
|
+
className: "w-full",
|
|
1934
|
+
onPress: onOpenEvolution,
|
|
1935
|
+
children: "Open Evolution Dashboard →"
|
|
1936
|
+
})
|
|
1937
|
+
})
|
|
1938
|
+
]
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Compact suggestion card for sidebar
|
|
1943
|
+
*/
|
|
1944
|
+
function CompactSuggestionCard({ suggestion, onApprove, onReject }) {
|
|
1945
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1946
|
+
className: "rounded border border-violet-500/20 bg-violet-500/5 p-2",
|
|
1947
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1948
|
+
className: "flex items-start justify-between gap-2",
|
|
1949
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
1950
|
+
className: "min-w-0 flex-1",
|
|
1951
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
1952
|
+
className: "truncate text-xs font-medium",
|
|
1953
|
+
children: suggestion.proposal.summary
|
|
1954
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1955
|
+
className: "mt-1 flex items-center gap-2 text-xs",
|
|
1956
|
+
children: [/* @__PURE__ */ jsx(Badge, {
|
|
1957
|
+
variant: "secondary",
|
|
1958
|
+
children: suggestion.priority
|
|
1959
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
1960
|
+
className: "text-muted-foreground",
|
|
1961
|
+
children: [(suggestion.confidence * 100).toFixed(0), "%"]
|
|
1962
|
+
})]
|
|
1963
|
+
})]
|
|
1964
|
+
})
|
|
1965
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1966
|
+
className: "mt-2 flex justify-end gap-1",
|
|
1967
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
1968
|
+
onClick: () => onReject(suggestion.id),
|
|
1969
|
+
className: "rounded px-2 py-0.5 text-xs text-red-400 hover:bg-red-400/10",
|
|
1970
|
+
type: "button",
|
|
1971
|
+
children: "Reject"
|
|
1972
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
1973
|
+
onClick: () => onApprove(suggestion.id),
|
|
1974
|
+
className: "rounded bg-violet-500/20 px-2 py-0.5 text-xs text-violet-400 hover:bg-violet-500/30",
|
|
1975
|
+
type: "button",
|
|
1976
|
+
children: "Approve"
|
|
1977
|
+
})]
|
|
1978
|
+
})]
|
|
1979
|
+
});
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
//#endregion
|
|
1983
|
+
//#region src/OverlayContextProvider.tsx
|
|
1984
|
+
const OverlayContext = React.createContext(null);
|
|
1985
|
+
/**
|
|
1986
|
+
* Provider for overlay engine context.
|
|
1987
|
+
* Loads template-specific overlays and provides helper functions.
|
|
1988
|
+
*/
|
|
1989
|
+
function OverlayContextProvider({ templateId, role = "user", device = "desktop", children }) {
|
|
1990
|
+
const overlays = useMemo(() => getTemplateOverlays(templateId, role), [templateId, role]);
|
|
1991
|
+
const activeOverlays = useMemo(() => {
|
|
1992
|
+
return overlays.filter((overlay) => {
|
|
1993
|
+
const conditions = overlay.conditions;
|
|
1994
|
+
if (!conditions) return true;
|
|
1995
|
+
if (conditions.role && !conditions.role.includes(role)) return false;
|
|
1996
|
+
if (conditions.device && conditions.device !== "any" && conditions.device !== device) return false;
|
|
1997
|
+
return true;
|
|
1998
|
+
});
|
|
1999
|
+
}, [
|
|
2000
|
+
overlays,
|
|
2001
|
+
role,
|
|
2002
|
+
device
|
|
2003
|
+
]);
|
|
2004
|
+
const overlayMap = useMemo(() => {
|
|
2005
|
+
const map = /* @__PURE__ */ new Map();
|
|
2006
|
+
for (const overlay of activeOverlays) map.set(overlay.target, overlay);
|
|
2007
|
+
return map;
|
|
2008
|
+
}, [activeOverlays]);
|
|
2009
|
+
const applyOverlay = useMemo(() => (path, target) => {
|
|
2010
|
+
const overlay = overlayMap.get(path);
|
|
2011
|
+
if (!overlay) return target;
|
|
2012
|
+
let result = { ...target };
|
|
2013
|
+
for (const mod of overlay.modifications) switch (mod.op) {
|
|
2014
|
+
case "hide":
|
|
2015
|
+
result = {
|
|
2016
|
+
...result,
|
|
2017
|
+
hidden: true
|
|
2018
|
+
};
|
|
2019
|
+
break;
|
|
2020
|
+
case "relabel":
|
|
2021
|
+
result = {
|
|
2022
|
+
...result,
|
|
2023
|
+
label: mod.label
|
|
2024
|
+
};
|
|
2025
|
+
break;
|
|
2026
|
+
case "reorder":
|
|
2027
|
+
result = {
|
|
2028
|
+
...result,
|
|
2029
|
+
position: mod.position
|
|
2030
|
+
};
|
|
2031
|
+
break;
|
|
2032
|
+
case "restyle":
|
|
2033
|
+
result = {
|
|
2034
|
+
...result,
|
|
2035
|
+
className: mod.className,
|
|
2036
|
+
variant: mod.variant
|
|
2037
|
+
};
|
|
2038
|
+
break;
|
|
2039
|
+
case "set-default":
|
|
2040
|
+
result = {
|
|
2041
|
+
...result,
|
|
2042
|
+
defaultValue: mod.value
|
|
2043
|
+
};
|
|
2044
|
+
break;
|
|
2045
|
+
}
|
|
2046
|
+
return result;
|
|
2047
|
+
}, [overlayMap]);
|
|
2048
|
+
const isHidden = useMemo(() => (path) => {
|
|
2049
|
+
return overlayMap.get(path)?.modifications.some((m) => m.op === "hide") ?? false;
|
|
2050
|
+
}, [overlayMap]);
|
|
2051
|
+
const getLabel = useMemo(() => (path, defaultLabel) => {
|
|
2052
|
+
const relabel = overlayMap.get(path)?.modifications.find((m) => m.op === "relabel");
|
|
2053
|
+
return relabel && relabel.op === "relabel" ? relabel.label : defaultLabel;
|
|
2054
|
+
}, [overlayMap]);
|
|
2055
|
+
const getPosition = useMemo(() => (path, defaultPosition) => {
|
|
2056
|
+
const reorder = overlayMap.get(path)?.modifications.find((m) => m.op === "reorder");
|
|
2057
|
+
return reorder && reorder.op === "reorder" ? reorder.position : defaultPosition;
|
|
2058
|
+
}, [overlayMap]);
|
|
2059
|
+
const getStyle = useMemo(() => (path) => {
|
|
2060
|
+
const restyle = overlayMap.get(path)?.modifications.find((m) => m.op === "restyle");
|
|
2061
|
+
if (restyle && restyle.op === "restyle") return {
|
|
2062
|
+
className: restyle.className,
|
|
2063
|
+
variant: restyle.variant
|
|
2064
|
+
};
|
|
2065
|
+
return {};
|
|
2066
|
+
}, [overlayMap]);
|
|
2067
|
+
const getDefault = useMemo(() => (path, defaultValue) => {
|
|
2068
|
+
const setDefault = overlayMap.get(path)?.modifications.find((m) => m.op === "set-default");
|
|
2069
|
+
return setDefault && setDefault.op === "set-default" ? setDefault.value : defaultValue;
|
|
2070
|
+
}, [overlayMap]);
|
|
2071
|
+
const value = useMemo(() => ({
|
|
2072
|
+
overlays: activeOverlays,
|
|
2073
|
+
applyOverlay,
|
|
2074
|
+
isHidden,
|
|
2075
|
+
getLabel,
|
|
2076
|
+
getPosition,
|
|
2077
|
+
getStyle,
|
|
2078
|
+
getDefault,
|
|
2079
|
+
role,
|
|
2080
|
+
device
|
|
2081
|
+
}), [
|
|
2082
|
+
activeOverlays,
|
|
2083
|
+
applyOverlay,
|
|
2084
|
+
isHidden,
|
|
2085
|
+
getLabel,
|
|
2086
|
+
getPosition,
|
|
2087
|
+
getStyle,
|
|
2088
|
+
getDefault,
|
|
2089
|
+
role,
|
|
2090
|
+
device
|
|
2091
|
+
]);
|
|
2092
|
+
return /* @__PURE__ */ jsx(OverlayContext.Provider, {
|
|
2093
|
+
value,
|
|
2094
|
+
children
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
/**
|
|
2098
|
+
* Hook to access overlay context
|
|
2099
|
+
*/
|
|
2100
|
+
function useOverlayContext() {
|
|
2101
|
+
const context = useContext(OverlayContext);
|
|
2102
|
+
if (!context) throw new Error("useOverlayContext must be used within an OverlayContextProvider");
|
|
2103
|
+
return context;
|
|
2104
|
+
}
|
|
2105
|
+
/**
|
|
2106
|
+
* Hook to check if within overlay context
|
|
2107
|
+
*/
|
|
2108
|
+
function useIsInOverlayContext() {
|
|
2109
|
+
return useContext(OverlayContext) !== null;
|
|
2110
|
+
}
|
|
2111
|
+
/**
|
|
2112
|
+
* Get template-specific overlays
|
|
2113
|
+
*/
|
|
2114
|
+
function getTemplateOverlays(templateId, _role) {
|
|
2115
|
+
return {
|
|
2116
|
+
"crm-pipeline": [{
|
|
2117
|
+
id: "crm-hide-internal-fields",
|
|
2118
|
+
target: "deal.internalNotes",
|
|
2119
|
+
modifications: [{ op: "hide" }],
|
|
2120
|
+
conditions: { role: ["viewer", "user"] }
|
|
2121
|
+
}, {
|
|
2122
|
+
id: "crm-relabel-value",
|
|
2123
|
+
target: "deal.value",
|
|
2124
|
+
modifications: [{
|
|
2125
|
+
op: "relabel",
|
|
2126
|
+
label: "Deal Amount"
|
|
2127
|
+
}]
|
|
2128
|
+
}],
|
|
2129
|
+
"saas-boilerplate": [{
|
|
2130
|
+
id: "saas-hide-billing",
|
|
2131
|
+
target: "settings.billing",
|
|
2132
|
+
modifications: [{ op: "hide" }],
|
|
2133
|
+
conditions: { role: ["viewer"] }
|
|
2134
|
+
}, {
|
|
2135
|
+
id: "saas-restyle-plan",
|
|
2136
|
+
target: "settings.plan",
|
|
2137
|
+
modifications: [{
|
|
2138
|
+
op: "restyle",
|
|
2139
|
+
variant: "premium"
|
|
2140
|
+
}],
|
|
2141
|
+
conditions: { role: ["admin"] }
|
|
2142
|
+
}],
|
|
2143
|
+
"agent-console": [{
|
|
2144
|
+
id: "agent-hide-cost",
|
|
2145
|
+
target: "run.cost",
|
|
2146
|
+
modifications: [{ op: "hide" }],
|
|
2147
|
+
conditions: { role: ["viewer"] }
|
|
2148
|
+
}, {
|
|
2149
|
+
id: "agent-relabel-tokens",
|
|
2150
|
+
target: "run.tokens",
|
|
2151
|
+
modifications: [{
|
|
2152
|
+
op: "relabel",
|
|
2153
|
+
label: "Token Usage"
|
|
2154
|
+
}]
|
|
2155
|
+
}],
|
|
2156
|
+
"todos-app": [{
|
|
2157
|
+
id: "todos-hide-assignee",
|
|
2158
|
+
target: "task.assignee",
|
|
2159
|
+
modifications: [{ op: "hide" }],
|
|
2160
|
+
conditions: { device: "mobile" }
|
|
2161
|
+
}],
|
|
2162
|
+
"messaging-app": [{
|
|
2163
|
+
id: "messaging-reorder-timestamp",
|
|
2164
|
+
target: "message.timestamp",
|
|
2165
|
+
modifications: [{
|
|
2166
|
+
op: "reorder",
|
|
2167
|
+
position: 0
|
|
2168
|
+
}]
|
|
2169
|
+
}],
|
|
2170
|
+
"recipe-app-i18n": [{
|
|
2171
|
+
id: "recipe-relabel-servings",
|
|
2172
|
+
target: "recipe.servings",
|
|
2173
|
+
modifications: [{
|
|
2174
|
+
op: "relabel",
|
|
2175
|
+
label: "Portions"
|
|
2176
|
+
}]
|
|
2177
|
+
}]
|
|
2178
|
+
}[templateId] ?? [];
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
//#endregion
|
|
2182
|
+
//#region src/hooks/useBehaviorTracking.ts
|
|
2183
|
+
/**
|
|
2184
|
+
* Storage key for behavior data
|
|
2185
|
+
*/
|
|
2186
|
+
const BEHAVIOR_STORAGE_KEY = "contractspec-behavior-data";
|
|
2187
|
+
/**
|
|
2188
|
+
* All available features in the sandbox
|
|
2189
|
+
*/
|
|
2190
|
+
const ALL_FEATURES = [
|
|
2191
|
+
"playground",
|
|
2192
|
+
"specs",
|
|
2193
|
+
"builder",
|
|
2194
|
+
"markdown",
|
|
2195
|
+
"evolution",
|
|
2196
|
+
"canvas_add",
|
|
2197
|
+
"canvas_delete",
|
|
2198
|
+
"spec_save",
|
|
2199
|
+
"spec_validate",
|
|
2200
|
+
"ai_suggestions"
|
|
2201
|
+
];
|
|
2202
|
+
/**
|
|
2203
|
+
* Hook for tracking user behavior in the sandbox.
|
|
2204
|
+
* Provides insights into usage patterns and feature adoption.
|
|
2205
|
+
*/
|
|
2206
|
+
function useBehaviorTracking(templateId) {
|
|
2207
|
+
const [events, setEvents] = useState([]);
|
|
2208
|
+
const sessionStartRef = useRef(/* @__PURE__ */ new Date());
|
|
2209
|
+
const [eventCount, setEventCount] = useState(0);
|
|
2210
|
+
useEffect(() => {
|
|
2211
|
+
try {
|
|
2212
|
+
const stored = localStorage.getItem(BEHAVIOR_STORAGE_KEY);
|
|
2213
|
+
if (stored) {
|
|
2214
|
+
const data = JSON.parse(stored);
|
|
2215
|
+
setEvents(data.events.map((e) => ({
|
|
2216
|
+
...e,
|
|
2217
|
+
timestamp: new Date(e.timestamp)
|
|
2218
|
+
})));
|
|
2219
|
+
sessionStartRef.current = new Date(data.sessionStart);
|
|
2220
|
+
}
|
|
2221
|
+
} catch {}
|
|
2222
|
+
}, []);
|
|
2223
|
+
useEffect(() => {
|
|
2224
|
+
if (events.length > 0) try {
|
|
2225
|
+
localStorage.setItem(BEHAVIOR_STORAGE_KEY, JSON.stringify({
|
|
2226
|
+
events: events.map((e) => ({
|
|
2227
|
+
...e,
|
|
2228
|
+
timestamp: e.timestamp.toISOString()
|
|
2229
|
+
})),
|
|
2230
|
+
sessionStart: sessionStartRef.current.toISOString()
|
|
2231
|
+
}));
|
|
2232
|
+
} catch {}
|
|
2233
|
+
}, [events]);
|
|
2234
|
+
/**
|
|
2235
|
+
* Track a behavior event
|
|
2236
|
+
*/
|
|
2237
|
+
const trackEvent = useCallback((type, metadata) => {
|
|
2238
|
+
const event = {
|
|
2239
|
+
type,
|
|
2240
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2241
|
+
templateId,
|
|
2242
|
+
metadata
|
|
2243
|
+
};
|
|
2244
|
+
setEvents((prev) => [...prev, event]);
|
|
2245
|
+
setEventCount((prev) => prev + 1);
|
|
2246
|
+
}, [templateId]);
|
|
2247
|
+
/**
|
|
2248
|
+
* Get events by type
|
|
2249
|
+
*/
|
|
2250
|
+
const getEventsByType = useCallback((type) => {
|
|
2251
|
+
return events.filter((e) => e.type === type);
|
|
2252
|
+
}, [events]);
|
|
2253
|
+
/**
|
|
2254
|
+
* Get behavior summary
|
|
2255
|
+
*/
|
|
2256
|
+
const getSummary = useCallback(() => {
|
|
2257
|
+
const sessionDuration = (/* @__PURE__ */ new Date()).getTime() - sessionStartRef.current.getTime();
|
|
2258
|
+
const templateCounts = /* @__PURE__ */ new Map();
|
|
2259
|
+
for (const event of events) {
|
|
2260
|
+
const count = templateCounts.get(event.templateId) ?? 0;
|
|
2261
|
+
templateCounts.set(event.templateId, count + 1);
|
|
2262
|
+
}
|
|
2263
|
+
const mostUsedTemplates = Array.from(templateCounts.entries()).map(([templateId$1, count]) => ({
|
|
2264
|
+
templateId: templateId$1,
|
|
2265
|
+
count
|
|
2266
|
+
})).sort((a, b) => b.count - a.count).slice(0, 3);
|
|
2267
|
+
const modeCounts = /* @__PURE__ */ new Map();
|
|
2268
|
+
for (const event of events) if (event.type === "mode_change" && event.metadata?.mode) {
|
|
2269
|
+
const mode = event.metadata.mode;
|
|
2270
|
+
const count = modeCounts.get(mode) ?? 0;
|
|
2271
|
+
modeCounts.set(mode, count + 1);
|
|
2272
|
+
}
|
|
2273
|
+
const mostUsedModes = Array.from(modeCounts.entries()).map(([mode, count]) => ({
|
|
2274
|
+
mode,
|
|
2275
|
+
count
|
|
2276
|
+
})).sort((a, b) => b.count - a.count);
|
|
2277
|
+
const featuresUsed = /* @__PURE__ */ new Set();
|
|
2278
|
+
for (const event of events) {
|
|
2279
|
+
if (event.type === "mode_change" && event.metadata?.mode) featuresUsed.add(event.metadata.mode);
|
|
2280
|
+
if (event.type === "feature_usage" && event.metadata?.feature) featuresUsed.add(event.metadata.feature);
|
|
2281
|
+
if (event.type === "canvas_interaction") {
|
|
2282
|
+
const action = event.metadata?.action;
|
|
2283
|
+
if (action === "add") featuresUsed.add("canvas_add");
|
|
2284
|
+
if (action === "delete") featuresUsed.add("canvas_delete");
|
|
2285
|
+
}
|
|
2286
|
+
if (event.type === "spec_edit") {
|
|
2287
|
+
const action = event.metadata?.action;
|
|
2288
|
+
if (action === "save") featuresUsed.add("spec_save");
|
|
2289
|
+
if (action === "validate") featuresUsed.add("spec_validate");
|
|
2290
|
+
}
|
|
2291
|
+
}
|
|
2292
|
+
const unusedFeatures = ALL_FEATURES.filter((f) => !featuresUsed.has(f));
|
|
2293
|
+
const errorCount = events.filter((e) => e.type === "error").length;
|
|
2294
|
+
const recommendations = generateRecommendations(Array.from(featuresUsed), unusedFeatures, mostUsedModes, events.length);
|
|
2295
|
+
return {
|
|
2296
|
+
totalEvents: events.length,
|
|
2297
|
+
sessionDuration,
|
|
2298
|
+
mostUsedTemplates,
|
|
2299
|
+
mostUsedModes,
|
|
2300
|
+
featuresUsed: Array.from(featuresUsed),
|
|
2301
|
+
unusedFeatures,
|
|
2302
|
+
errorCount,
|
|
2303
|
+
recommendations
|
|
2304
|
+
};
|
|
2305
|
+
}, [events]);
|
|
2306
|
+
/**
|
|
2307
|
+
* Clear all tracking data
|
|
2308
|
+
*/
|
|
2309
|
+
const clear = useCallback(() => {
|
|
2310
|
+
setEvents([]);
|
|
2311
|
+
setEventCount(0);
|
|
2312
|
+
sessionStartRef.current = /* @__PURE__ */ new Date();
|
|
2313
|
+
localStorage.removeItem(BEHAVIOR_STORAGE_KEY);
|
|
2314
|
+
}, []);
|
|
2315
|
+
return useMemo(() => ({
|
|
2316
|
+
trackEvent,
|
|
2317
|
+
getSummary,
|
|
2318
|
+
getEventsByType,
|
|
2319
|
+
eventCount,
|
|
2320
|
+
sessionStart: sessionStartRef.current,
|
|
2321
|
+
clear
|
|
2322
|
+
}), [
|
|
2323
|
+
trackEvent,
|
|
2324
|
+
getSummary,
|
|
2325
|
+
getEventsByType,
|
|
2326
|
+
eventCount,
|
|
2327
|
+
clear
|
|
2328
|
+
]);
|
|
2329
|
+
}
|
|
2330
|
+
/**
|
|
2331
|
+
* Generate recommendations based on behavior
|
|
2332
|
+
*/
|
|
2333
|
+
function generateRecommendations(featuresUsed, unusedFeatures, mostUsedModes, totalEvents) {
|
|
2334
|
+
const recommendations = [];
|
|
2335
|
+
if (unusedFeatures.includes("evolution")) recommendations.push("Try the AI Evolution mode to get automated improvement suggestions");
|
|
2336
|
+
if (unusedFeatures.includes("markdown")) recommendations.push("Use Markdown preview to see documentation for your specs");
|
|
2337
|
+
if (unusedFeatures.includes("builder")) recommendations.push("Explore the Visual Builder to design your UI components");
|
|
2338
|
+
if (!featuresUsed.includes("spec_validate") && featuresUsed.includes("specs")) recommendations.push("Don't forget to validate your specs before saving");
|
|
2339
|
+
if (featuresUsed.includes("evolution") && !featuresUsed.includes("ai_suggestions")) recommendations.push("Generate AI suggestions to get actionable improvement recommendations");
|
|
2340
|
+
if (totalEvents > 50) recommendations.push("Great engagement! Consider saving your work regularly");
|
|
2341
|
+
if (mostUsedModes.length === 1) recommendations.push("Try different modes to explore all sandbox capabilities");
|
|
2342
|
+
return recommendations;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
//#endregion
|
|
2346
|
+
//#region src/PersonalizationInsights.tsx
|
|
2347
|
+
/**
|
|
2348
|
+
* Component showing personalization insights based on user behavior.
|
|
2349
|
+
* Displays usage patterns, recommendations, and feature adoption.
|
|
2350
|
+
*/
|
|
2351
|
+
function PersonalizationInsights({ templateId, collapsed = false, onToggle }) {
|
|
2352
|
+
const { getSummary, eventCount, clear, sessionStart } = useBehaviorTracking(templateId);
|
|
2353
|
+
const [showDetails, setShowDetails] = useState(false);
|
|
2354
|
+
const summary = useMemo(() => getSummary(), [getSummary]);
|
|
2355
|
+
const formatDuration = useCallback((ms) => {
|
|
2356
|
+
const seconds = Math.floor(ms / 1e3);
|
|
2357
|
+
const minutes = Math.floor(seconds / 60);
|
|
2358
|
+
const hours = Math.floor(minutes / 60);
|
|
2359
|
+
if (hours > 0) return `${hours}h ${minutes % 60}m`;
|
|
2360
|
+
if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
|
|
2361
|
+
return `${seconds}s`;
|
|
2362
|
+
}, []);
|
|
2363
|
+
const handleClear = useCallback(() => {
|
|
2364
|
+
clear();
|
|
2365
|
+
}, [clear]);
|
|
2366
|
+
if (collapsed) return /* @__PURE__ */ jsxs("button", {
|
|
2367
|
+
onClick: onToggle,
|
|
2368
|
+
className: "flex items-center gap-2 rounded-lg border border-blue-500/30 bg-blue-500/10 px-3 py-2 text-sm transition hover:bg-blue-500/20",
|
|
2369
|
+
type: "button",
|
|
2370
|
+
children: [
|
|
2371
|
+
/* @__PURE__ */ jsx("span", { children: "📊" }),
|
|
2372
|
+
/* @__PURE__ */ jsx("span", { children: "Insights" }),
|
|
2373
|
+
/* @__PURE__ */ jsx(Badge, {
|
|
2374
|
+
variant: "secondary",
|
|
2375
|
+
children: eventCount
|
|
2376
|
+
})
|
|
2377
|
+
]
|
|
2378
|
+
});
|
|
2379
|
+
return /* @__PURE__ */ jsxs(Card, {
|
|
2380
|
+
className: "overflow-hidden",
|
|
2381
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2382
|
+
className: "flex items-center justify-between border-b border-blue-500/20 bg-blue-500/5 px-4 py-3",
|
|
2383
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2384
|
+
className: "flex items-center gap-2",
|
|
2385
|
+
children: [/* @__PURE__ */ jsx("span", { children: "📊" }), /* @__PURE__ */ jsx("span", {
|
|
2386
|
+
className: "font-semibold",
|
|
2387
|
+
children: "Personalization Insights"
|
|
2388
|
+
})]
|
|
2389
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
2390
|
+
className: "flex items-center gap-2",
|
|
2391
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
2392
|
+
variant: "ghost",
|
|
2393
|
+
size: "sm",
|
|
2394
|
+
onPress: () => setShowDetails(!showDetails),
|
|
2395
|
+
children: showDetails ? "Hide Details" : "Show Details"
|
|
2396
|
+
}), onToggle && /* @__PURE__ */ jsx("button", {
|
|
2397
|
+
onClick: onToggle,
|
|
2398
|
+
className: "text-muted-foreground hover:text-foreground p-1",
|
|
2399
|
+
type: "button",
|
|
2400
|
+
title: "Collapse",
|
|
2401
|
+
children: "✕"
|
|
2402
|
+
})]
|
|
2403
|
+
})]
|
|
2404
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
2405
|
+
className: "p-4",
|
|
2406
|
+
children: [
|
|
2407
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2408
|
+
className: "mb-4 grid grid-cols-2 gap-3 md:grid-cols-4",
|
|
2409
|
+
children: [
|
|
2410
|
+
/* @__PURE__ */ jsx(StatCard, {
|
|
2411
|
+
label: "Session Time",
|
|
2412
|
+
value: formatDuration(summary.sessionDuration),
|
|
2413
|
+
icon: "⏱️"
|
|
2414
|
+
}),
|
|
2415
|
+
/* @__PURE__ */ jsx(StatCard, {
|
|
2416
|
+
label: "Events Tracked",
|
|
2417
|
+
value: summary.totalEvents.toString(),
|
|
2418
|
+
icon: "📈"
|
|
2419
|
+
}),
|
|
2420
|
+
/* @__PURE__ */ jsx(StatCard, {
|
|
2421
|
+
label: "Features Used",
|
|
2422
|
+
value: `${summary.featuresUsed.length}/${summary.featuresUsed.length + summary.unusedFeatures.length}`,
|
|
2423
|
+
icon: "✨"
|
|
2424
|
+
}),
|
|
2425
|
+
/* @__PURE__ */ jsx(StatCard, {
|
|
2426
|
+
label: "Errors",
|
|
2427
|
+
value: summary.errorCount.toString(),
|
|
2428
|
+
icon: "⚠️",
|
|
2429
|
+
variant: summary.errorCount > 0 ? "warning" : "success"
|
|
2430
|
+
})
|
|
2431
|
+
]
|
|
2432
|
+
}),
|
|
2433
|
+
summary.recommendations.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
2434
|
+
className: "mb-4",
|
|
2435
|
+
children: [/* @__PURE__ */ jsx("h4", {
|
|
2436
|
+
className: "mb-2 text-xs font-semibold text-blue-400 uppercase",
|
|
2437
|
+
children: "Recommendations"
|
|
2438
|
+
}), /* @__PURE__ */ jsx("ul", {
|
|
2439
|
+
className: "space-y-1",
|
|
2440
|
+
children: summary.recommendations.map((rec, index) => /* @__PURE__ */ jsxs("li", {
|
|
2441
|
+
className: "flex items-start gap-2 text-sm",
|
|
2442
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
2443
|
+
className: "text-blue-400",
|
|
2444
|
+
children: "💡"
|
|
2445
|
+
}), /* @__PURE__ */ jsx("span", { children: rec })]
|
|
2446
|
+
}, index))
|
|
2447
|
+
})]
|
|
2448
|
+
}),
|
|
2449
|
+
summary.unusedFeatures.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
2450
|
+
className: "mb-4",
|
|
2451
|
+
children: [/* @__PURE__ */ jsx("h4", {
|
|
2452
|
+
className: "mb-2 text-xs font-semibold text-blue-400 uppercase",
|
|
2453
|
+
children: "Try These Features"
|
|
2454
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
2455
|
+
className: "flex flex-wrap gap-2",
|
|
2456
|
+
children: summary.unusedFeatures.slice(0, 5).map((feature) => /* @__PURE__ */ jsx(Badge, {
|
|
2457
|
+
variant: "secondary",
|
|
2458
|
+
children: formatFeatureName(feature)
|
|
2459
|
+
}, feature))
|
|
2460
|
+
})]
|
|
2461
|
+
}),
|
|
2462
|
+
showDetails && /* @__PURE__ */ jsx(DetailedInsights, {
|
|
2463
|
+
summary,
|
|
2464
|
+
sessionStart
|
|
2465
|
+
}),
|
|
2466
|
+
/* @__PURE__ */ jsx("div", {
|
|
2467
|
+
className: "mt-4 flex justify-end border-t border-blue-500/10 pt-4",
|
|
2468
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
2469
|
+
variant: "ghost",
|
|
2470
|
+
size: "sm",
|
|
2471
|
+
onPress: handleClear,
|
|
2472
|
+
children: "Clear Data"
|
|
2473
|
+
})
|
|
2474
|
+
})
|
|
2475
|
+
]
|
|
2476
|
+
})]
|
|
2477
|
+
});
|
|
2478
|
+
}
|
|
2479
|
+
/**
|
|
2480
|
+
* Stat card component
|
|
2481
|
+
*/
|
|
2482
|
+
function StatCard({ label, value, icon, variant = "default" }) {
|
|
2483
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2484
|
+
className: `rounded-lg border p-3 text-center ${{
|
|
2485
|
+
default: "bg-blue-500/5 border-blue-500/20",
|
|
2486
|
+
warning: "bg-amber-500/5 border-amber-500/20",
|
|
2487
|
+
success: "bg-green-500/5 border-green-500/20"
|
|
2488
|
+
}[variant]}`,
|
|
2489
|
+
children: [
|
|
2490
|
+
/* @__PURE__ */ jsx("div", {
|
|
2491
|
+
className: "mb-1 text-lg",
|
|
2492
|
+
children: icon
|
|
2493
|
+
}),
|
|
2494
|
+
/* @__PURE__ */ jsx("div", {
|
|
2495
|
+
className: "text-lg font-bold",
|
|
2496
|
+
children: value
|
|
2497
|
+
}),
|
|
2498
|
+
/* @__PURE__ */ jsx("div", {
|
|
2499
|
+
className: "text-muted-foreground text-xs",
|
|
2500
|
+
children: label
|
|
2501
|
+
})
|
|
2502
|
+
]
|
|
2503
|
+
});
|
|
2504
|
+
}
|
|
2505
|
+
/**
|
|
2506
|
+
* Detailed insights section
|
|
2507
|
+
*/
|
|
2508
|
+
function DetailedInsights({ summary, sessionStart }) {
|
|
2509
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2510
|
+
className: "mt-4 space-y-4 border-t border-blue-500/10 pt-4",
|
|
2511
|
+
children: [
|
|
2512
|
+
summary.mostUsedTemplates.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h4", {
|
|
2513
|
+
className: "mb-2 text-xs font-semibold text-blue-400 uppercase",
|
|
2514
|
+
children: "Most Used Templates"
|
|
2515
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
2516
|
+
className: "space-y-1",
|
|
2517
|
+
children: summary.mostUsedTemplates.map(({ templateId, count }) => /* @__PURE__ */ jsxs("div", {
|
|
2518
|
+
className: "flex items-center justify-between text-sm",
|
|
2519
|
+
children: [/* @__PURE__ */ jsx("span", { children: formatTemplateId(templateId) }), /* @__PURE__ */ jsxs("span", {
|
|
2520
|
+
className: "text-muted-foreground",
|
|
2521
|
+
children: [count, " events"]
|
|
2522
|
+
})]
|
|
2523
|
+
}, templateId))
|
|
2524
|
+
})] }),
|
|
2525
|
+
summary.mostUsedModes.length > 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h4", {
|
|
2526
|
+
className: "mb-2 text-xs font-semibold text-blue-400 uppercase",
|
|
2527
|
+
children: "Mode Usage"
|
|
2528
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
2529
|
+
className: "space-y-1",
|
|
2530
|
+
children: summary.mostUsedModes.map(({ mode, count }) => /* @__PURE__ */ jsxs("div", {
|
|
2531
|
+
className: "flex items-center justify-between text-sm",
|
|
2532
|
+
children: [/* @__PURE__ */ jsx("span", { children: formatFeatureName(mode) }), /* @__PURE__ */ jsxs("span", {
|
|
2533
|
+
className: "text-muted-foreground",
|
|
2534
|
+
children: [count, " switches"]
|
|
2535
|
+
})]
|
|
2536
|
+
}, mode))
|
|
2537
|
+
})] }),
|
|
2538
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("h4", {
|
|
2539
|
+
className: "mb-2 text-xs font-semibold text-blue-400 uppercase",
|
|
2540
|
+
children: "Features Used"
|
|
2541
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
2542
|
+
className: "flex flex-wrap gap-2",
|
|
2543
|
+
children: summary.featuresUsed.map((feature) => /* @__PURE__ */ jsxs(Badge, {
|
|
2544
|
+
variant: "default",
|
|
2545
|
+
className: "border-green-500/30 bg-green-500/20 text-green-400",
|
|
2546
|
+
children: ["✓ ", formatFeatureName(feature)]
|
|
2547
|
+
}, feature))
|
|
2548
|
+
})] }),
|
|
2549
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2550
|
+
className: "text-muted-foreground text-xs",
|
|
2551
|
+
children: ["Session started: ", sessionStart.toLocaleString()]
|
|
2552
|
+
})
|
|
2553
|
+
]
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
/**
|
|
2557
|
+
* Format feature name for display
|
|
2558
|
+
*/
|
|
2559
|
+
function formatFeatureName(feature) {
|
|
2560
|
+
return feature.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
2561
|
+
}
|
|
2562
|
+
/**
|
|
2563
|
+
* Format template ID for display
|
|
2564
|
+
*/
|
|
2565
|
+
function formatTemplateId(id) {
|
|
2566
|
+
return id.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
//#endregion
|
|
2570
|
+
//#region src/hooks/useWorkflowComposer.ts
|
|
2571
|
+
/**
|
|
2572
|
+
* Hook for composing workflows in the sandbox.
|
|
2573
|
+
* Provides workflow selection, extension management, and code generation.
|
|
2574
|
+
*/
|
|
2575
|
+
function useWorkflowComposer(templateId) {
|
|
2576
|
+
const [selectedWorkflow, setSelectedWorkflow] = useState(null);
|
|
2577
|
+
const [extensions, setExtensions] = useState([]);
|
|
2578
|
+
const [loading, _setLoading] = useState(false);
|
|
2579
|
+
const [error, _setError] = useState(null);
|
|
2580
|
+
const baseWorkflows = useMemo(() => getTemplateWorkflows(templateId), [templateId]);
|
|
2581
|
+
useEffect(() => {
|
|
2582
|
+
const firstWorkflow = baseWorkflows[0];
|
|
2583
|
+
if (baseWorkflows.length > 0 && !selectedWorkflow && firstWorkflow) setSelectedWorkflow(firstWorkflow.meta.key);
|
|
2584
|
+
}, [baseWorkflows, selectedWorkflow]);
|
|
2585
|
+
const currentBase = useMemo(() => {
|
|
2586
|
+
return baseWorkflows.find((w) => w.meta.key === selectedWorkflow) ?? null;
|
|
2587
|
+
}, [baseWorkflows, selectedWorkflow]);
|
|
2588
|
+
/**
|
|
2589
|
+
* Compose workflow with extensions
|
|
2590
|
+
*/
|
|
2591
|
+
const compose = useCallback((scope) => {
|
|
2592
|
+
if (!currentBase) return null;
|
|
2593
|
+
const applicableExtensions = extensions.filter((ext) => ext.workflow === currentBase.meta.key).filter((ext) => matchesScope(ext, scope)).sort((a, b) => (a.priority ?? 0) - (b.priority ?? 0));
|
|
2594
|
+
if (applicableExtensions.length === 0) return currentBase;
|
|
2595
|
+
let composedWorkflow = {
|
|
2596
|
+
...currentBase,
|
|
2597
|
+
steps: [...currentBase.steps]
|
|
2598
|
+
};
|
|
2599
|
+
for (const extension of applicableExtensions) composedWorkflow = applyExtension(composedWorkflow, extension);
|
|
2600
|
+
return composedWorkflow;
|
|
2601
|
+
}, [currentBase, extensions]);
|
|
2602
|
+
const workflow = useMemo(() => compose(), [compose]);
|
|
2603
|
+
return {
|
|
2604
|
+
workflow,
|
|
2605
|
+
baseWorkflows,
|
|
2606
|
+
extensions,
|
|
2607
|
+
selectWorkflow: useCallback((workflowName) => {
|
|
2608
|
+
setSelectedWorkflow(workflowName);
|
|
2609
|
+
}, []),
|
|
2610
|
+
addExtension: useCallback((extension) => {
|
|
2611
|
+
setExtensions((prev) => [...prev, extension]);
|
|
2612
|
+
}, []),
|
|
2613
|
+
removeExtension: useCallback((workflowName, index) => {
|
|
2614
|
+
setExtensions((prev) => {
|
|
2615
|
+
const forWorkflow = prev.filter((e) => e.workflow === workflowName);
|
|
2616
|
+
const others = prev.filter((e) => e.workflow !== workflowName);
|
|
2617
|
+
forWorkflow.splice(index, 1);
|
|
2618
|
+
return [...others, ...forWorkflow];
|
|
2619
|
+
});
|
|
2620
|
+
}, []),
|
|
2621
|
+
compose,
|
|
2622
|
+
generateSpecCode: useCallback(() => {
|
|
2623
|
+
const composed = workflow;
|
|
2624
|
+
if (!composed) return "// No workflow selected";
|
|
2625
|
+
const stepsCode = composed.steps.map((step) => ` {
|
|
2626
|
+
id: '${step.id}',
|
|
2627
|
+
name: '${step.name}',
|
|
2628
|
+
type: '${step.type}',${step.description ? `\n description: '${step.description}',` : ""}${step.next ? `\n next: ${JSON.stringify(step.next)},` : ""}${step.condition ? `\n condition: '${step.condition}',` : ""}${step.timeout ? `\n timeout: ${step.timeout},` : ""}${step.retries ? `\n retries: ${step.retries},` : ""}${step.onError ? `\n onError: '${step.onError}',` : ""}
|
|
2629
|
+
}`).join(",\n");
|
|
2630
|
+
const extensionsCode = extensions.length > 0 ? `
|
|
2631
|
+
|
|
2632
|
+
// Extensions applied:
|
|
2633
|
+
${extensions.map((ext) => `// - ${ext.workflow} (priority: ${ext.priority ?? 0})${ext.customSteps?.length ? ` +${ext.customSteps.length} steps` : ""}${ext.hiddenSteps?.length ? ` -${ext.hiddenSteps.length} hidden` : ""}`).join("\n")}` : "";
|
|
2634
|
+
return `// Workflow Spec: ${composed.meta.key} v${composed.meta.version}
|
|
2635
|
+
// Generated from ${templateId} template
|
|
2636
|
+
${extensionsCode}
|
|
2637
|
+
|
|
2638
|
+
import { workflowSpec } from '@contractspec/lib.contracts/workflow';
|
|
2639
|
+
|
|
2640
|
+
export const ${toCamelCase(composed.meta.key)}Workflow = workflowSpec({
|
|
2641
|
+
meta: {
|
|
2642
|
+
key: '${composed.meta.key}',
|
|
2643
|
+
version: ${composed.meta.version},${composed.meta.description ? `\n description: '${composed.meta.description}',` : ""}
|
|
2644
|
+
},
|
|
2645
|
+
start: '${composed.start}',
|
|
2646
|
+
steps: [
|
|
2647
|
+
${stepsCode}
|
|
2648
|
+
],${composed.context ? `\n context: ${JSON.stringify(composed.context, null, 2)},` : ""}
|
|
2649
|
+
});
|
|
2650
|
+
`;
|
|
2651
|
+
}, [
|
|
2652
|
+
workflow,
|
|
2653
|
+
extensions,
|
|
2654
|
+
templateId
|
|
2655
|
+
]),
|
|
2656
|
+
loading,
|
|
2657
|
+
error
|
|
2658
|
+
};
|
|
2659
|
+
}
|
|
2660
|
+
/**
|
|
2661
|
+
* Check if extension matches the scope
|
|
2662
|
+
*/
|
|
2663
|
+
function matchesScope(extension, scope) {
|
|
2664
|
+
if (!scope) return true;
|
|
2665
|
+
if (extension.tenantId && extension.tenantId !== scope.tenantId) return false;
|
|
2666
|
+
if (extension.role && extension.role !== scope.role) return false;
|
|
2667
|
+
if (extension.device && extension.device !== scope.device) return false;
|
|
2668
|
+
return true;
|
|
2669
|
+
}
|
|
2670
|
+
/**
|
|
2671
|
+
* Apply extension to workflow
|
|
2672
|
+
*/
|
|
2673
|
+
function applyExtension(workflow, extension) {
|
|
2674
|
+
let steps = [...workflow.steps];
|
|
2675
|
+
if (extension.hiddenSteps) steps = steps.filter((s) => !extension.hiddenSteps?.includes(s.id));
|
|
2676
|
+
if (extension.customSteps) for (const injection of extension.customSteps) {
|
|
2677
|
+
const stepToInject = {
|
|
2678
|
+
...injection.inject,
|
|
2679
|
+
id: injection.id ?? injection.inject.id
|
|
2680
|
+
};
|
|
2681
|
+
if (injection.after) {
|
|
2682
|
+
const afterIndex = steps.findIndex((s) => s.id === injection.after);
|
|
2683
|
+
if (afterIndex !== -1) steps.splice(afterIndex + 1, 0, stepToInject);
|
|
2684
|
+
} else if (injection.before) {
|
|
2685
|
+
const beforeIndex = steps.findIndex((s) => s.id === injection.before);
|
|
2686
|
+
if (beforeIndex !== -1) steps.splice(beforeIndex, 0, stepToInject);
|
|
2687
|
+
} else steps.push(stepToInject);
|
|
2688
|
+
}
|
|
2689
|
+
return {
|
|
2690
|
+
...workflow,
|
|
2691
|
+
steps
|
|
2692
|
+
};
|
|
2693
|
+
}
|
|
2694
|
+
/**
|
|
2695
|
+
* Convert string to camelCase
|
|
2696
|
+
*/
|
|
2697
|
+
function toCamelCase(str) {
|
|
2698
|
+
return str.replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase()).replace(/^./, (c) => c.toLowerCase());
|
|
2699
|
+
}
|
|
2700
|
+
/**
|
|
2701
|
+
* Get template-specific workflows
|
|
2702
|
+
*/
|
|
2703
|
+
function getTemplateWorkflows(templateId) {
|
|
2704
|
+
return {
|
|
2705
|
+
"crm-pipeline": [{
|
|
2706
|
+
meta: {
|
|
2707
|
+
key: "deal.qualification",
|
|
2708
|
+
version: "1.0.0",
|
|
2709
|
+
description: "Deal qualification workflow"
|
|
2710
|
+
},
|
|
2711
|
+
start: "lead-received",
|
|
2712
|
+
steps: [
|
|
2713
|
+
{
|
|
2714
|
+
id: "lead-received",
|
|
2715
|
+
name: "Lead Received",
|
|
2716
|
+
type: "action",
|
|
2717
|
+
description: "New lead enters the pipeline",
|
|
2718
|
+
next: "qualify-lead"
|
|
2719
|
+
},
|
|
2720
|
+
{
|
|
2721
|
+
id: "qualify-lead",
|
|
2722
|
+
name: "Qualify Lead",
|
|
2723
|
+
type: "decision",
|
|
2724
|
+
description: "Determine if lead meets qualification criteria",
|
|
2725
|
+
next: ["qualified", "disqualified"],
|
|
2726
|
+
condition: "lead.score >= threshold"
|
|
2727
|
+
},
|
|
2728
|
+
{
|
|
2729
|
+
id: "qualified",
|
|
2730
|
+
name: "Lead Qualified",
|
|
2731
|
+
type: "action",
|
|
2732
|
+
next: "assign-rep"
|
|
2733
|
+
},
|
|
2734
|
+
{
|
|
2735
|
+
id: "disqualified",
|
|
2736
|
+
name: "Lead Disqualified",
|
|
2737
|
+
type: "end"
|
|
2738
|
+
},
|
|
2739
|
+
{
|
|
2740
|
+
id: "assign-rep",
|
|
2741
|
+
name: "Assign Sales Rep",
|
|
2742
|
+
type: "action",
|
|
2743
|
+
next: "complete"
|
|
2744
|
+
},
|
|
2745
|
+
{
|
|
2746
|
+
id: "complete",
|
|
2747
|
+
name: "Workflow Complete",
|
|
2748
|
+
type: "end"
|
|
2749
|
+
}
|
|
2750
|
+
]
|
|
2751
|
+
}, {
|
|
2752
|
+
meta: {
|
|
2753
|
+
key: "deal.closing",
|
|
2754
|
+
version: "1.0.0",
|
|
2755
|
+
description: "Deal closing workflow"
|
|
2756
|
+
},
|
|
2757
|
+
start: "proposal-sent",
|
|
2758
|
+
steps: [
|
|
2759
|
+
{
|
|
2760
|
+
id: "proposal-sent",
|
|
2761
|
+
name: "Proposal Sent",
|
|
2762
|
+
type: "action",
|
|
2763
|
+
next: "wait-response"
|
|
2764
|
+
},
|
|
2765
|
+
{
|
|
2766
|
+
id: "wait-response",
|
|
2767
|
+
name: "Wait for Response",
|
|
2768
|
+
type: "wait",
|
|
2769
|
+
timeout: 6048e5,
|
|
2770
|
+
next: "negotiate",
|
|
2771
|
+
onError: "follow-up"
|
|
2772
|
+
},
|
|
2773
|
+
{
|
|
2774
|
+
id: "follow-up",
|
|
2775
|
+
name: "Follow Up",
|
|
2776
|
+
type: "action",
|
|
2777
|
+
next: "wait-response",
|
|
2778
|
+
retries: 3
|
|
2779
|
+
},
|
|
2780
|
+
{
|
|
2781
|
+
id: "negotiate",
|
|
2782
|
+
name: "Negotiation",
|
|
2783
|
+
type: "action",
|
|
2784
|
+
next: "finalize"
|
|
2785
|
+
},
|
|
2786
|
+
{
|
|
2787
|
+
id: "finalize",
|
|
2788
|
+
name: "Finalize Deal",
|
|
2789
|
+
type: "decision",
|
|
2790
|
+
next: ["won", "lost"],
|
|
2791
|
+
condition: "deal.accepted"
|
|
2792
|
+
},
|
|
2793
|
+
{
|
|
2794
|
+
id: "won",
|
|
2795
|
+
name: "Deal Won",
|
|
2796
|
+
type: "end"
|
|
2797
|
+
},
|
|
2798
|
+
{
|
|
2799
|
+
id: "lost",
|
|
2800
|
+
name: "Deal Lost",
|
|
2801
|
+
type: "end"
|
|
2802
|
+
}
|
|
2803
|
+
]
|
|
2804
|
+
}],
|
|
2805
|
+
"saas-boilerplate": [{
|
|
2806
|
+
meta: {
|
|
2807
|
+
key: "user.onboarding",
|
|
2808
|
+
version: "1.0.0",
|
|
2809
|
+
description: "User onboarding workflow"
|
|
2810
|
+
},
|
|
2811
|
+
start: "signup",
|
|
2812
|
+
steps: [
|
|
2813
|
+
{
|
|
2814
|
+
id: "signup",
|
|
2815
|
+
name: "User Signup",
|
|
2816
|
+
type: "action",
|
|
2817
|
+
next: "verify-email"
|
|
2818
|
+
},
|
|
2819
|
+
{
|
|
2820
|
+
id: "verify-email",
|
|
2821
|
+
name: "Verify Email",
|
|
2822
|
+
type: "wait",
|
|
2823
|
+
timeout: 864e5,
|
|
2824
|
+
next: "profile-setup",
|
|
2825
|
+
onError: "resend-verification"
|
|
2826
|
+
},
|
|
2827
|
+
{
|
|
2828
|
+
id: "resend-verification",
|
|
2829
|
+
name: "Resend Verification",
|
|
2830
|
+
type: "action",
|
|
2831
|
+
next: "verify-email",
|
|
2832
|
+
retries: 2
|
|
2833
|
+
},
|
|
2834
|
+
{
|
|
2835
|
+
id: "profile-setup",
|
|
2836
|
+
name: "Setup Profile",
|
|
2837
|
+
type: "action",
|
|
2838
|
+
next: "onboarding-tour"
|
|
2839
|
+
},
|
|
2840
|
+
{
|
|
2841
|
+
id: "onboarding-tour",
|
|
2842
|
+
name: "Onboarding Tour",
|
|
2843
|
+
type: "action",
|
|
2844
|
+
next: "complete"
|
|
2845
|
+
},
|
|
2846
|
+
{
|
|
2847
|
+
id: "complete",
|
|
2848
|
+
name: "Onboarding Complete",
|
|
2849
|
+
type: "end"
|
|
2850
|
+
}
|
|
2851
|
+
]
|
|
2852
|
+
}],
|
|
2853
|
+
"agent-console": [{
|
|
2854
|
+
meta: {
|
|
2855
|
+
key: "agent.execution",
|
|
2856
|
+
version: "1.0.0",
|
|
2857
|
+
description: "Agent execution workflow"
|
|
2858
|
+
},
|
|
2859
|
+
start: "receive-task",
|
|
2860
|
+
steps: [
|
|
2861
|
+
{
|
|
2862
|
+
id: "receive-task",
|
|
2863
|
+
name: "Receive Task",
|
|
2864
|
+
type: "action",
|
|
2865
|
+
next: "plan-execution"
|
|
2866
|
+
},
|
|
2867
|
+
{
|
|
2868
|
+
id: "plan-execution",
|
|
2869
|
+
name: "Plan Execution",
|
|
2870
|
+
type: "action",
|
|
2871
|
+
next: "execute-steps"
|
|
2872
|
+
},
|
|
2873
|
+
{
|
|
2874
|
+
id: "execute-steps",
|
|
2875
|
+
name: "Execute Steps",
|
|
2876
|
+
type: "parallel",
|
|
2877
|
+
next: [
|
|
2878
|
+
"tool-call",
|
|
2879
|
+
"observe",
|
|
2880
|
+
"reason"
|
|
2881
|
+
]
|
|
2882
|
+
},
|
|
2883
|
+
{
|
|
2884
|
+
id: "tool-call",
|
|
2885
|
+
name: "Tool Call",
|
|
2886
|
+
type: "action",
|
|
2887
|
+
next: "evaluate"
|
|
2888
|
+
},
|
|
2889
|
+
{
|
|
2890
|
+
id: "observe",
|
|
2891
|
+
name: "Observe",
|
|
2892
|
+
type: "action",
|
|
2893
|
+
next: "evaluate"
|
|
2894
|
+
},
|
|
2895
|
+
{
|
|
2896
|
+
id: "reason",
|
|
2897
|
+
name: "Reason",
|
|
2898
|
+
type: "action",
|
|
2899
|
+
next: "evaluate"
|
|
2900
|
+
},
|
|
2901
|
+
{
|
|
2902
|
+
id: "evaluate",
|
|
2903
|
+
name: "Evaluate Result",
|
|
2904
|
+
type: "decision",
|
|
2905
|
+
condition: "task.isComplete",
|
|
2906
|
+
next: ["complete", "execute-steps"]
|
|
2907
|
+
},
|
|
2908
|
+
{
|
|
2909
|
+
id: "complete",
|
|
2910
|
+
name: "Task Complete",
|
|
2911
|
+
type: "end"
|
|
2912
|
+
}
|
|
2913
|
+
]
|
|
2914
|
+
}],
|
|
2915
|
+
"todos-app": [{
|
|
2916
|
+
meta: {
|
|
2917
|
+
key: "task.lifecycle",
|
|
2918
|
+
version: "1.0.0",
|
|
2919
|
+
description: "Task lifecycle workflow"
|
|
2920
|
+
},
|
|
2921
|
+
start: "created",
|
|
2922
|
+
steps: [
|
|
2923
|
+
{
|
|
2924
|
+
id: "created",
|
|
2925
|
+
name: "Task Created",
|
|
2926
|
+
type: "action",
|
|
2927
|
+
next: "in-progress"
|
|
2928
|
+
},
|
|
2929
|
+
{
|
|
2930
|
+
id: "in-progress",
|
|
2931
|
+
name: "In Progress",
|
|
2932
|
+
type: "action",
|
|
2933
|
+
next: "review"
|
|
2934
|
+
},
|
|
2935
|
+
{
|
|
2936
|
+
id: "review",
|
|
2937
|
+
name: "Review",
|
|
2938
|
+
type: "decision",
|
|
2939
|
+
condition: "task.approved",
|
|
2940
|
+
next: ["done", "in-progress"]
|
|
2941
|
+
},
|
|
2942
|
+
{
|
|
2943
|
+
id: "done",
|
|
2944
|
+
name: "Done",
|
|
2945
|
+
type: "end"
|
|
2946
|
+
}
|
|
2947
|
+
]
|
|
2948
|
+
}],
|
|
2949
|
+
"messaging-app": [{
|
|
2950
|
+
meta: {
|
|
2951
|
+
key: "message.delivery",
|
|
2952
|
+
version: "1.0.0",
|
|
2953
|
+
description: "Message delivery workflow"
|
|
2954
|
+
},
|
|
2955
|
+
start: "compose",
|
|
2956
|
+
steps: [
|
|
2957
|
+
{
|
|
2958
|
+
id: "compose",
|
|
2959
|
+
name: "Compose Message",
|
|
2960
|
+
type: "action",
|
|
2961
|
+
next: "send"
|
|
2962
|
+
},
|
|
2963
|
+
{
|
|
2964
|
+
id: "send",
|
|
2965
|
+
name: "Send Message",
|
|
2966
|
+
type: "action",
|
|
2967
|
+
next: "deliver"
|
|
2968
|
+
},
|
|
2969
|
+
{
|
|
2970
|
+
id: "deliver",
|
|
2971
|
+
name: "Deliver",
|
|
2972
|
+
type: "decision",
|
|
2973
|
+
condition: "recipient.online",
|
|
2974
|
+
next: ["delivered", "queue"]
|
|
2975
|
+
},
|
|
2976
|
+
{
|
|
2977
|
+
id: "queue",
|
|
2978
|
+
name: "Queue for Delivery",
|
|
2979
|
+
type: "wait",
|
|
2980
|
+
next: "deliver"
|
|
2981
|
+
},
|
|
2982
|
+
{
|
|
2983
|
+
id: "delivered",
|
|
2984
|
+
name: "Message Delivered",
|
|
2985
|
+
type: "action",
|
|
2986
|
+
next: "read"
|
|
2987
|
+
},
|
|
2988
|
+
{
|
|
2989
|
+
id: "read",
|
|
2990
|
+
name: "Message Read",
|
|
2991
|
+
type: "end"
|
|
2992
|
+
}
|
|
2993
|
+
]
|
|
2994
|
+
}],
|
|
2995
|
+
"recipe-app-i18n": [{
|
|
2996
|
+
meta: {
|
|
2997
|
+
key: "recipe.creation",
|
|
2998
|
+
version: "1.0.0",
|
|
2999
|
+
description: "Recipe creation workflow"
|
|
3000
|
+
},
|
|
3001
|
+
start: "draft",
|
|
3002
|
+
steps: [
|
|
3003
|
+
{
|
|
3004
|
+
id: "draft",
|
|
3005
|
+
name: "Draft Recipe",
|
|
3006
|
+
type: "action",
|
|
3007
|
+
next: "add-ingredients"
|
|
3008
|
+
},
|
|
3009
|
+
{
|
|
3010
|
+
id: "add-ingredients",
|
|
3011
|
+
name: "Add Ingredients",
|
|
3012
|
+
type: "action",
|
|
3013
|
+
next: "add-steps"
|
|
3014
|
+
},
|
|
3015
|
+
{
|
|
3016
|
+
id: "add-steps",
|
|
3017
|
+
name: "Add Steps",
|
|
3018
|
+
type: "action",
|
|
3019
|
+
next: "review"
|
|
3020
|
+
},
|
|
3021
|
+
{
|
|
3022
|
+
id: "review",
|
|
3023
|
+
name: "Review Recipe",
|
|
3024
|
+
type: "decision",
|
|
3025
|
+
condition: "recipe.isComplete",
|
|
3026
|
+
next: ["publish", "draft"]
|
|
3027
|
+
},
|
|
3028
|
+
{
|
|
3029
|
+
id: "publish",
|
|
3030
|
+
name: "Publish Recipe",
|
|
3031
|
+
type: "end"
|
|
3032
|
+
}
|
|
3033
|
+
]
|
|
3034
|
+
}]
|
|
3035
|
+
}[templateId] ?? [];
|
|
3036
|
+
}
|
|
3037
|
+
|
|
3038
|
+
//#endregion
|
|
3039
|
+
//#region src/hooks/useRegistryTemplates.ts
|
|
3040
|
+
function useRegistryTemplates() {
|
|
3041
|
+
return useQuery({
|
|
3042
|
+
queryKey: ["registryTemplates"],
|
|
3043
|
+
queryFn: async () => {
|
|
3044
|
+
const registryUrl = process.env.NEXT_PUBLIC_CONTRACTSPEC_REGISTRY_URL ?? "";
|
|
3045
|
+
if (!registryUrl) return [];
|
|
3046
|
+
const res = await fetch(`${registryUrl.replace(/\/$/, "")}/r/contractspec.json`, {
|
|
3047
|
+
method: "GET",
|
|
3048
|
+
headers: { Accept: "application/json" }
|
|
3049
|
+
});
|
|
3050
|
+
if (!res.ok) return [];
|
|
3051
|
+
return ((await res.json()).items ?? []).filter((i) => i.type === "contractspec:template").map((i) => ({
|
|
3052
|
+
id: i.name,
|
|
3053
|
+
name: i.title ?? i.name,
|
|
3054
|
+
description: i.description,
|
|
3055
|
+
tags: i.meta?.tags ?? [],
|
|
3056
|
+
source: "registry",
|
|
3057
|
+
registryUrl
|
|
3058
|
+
}));
|
|
3059
|
+
}
|
|
3060
|
+
});
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
//#endregion
|
|
3064
|
+
//#region src/utils/fetchPresentationData.ts
|
|
3065
|
+
/**
|
|
3066
|
+
* @deprecated Use fetchData from TemplateRuntimeContext instead.
|
|
3067
|
+
*/
|
|
3068
|
+
/**
|
|
3069
|
+
* @deprecated Use fetchData from TemplateRuntimeContext instead.
|
|
3070
|
+
*/
|
|
3071
|
+
async function fetchPresentationData(_presentationName, _templateId) {
|
|
3072
|
+
throw new Error("fetchPresentationData is deprecated. Use fetchData from TemplateRuntimeContext.");
|
|
3073
|
+
}
|
|
3074
|
+
/**
|
|
3075
|
+
* @deprecated
|
|
3076
|
+
*/
|
|
3077
|
+
function hasPresentationDataFetcher(_presentationName) {
|
|
3078
|
+
return false;
|
|
3079
|
+
}
|
|
3080
|
+
/**
|
|
3081
|
+
* @deprecated
|
|
3082
|
+
*/
|
|
3083
|
+
function getRegisteredPresentationFetchers() {
|
|
3084
|
+
return [];
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
//#endregion
|
|
3088
|
+
//#region src/lib/component-registry.tsx
|
|
3089
|
+
var TemplateComponentRegistry = class {
|
|
3090
|
+
components = /* @__PURE__ */ new Map();
|
|
3091
|
+
listeners = /* @__PURE__ */ new Set();
|
|
3092
|
+
register(templateId, registration) {
|
|
3093
|
+
this.components.set(templateId, registration);
|
|
3094
|
+
this.listeners.forEach((l) => l(templateId));
|
|
3095
|
+
}
|
|
3096
|
+
get(templateId) {
|
|
3097
|
+
return this.components.get(templateId);
|
|
3098
|
+
}
|
|
3099
|
+
subscribe(listener) {
|
|
3100
|
+
this.listeners.add(listener);
|
|
3101
|
+
return () => {
|
|
3102
|
+
this.listeners.delete(listener);
|
|
3103
|
+
};
|
|
3104
|
+
}
|
|
3105
|
+
};
|
|
3106
|
+
const templateComponentRegistry = new TemplateComponentRegistry();
|
|
3107
|
+
function registerTemplateComponents(templateId, components) {
|
|
3108
|
+
templateComponentRegistry.register(templateId, components);
|
|
3109
|
+
}
|
|
3110
|
+
function useTemplateComponents(templateId) {
|
|
3111
|
+
const [components, setComponents] = useState(() => templateComponentRegistry.get(templateId));
|
|
3112
|
+
useEffect(() => {
|
|
3113
|
+
return templateComponentRegistry.subscribe((updatedId) => {
|
|
3114
|
+
if (updatedId === templateId) setComponents(templateComponentRegistry.get(templateId));
|
|
3115
|
+
});
|
|
3116
|
+
}, [templateId]);
|
|
3117
|
+
return components;
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
//#endregion
|
|
3121
|
+
export { EvolutionDashboard, EvolutionSidebar, LocalDataIndicator, MarkdownView, OverlayContextProvider, PersonalizationInsights, SaveToStudioButton, SpecEditorPanel, TemplateComponentRegistry, TemplateRuntimeContext, TemplateShell, fetchPresentationData, generateSpecFromTemplate, getRegisteredPresentationFetchers, hasPresentationDataFetcher, registerTemplateComponents, templateComponentRegistry, useBehaviorTracking, useEvolution, useIsInOverlayContext, useOverlayContext, useRegistryTemplates, useSpecContent, useTemplateComponents, useTemplateRuntime, useWorkflowComposer };
|