begeniux 0.1.0 → 0.2.0
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/README.md +278 -189
- package/dist/copilotkit.cjs +138 -0
- package/dist/copilotkit.cjs.map +1 -0
- package/dist/copilotkit.d.cts +20 -0
- package/dist/copilotkit.d.ts +20 -0
- package/dist/copilotkit.js +102 -0
- package/dist/copilotkit.js.map +1 -0
- package/dist/index.cjs +656 -320
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +266 -47
- package/dist/index.d.ts +266 -47
- package/dist/index.js +652 -315
- package/dist/index.js.map +1 -1
- package/package.json +20 -5
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/adapters/copilotkit.ts
|
|
31
|
+
var copilotkit_exports = {};
|
|
32
|
+
__export(copilotkit_exports, {
|
|
33
|
+
CopilotKitAdapter: () => CopilotKitAdapter,
|
|
34
|
+
useCopilotKitAdapter: () => useCopilotKitAdapter
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(copilotkit_exports);
|
|
37
|
+
var import_zod = require("zod");
|
|
38
|
+
var import_v2 = require("@copilotkit/react-core/v2");
|
|
39
|
+
|
|
40
|
+
// src/BeGenProvider.tsx
|
|
41
|
+
var React = __toESM(require("react"), 1);
|
|
42
|
+
|
|
43
|
+
// src/useBehaviorTracker.ts
|
|
44
|
+
var import_react = require("react");
|
|
45
|
+
|
|
46
|
+
// src/BeGenProvider.tsx
|
|
47
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
48
|
+
var BeGenContext = React.createContext(null);
|
|
49
|
+
function useBeGenContext() {
|
|
50
|
+
const ctx = React.useContext(BeGenContext);
|
|
51
|
+
if (!ctx) {
|
|
52
|
+
throw new Error("useBeGenContext must be used inside <BeGenProvider>");
|
|
53
|
+
}
|
|
54
|
+
return ctx;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/adapters/copilotkit.ts
|
|
58
|
+
var adaptationSchema = import_zod.z.discriminatedUnion("kind", [
|
|
59
|
+
import_zod.z.object({
|
|
60
|
+
kind: import_zod.z.literal("set-css-var"),
|
|
61
|
+
selector: import_zod.z.string(),
|
|
62
|
+
name: import_zod.z.string(),
|
|
63
|
+
value: import_zod.z.string()
|
|
64
|
+
}),
|
|
65
|
+
import_zod.z.object({
|
|
66
|
+
kind: import_zod.z.literal("add-class"),
|
|
67
|
+
selector: import_zod.z.string(),
|
|
68
|
+
className: import_zod.z.string()
|
|
69
|
+
}),
|
|
70
|
+
import_zod.z.object({
|
|
71
|
+
kind: import_zod.z.literal("remove-class"),
|
|
72
|
+
selector: import_zod.z.string(),
|
|
73
|
+
className: import_zod.z.string()
|
|
74
|
+
}),
|
|
75
|
+
import_zod.z.object({
|
|
76
|
+
kind: import_zod.z.literal("set-style"),
|
|
77
|
+
selector: import_zod.z.string(),
|
|
78
|
+
property: import_zod.z.string(),
|
|
79
|
+
value: import_zod.z.string()
|
|
80
|
+
}),
|
|
81
|
+
import_zod.z.object({
|
|
82
|
+
kind: import_zod.z.literal("set-attribute"),
|
|
83
|
+
selector: import_zod.z.string(),
|
|
84
|
+
name: import_zod.z.string(),
|
|
85
|
+
value: import_zod.z.string()
|
|
86
|
+
}),
|
|
87
|
+
import_zod.z.object({
|
|
88
|
+
kind: import_zod.z.literal("set-aria-label"),
|
|
89
|
+
selector: import_zod.z.string(),
|
|
90
|
+
value: import_zod.z.string()
|
|
91
|
+
})
|
|
92
|
+
]);
|
|
93
|
+
var adaptationPlanSchema = import_zod.z.object({
|
|
94
|
+
adaptations: import_zod.z.array(adaptationSchema),
|
|
95
|
+
confidence: import_zod.z.number().min(0).max(1),
|
|
96
|
+
reasoning: import_zod.z.string(),
|
|
97
|
+
meta: import_zod.z.record(import_zod.z.unknown()).optional()
|
|
98
|
+
});
|
|
99
|
+
var DEFAULT_DESCRIPTION = `Apply CSS-only UI adaptations to the live page. Use this whenever you decide that the user would benefit from a UI change based on their behavior, current page state, and the design system the host app has declared.
|
|
100
|
+
|
|
101
|
+
Each adaptation is one of:
|
|
102
|
+
- set-css-var: change a CSS custom property on a target element
|
|
103
|
+
- add-class / remove-class: toggle a class on a target element
|
|
104
|
+
- set-style: set a single CSS declaration (no display/position/visibility/float/clear/z-index/overflow \u2014 those are denied for safety)
|
|
105
|
+
- set-attribute / set-aria-label: change an attribute, often for accessibility
|
|
106
|
+
|
|
107
|
+
You should:
|
|
108
|
+
- Use the design system manifest to know which CSS variables and classes are valid
|
|
109
|
+
- Target stable selectors (id, [data-begen-id], [data-testid], semantic tags) \u2014 avoid utility class soup
|
|
110
|
+
- Justify the plan in the reasoning field (one sentence)
|
|
111
|
+
- Set confidence \u2208 [0,1]; below 0.4 means "not sure, prefer no change"`;
|
|
112
|
+
function CopilotKitAdapter(props = {}) {
|
|
113
|
+
useCopilotKitAdapter(props);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
function useCopilotKitAdapter(opts = {}) {
|
|
117
|
+
const { toolName = "apply_adaptations", toolDescription = DEFAULT_DESCRIPTION } = opts;
|
|
118
|
+
const { applyPlan } = useBeGenContext();
|
|
119
|
+
(0, import_v2.useFrontendTool)({
|
|
120
|
+
name: toolName,
|
|
121
|
+
description: toolDescription,
|
|
122
|
+
parameters: adaptationPlanSchema,
|
|
123
|
+
handler: async (plan) => {
|
|
124
|
+
try {
|
|
125
|
+
applyPlan(plan);
|
|
126
|
+
return `Applied ${plan.adaptations.length} adaptation${plan.adaptations.length === 1 ? "" : "s"}. Reasoning: ${plan.reasoning}`;
|
|
127
|
+
} catch (err) {
|
|
128
|
+
return `Adaptation engine error: ${err instanceof Error ? err.message : String(err)}`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
134
|
+
0 && (module.exports = {
|
|
135
|
+
CopilotKitAdapter,
|
|
136
|
+
useCopilotKitAdapter
|
|
137
|
+
});
|
|
138
|
+
//# sourceMappingURL=copilotkit.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapters/copilotkit.ts","../src/BeGenProvider.tsx","../src/useBehaviorTracker.ts"],"sourcesContent":["// CopilotKit v2 adapter for begeniux.\n//\n// Pattern:\n// <CopilotKitProvider runtimeUrl=\"/api/copilotkit\">\n// <BeGenProvider designSystem={...} pageContext={...}>\n// <CopilotKitAdapter /> // mount once, anywhere inside both providers\n// <App />\n// </BeGenProvider>\n// </CopilotKitProvider>\n//\n// What it does:\n// 1. Registers `apply_adaptations` as a frontend tool. The agent calls this\n// tool with an AdaptationPlan; we route the plan into the provider's\n// AdaptationEngine which mutates the live DOM with full reversibility.\n//\n// Why a component, not a function: CopilotKit hooks (`useFrontendTool`) must\n// run inside a React render. Wrapping in `<CopilotKitAdapter />` keeps the\n// integration declarative.\n//\n// Peer-deps: `@copilotkit/react-core@>=2.0.0` and `zod@>=3` must be installed\n// by the consumer. Without them, importing this module throws at runtime; the\n// rest of begeniux (HTTP adapter, manual engine) still works.\n\nimport * as React from \"react\";\nimport { z } from \"zod\";\nimport { useFrontendTool } from \"@copilotkit/react-core/v2\";\nimport { useBeGenContext } from \"../BeGenProvider\";\nimport type { AdaptationPlan } from \"../types\";\n\n// ─── Zod schemas mirroring src/types.ts ─────────────────────────────\n\nconst adaptationSchema = z.discriminatedUnion(\"kind\", [\n z.object({\n kind: z.literal(\"set-css-var\"),\n selector: z.string(),\n name: z.string(),\n value: z.string(),\n }),\n z.object({\n kind: z.literal(\"add-class\"),\n selector: z.string(),\n className: z.string(),\n }),\n z.object({\n kind: z.literal(\"remove-class\"),\n selector: z.string(),\n className: z.string(),\n }),\n z.object({\n kind: z.literal(\"set-style\"),\n selector: z.string(),\n property: z.string(),\n value: z.string(),\n }),\n z.object({\n kind: z.literal(\"set-attribute\"),\n selector: z.string(),\n name: z.string(),\n value: z.string(),\n }),\n z.object({\n kind: z.literal(\"set-aria-label\"),\n selector: z.string(),\n value: z.string(),\n }),\n]);\n\nconst adaptationPlanSchema = z.object({\n adaptations: z.array(adaptationSchema),\n confidence: z.number().min(0).max(1),\n reasoning: z.string(),\n meta: z.record(z.unknown()).optional(),\n});\n\n// ─── Public API ─────────────────────────────────────────────────────\n\nexport type CopilotKitAdapterOpts = {\n /** Override the tool name registered with the agent. Default: \"apply_adaptations\". */\n toolName?: string;\n /** Override the tool description shown to the agent. Default: see source. */\n toolDescription?: string;\n};\n\nconst DEFAULT_DESCRIPTION = `Apply CSS-only UI adaptations to the live page. Use this whenever you decide that the user would benefit from a UI change based on their behavior, current page state, and the design system the host app has declared.\n\nEach adaptation is one of:\n- set-css-var: change a CSS custom property on a target element\n- add-class / remove-class: toggle a class on a target element\n- set-style: set a single CSS declaration (no display/position/visibility/float/clear/z-index/overflow — those are denied for safety)\n- set-attribute / set-aria-label: change an attribute, often for accessibility\n\nYou should:\n- Use the design system manifest to know which CSS variables and classes are valid\n- Target stable selectors (id, [data-begen-id], [data-testid], semantic tags) — avoid utility class soup\n- Justify the plan in the reasoning field (one sentence)\n- Set confidence ∈ [0,1]; below 0.4 means \"not sure, prefer no change\"`;\n\n/**\n * React component that registers begeniux's `apply_adaptations` frontend tool\n * with CopilotKit. Mount once inside <BeGenProvider> and <CopilotKitProvider>.\n */\nexport function CopilotKitAdapter(\n props: CopilotKitAdapterOpts = {},\n): React.ReactElement | null {\n useCopilotKitAdapter(props);\n return null;\n}\n\n/**\n * Hook variant of CopilotKitAdapter — call from your own component if you\n * want to add custom behavior alongside the registration.\n */\nexport function useCopilotKitAdapter(opts: CopilotKitAdapterOpts = {}): void {\n const { toolName = \"apply_adaptations\", toolDescription = DEFAULT_DESCRIPTION } = opts;\n const { applyPlan } = useBeGenContext();\n\n useFrontendTool({\n name: toolName,\n description: toolDescription,\n parameters: adaptationPlanSchema,\n handler: async (plan: AdaptationPlan) => {\n try {\n applyPlan(plan);\n return `Applied ${plan.adaptations.length} adaptation${\n plan.adaptations.length === 1 ? \"\" : \"s\"\n }. Reasoning: ${plan.reasoning}`;\n } catch (err) {\n return `Adaptation engine error: ${\n err instanceof Error ? err.message : String(err)\n }`;\n }\n },\n });\n}\n","import * as React from \"react\";\nimport type {\n Adaptation,\n AdaptationPlan,\n AdaptInput,\n BeGenContextValue,\n BehaviorEvent,\n BehaviorListener,\n BehaviorSummary,\n ClassifyFn,\n DesignSystem,\n ScopeOpts,\n} from \"./types\";\nimport { useBehaviorTracker } from \"./useBehaviorTracker\";\nimport { AdaptationEngine } from \"./AdaptationEngine\";\nimport { snapshotVisibleSelectors } from \"./domSnapshot\";\n\nexport type BeGenProviderProps<TContext = { route: string }> = {\n /** The agent's vocabulary — what CSS variables and classes it may emit. */\n designSystem: DesignSystem;\n /** Per-page state forwarded into every BehaviorSummary. */\n pageContext: TContext;\n /**\n * Single-pass agent. Receives behavior + DOM + design system, returns a plan.\n * If omitted, the provider does NOT call any agent — it only tracks behavior\n * and exposes summaries via context. (Pair with createCopilotKitAdapter or\n * createHttpAdapter for real adaptation.)\n */\n classify?: ClassifyFn<TContext>;\n /** Restrict where the agent's adaptations may target. */\n scope?: ScopeOpts;\n /** Minimum ms between agent calls. Default 5000. */\n rateLimitMs?: number;\n /** Don't trigger an agent call until at least this many events have flushed since the last call. Default 5. */\n triggerEveryEvents?: number;\n /** Tracker idle-flush window. */\n flushAfterMs?: number;\n /** Tracker count-flush. */\n flushEveryEvents?: number;\n /** Tracker ring-buffer cap. */\n bufferSize?: number;\n /** Plug-in event sources. */\n customListeners?: BehaviorListener[];\n /** Element to scope listeners to. Default: document.body. */\n containerRef?: React.RefObject<HTMLElement | null>;\n /** Pre-canned events injected on mount (for demos). */\n seedTrace?: BehaviorEvent[];\n children: React.ReactNode;\n};\n\nconst BeGenContext = React.createContext<BeGenContextValue<any> | null>(null);\n\nexport function useBeGenContext<TContext = { route: string }>(): BeGenContextValue<TContext> {\n const ctx = React.useContext(BeGenContext);\n if (!ctx) {\n throw new Error(\"useBeGenContext must be used inside <BeGenProvider>\");\n }\n return ctx as BeGenContextValue<TContext>;\n}\n\nexport function BeGenProvider<TContext = { route: string }>(\n props: BeGenProviderProps<TContext>,\n): React.ReactElement {\n const {\n designSystem,\n pageContext,\n classify,\n scope,\n rateLimitMs = 5000,\n triggerEveryEvents = 5,\n flushAfterMs = 5000,\n flushEveryEvents = 10,\n bufferSize = 50,\n customListeners,\n containerRef,\n seedTrace,\n children,\n } = props;\n\n // Engine is created lazily once document.body exists (post-mount).\n const engineRef = React.useRef<AdaptationEngine | null>(null);\n\n const [summary, setSummary] = React.useState<BehaviorSummary<TContext> | null>(null);\n const [lastPlan, setLastPlan] = React.useState<AdaptationPlan | null>(null);\n const [appliedAdaptations, setAppliedAdaptations] = React.useState<\n ReadonlyArray<Adaptation>\n >([]);\n\n const lastClassifyAtRef = React.useRef(0);\n const inFlightRef = React.useRef(false);\n const eventsSinceClassifyRef = React.useRef(0);\n const lastPlanHashRef = React.useRef<string | null>(null);\n\n const classifyRef = React.useRef(classify);\n React.useEffect(() => {\n classifyRef.current = classify;\n }, [classify]);\n\n const designSystemRef = React.useRef(designSystem);\n React.useEffect(() => {\n designSystemRef.current = designSystem;\n }, [designSystem]);\n\n const pageContextRef = React.useRef(pageContext);\n React.useEffect(() => {\n pageContextRef.current = pageContext;\n }, [pageContext]);\n\n const scopeRef = React.useRef(scope);\n React.useEffect(() => {\n scopeRef.current = scope;\n }, [scope]);\n\n // Mount the engine once we're in the browser.\n React.useEffect(() => {\n if (typeof document === \"undefined\") return;\n const root = containerRef?.current ?? document.body;\n if (!root) return;\n engineRef.current = new AdaptationEngine({\n root,\n scope: scopeRef.current,\n onEvent: () => {\n if (engineRef.current) {\n setAppliedAdaptations(engineRef.current.getApplied().slice());\n }\n },\n });\n return () => {\n engineRef.current?.revertAll();\n engineRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const handleFlush = React.useCallback(\n async (s: BehaviorSummary<TContext>) => {\n setSummary(s);\n eventsSinceClassifyRef.current += 1;\n\n const fn = classifyRef.current;\n if (!fn) return;\n if (inFlightRef.current) return;\n const now = Date.now();\n if (now - lastClassifyAtRef.current < rateLimitMs) return;\n if (eventsSinceClassifyRef.current < triggerEveryEvents) return;\n\n lastClassifyAtRef.current = now;\n eventsSinceClassifyRef.current = 0;\n inFlightRef.current = true;\n\n try {\n const root =\n (containerRef?.current ?? (typeof document !== \"undefined\" ? document.body : null)) ??\n null;\n const visibleSelectors = root\n ? snapshotVisibleSelectors(root, { scope: scopeRef.current })\n : [];\n\n const route =\n (s.page_context as unknown as { route?: string })?.route ?? \"/\";\n\n const input: AdaptInput<TContext> = {\n summary: s,\n designSystem: designSystemRef.current,\n dom: { visibleSelectors, route },\n };\n\n const plan = await fn(input);\n if (!plan || !Array.isArray(plan.adaptations)) return;\n\n // Skip identical consecutive plans.\n const hash = hashPlan(plan);\n if (hash === lastPlanHashRef.current) return;\n lastPlanHashRef.current = hash;\n\n setLastPlan(plan);\n engineRef.current?.apply(plan);\n } catch {\n // Classifier failure: keep current state.\n } finally {\n inFlightRef.current = false;\n }\n },\n [rateLimitMs, triggerEveryEvents, containerRef],\n );\n\n useBehaviorTracker<TContext>({\n containerRef,\n onFlush: handleFlush,\n pageContext,\n seedTrace,\n customListeners,\n flushAfterMs,\n flushEveryEvents,\n bufferSize,\n });\n\n const recentEventsRef = React.useRef<ReadonlyArray<BehaviorEvent>>([]);\n // We don't expose recentEvents directly from tracker into context here to\n // avoid extra renders; consumers needing a live event stream can use\n // useBehaviorTracker directly.\n\n const applyPlan = React.useCallback((plan: AdaptationPlan) => {\n if (!plan || !Array.isArray(plan.adaptations)) return;\n const hash = hashPlan(plan);\n if (hash === lastPlanHashRef.current) return;\n lastPlanHashRef.current = hash;\n setLastPlan(plan);\n engineRef.current?.apply(plan);\n }, []);\n\n const getDesignSystem = React.useCallback(\n () => designSystemRef.current,\n [],\n );\n\n const value = React.useMemo<BeGenContextValue<TContext>>(\n () => ({\n summary,\n lastPlan,\n appliedAdaptations,\n recentEvents: recentEventsRef.current,\n applyPlan,\n getDesignSystem,\n }),\n [summary, lastPlan, appliedAdaptations, applyPlan, getDesignSystem],\n );\n\n return (\n <BeGenContext.Provider value={value as BeGenContextValue<any>}>\n {children}\n </BeGenContext.Provider>\n );\n}\n\nfunction hashPlan(plan: AdaptationPlan): string {\n // Cheap deterministic hash for skip-if-identical comparison.\n // Order-sensitive: agent emitting adaptations in different orders is treated as different intent.\n const parts = plan.adaptations.map((a) => {\n switch (a.kind) {\n case \"set-css-var\":\n return `v|${a.selector}|${a.name}|${a.value}`;\n case \"add-class\":\n return `+|${a.selector}|${a.className}`;\n case \"remove-class\":\n return `-|${a.selector}|${a.className}`;\n case \"set-style\":\n return `s|${a.selector}|${a.property}|${a.value}`;\n case \"set-attribute\":\n return `a|${a.selector}|${a.name}|${a.value}`;\n case \"set-aria-label\":\n return `l|${a.selector}|${a.value}`;\n }\n });\n return parts.join(\"\\n\");\n}\n","import { useEffect, useRef, useState } from \"react\";\nimport type {\n BehaviorEvent,\n BehaviorListener,\n BehaviorSummary,\n} from \"./types\";\n\nexport type UseBehaviorTrackerOpts<TContext = { route: string }> = {\n /** DOM element to scope listeners to. Defaults to document.body. */\n containerRef?: React.RefObject<HTMLElement | null>;\n /** Flush after this many events. Default 10. */\n flushEveryEvents?: number;\n /** Flush after this many ms of idle. Default 5000. */\n flushAfterMs?: number;\n /** Ring buffer cap. Default 50. */\n bufferSize?: number;\n /** Called on every flush with the computed summary. */\n onFlush: (summary: BehaviorSummary<TContext>) => void;\n /** Page context (route, etc.) — included in every summary. */\n pageContext: TContext;\n /** Optional pre-canned events injected on mount. */\n seedTrace?: BehaviorEvent[];\n /** Plug-in event sources beyond the built-ins. */\n customListeners?: BehaviorListener[];\n};\n\nconst RECENT_EVENTS_CAP = 10;\nconst HOVER_MIN_MS = 200;\nconst RAGE_CLICK_WINDOW_MS = 1000;\nconst RAGE_CLICK_THRESHOLD = 3;\nconst INPUT_THROTTLE_MS = 1000;\n\nfunction targetLabel(el: EventTarget | null): string {\n if (!(el instanceof HTMLElement)) return \"unknown\";\n const begenId = el.getAttribute(\"data-begen-id\");\n if (begenId) return `[data-begen-id=\"${begenId}\"]`;\n if (el.id) return `#${el.id}`;\n const testId = el.getAttribute(\"data-testid\");\n if (testId) return `[data-testid=\"${testId}\"]`;\n return el.tagName.toLowerCase();\n}\n\nfunction computeSummary<TContext>(\n buffer: BehaviorEvent[],\n bufferSize: number,\n pageContext: TContext,\n custom: Record<string, number | string | boolean>,\n): BehaviorSummary<TContext> {\n const now = Date.now();\n const windowStart = now - 60_000;\n\n let clicksLastMin = 0;\n let rageClicks = 0;\n let dwellSum = 0;\n let dwellCount = 0;\n let maxScroll = 0;\n let formInteractions = 0;\n let errorsSeen = 0;\n const hoverTargets = new Set<string>();\n let viewportW =\n typeof window !== \"undefined\" ? window.innerWidth || 0 : 0;\n let viewportH =\n typeof window !== \"undefined\" ? window.innerHeight || 0 : 0;\n\n for (const ev of buffer) {\n switch (ev.kind) {\n case \"click\":\n if (ev.t >= windowStart) clicksLastMin += 1;\n break;\n case \"rage-click\":\n rageClicks += 1;\n break;\n case \"dwell\":\n dwellSum += ev.durationMs;\n dwellCount += 1;\n break;\n case \"scroll\":\n if (ev.depth > maxScroll) maxScroll = ev.depth;\n break;\n case \"hover\":\n hoverTargets.add(ev.target);\n break;\n case \"input\":\n case \"submit\":\n formInteractions += 1;\n break;\n case \"error\":\n errorsSeen += 1;\n break;\n case \"viewport-change\":\n viewportW = ev.width;\n viewportH = ev.height;\n break;\n default:\n break;\n }\n }\n\n return {\n clicks_per_min: clicksLastMin,\n rage_clicks: rageClicks,\n avg_dwell_ms: dwellCount === 0 ? 0 : Math.round(dwellSum / dwellCount),\n scroll_depth: Math.max(0, Math.min(1, maxScroll)),\n hover_count: hoverTargets.size,\n form_interactions: formInteractions,\n errors_seen: errorsSeen,\n events_seen: Math.min(buffer.length, bufferSize),\n viewport: { width: viewportW, height: viewportH },\n custom,\n page_context: pageContext,\n };\n}\n\nexport function useBehaviorTracker<TContext = { route: string }>(\n opts: UseBehaviorTrackerOpts<TContext>,\n): { recentEvents: BehaviorEvent[] } {\n const {\n containerRef,\n flushEveryEvents = 10,\n flushAfterMs = 5000,\n bufferSize = 50,\n onFlush,\n pageContext,\n seedTrace,\n customListeners,\n } = opts;\n\n const bufferRef = useRef<BehaviorEvent[]>([]);\n const sinceFlushRef = useRef(0);\n const lastFlushAtRef = useRef(Date.now());\n const hoverStartRef = useRef<Map<string, number>>(new Map());\n const focusStartRef = useRef<Map<string, number>>(new Map());\n const lastInputAtRef = useRef<Map<string, number>>(new Map());\n const recentClicksRef = useRef<Map<string, number[]>>(new Map());\n const scrollRafRef = useRef<number | null>(null);\n const resizeRafRef = useRef<number | null>(null);\n const customSlotRef = useRef<Record<string, number | string | boolean>>({});\n\n const onFlushRef = useRef(onFlush);\n const pageContextRef = useRef(pageContext);\n useEffect(() => {\n onFlushRef.current = onFlush;\n }, [onFlush]);\n useEffect(() => {\n pageContextRef.current = pageContext;\n }, [pageContext]);\n\n const [recentEvents, setRecentEvents] = useState<BehaviorEvent[]>([]);\n\n const pushEvent = (ev: BehaviorEvent) => {\n const buf = bufferRef.current;\n buf.push(ev);\n if (buf.length > bufferSize) buf.splice(0, buf.length - bufferSize);\n sinceFlushRef.current += 1;\n\n setRecentEvents((prev) => {\n const next = [...prev, ev];\n if (next.length > RECENT_EVENTS_CAP) {\n next.splice(0, next.length - RECENT_EVENTS_CAP);\n }\n return next;\n });\n\n if (sinceFlushRef.current >= flushEveryEvents) {\n flush();\n }\n };\n\n const flush = () => {\n const summary = computeSummary(\n bufferRef.current,\n bufferSize,\n pageContextRef.current,\n customSlotRef.current,\n );\n sinceFlushRef.current = 0;\n lastFlushAtRef.current = Date.now();\n onFlushRef.current(summary);\n };\n\n // Seed trace handler (mount-only, rebases t to \"now\")\n useEffect(() => {\n if (!seedTrace || seedTrace.length === 0) return;\n const buf = bufferRef.current;\n const maxT = seedTrace.reduce((m, e) => (e.t > m ? e.t : m), -Infinity);\n const now = Date.now();\n const offset = Number.isFinite(maxT) ? now - maxT : 0;\n const rebased = seedTrace.map((e) => ({ ...e, t: e.t + offset }));\n buf.push(...rebased);\n if (buf.length > bufferSize) buf.splice(0, buf.length - bufferSize);\n const summary = computeSummary(\n buf,\n bufferSize,\n pageContextRef.current,\n customSlotRef.current,\n );\n sinceFlushRef.current = 0;\n lastFlushAtRef.current = Date.now();\n onFlushRef.current(summary);\n // mount-only seed\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Main listener wiring\n useEffect(() => {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return;\n const el =\n containerRef?.current ??\n (typeof document !== \"undefined\" ? document.body : null);\n if (!el) return;\n\n const onClick = (e: MouseEvent) => {\n const label = targetLabel(e.target);\n const t = Date.now();\n pushEvent({ kind: \"click\", target: label, t });\n\n // Rage-click detection\n const recent = recentClicksRef.current.get(label) ?? [];\n const filtered = recent.filter(\n (then) => t - then < RAGE_CLICK_WINDOW_MS,\n );\n filtered.push(t);\n recentClicksRef.current.set(label, filtered);\n if (filtered.length >= RAGE_CLICK_THRESHOLD) {\n pushEvent({\n kind: \"rage-click\",\n target: label,\n count: filtered.length,\n t,\n });\n recentClicksRef.current.set(label, []);\n }\n };\n\n const onMouseOver = (e: MouseEvent) => {\n const label = targetLabel(e.target);\n if (!hoverStartRef.current.has(label)) {\n hoverStartRef.current.set(label, Date.now());\n }\n };\n\n const onMouseOut = (e: MouseEvent) => {\n const label = targetLabel(e.target);\n const started = hoverStartRef.current.get(label);\n if (started == null) return;\n hoverStartRef.current.delete(label);\n const durationMs = Date.now() - started;\n if (durationMs < HOVER_MIN_MS) return;\n const t = Date.now();\n pushEvent({ kind: \"hover\", target: label, durationMs, t });\n pushEvent({ kind: \"dwell\", target: label, durationMs, t });\n };\n\n const onFocusIn = (e: FocusEvent) => {\n const label = targetLabel(e.target);\n focusStartRef.current.set(label, Date.now());\n pushEvent({ kind: \"focus\", target: label, t: Date.now() });\n };\n\n const onFocusOut = (e: FocusEvent) => {\n const label = targetLabel(e.target);\n const started = focusStartRef.current.get(label);\n if (started == null) return;\n focusStartRef.current.delete(label);\n const durationMs = Date.now() - started;\n if (durationMs < HOVER_MIN_MS) return;\n pushEvent({\n kind: \"blur\",\n target: label,\n durationMs,\n t: Date.now(),\n });\n };\n\n const onInput = (e: Event) => {\n const target = e.target;\n if (\n !(\n target instanceof HTMLInputElement ||\n target instanceof HTMLTextAreaElement ||\n target instanceof HTMLSelectElement ||\n (target instanceof HTMLElement &&\n (target as HTMLElement).isContentEditable)\n )\n ) {\n return;\n }\n const label = targetLabel(target);\n const now = Date.now();\n const last = lastInputAtRef.current.get(label) ?? 0;\n if (now - last < INPUT_THROTTLE_MS) return;\n lastInputAtRef.current.set(label, now);\n pushEvent({ kind: \"input\", target: label, t: now });\n };\n\n const onSubmit = (e: Event) => {\n const label = targetLabel(e.target);\n pushEvent({ kind: \"submit\", target: label, t: Date.now() });\n };\n\n const computeScrollDepth = () => {\n const rect = el.getBoundingClientRect();\n const viewportHCalc = window.innerHeight || 1;\n const elementH = el.scrollHeight || rect.height || 1;\n const visibleBottom = Math.min(rect.bottom, viewportHCalc);\n const scrolledPast = Math.max(0, visibleBottom - rect.top);\n return Math.max(0, Math.min(1, scrolledPast / elementH));\n };\n\n const onScroll = () => {\n if (scrollRafRef.current != null) return;\n scrollRafRef.current = requestAnimationFrame(() => {\n scrollRafRef.current = null;\n pushEvent({\n kind: \"scroll\",\n depth: computeScrollDepth(),\n t: Date.now(),\n });\n });\n };\n\n const onResize = () => {\n if (resizeRafRef.current != null) return;\n resizeRafRef.current = requestAnimationFrame(() => {\n resizeRafRef.current = null;\n pushEvent({\n kind: \"viewport-change\",\n width: window.innerWidth,\n height: window.innerHeight,\n t: Date.now(),\n });\n });\n };\n\n const onError = (e: ErrorEvent) => {\n pushEvent({\n kind: \"error\",\n message: String(e.message ?? \"unknown\"),\n t: Date.now(),\n });\n };\n\n const onUnhandledRejection = (e: PromiseRejectionEvent) => {\n pushEvent({\n kind: \"error\",\n message: `unhandled-rejection: ${String(e.reason ?? \"unknown\")}`,\n t: Date.now(),\n });\n };\n\n el.addEventListener(\"click\", onClick);\n el.addEventListener(\"mouseover\", onMouseOver);\n el.addEventListener(\"mouseout\", onMouseOut);\n el.addEventListener(\"focusin\", onFocusIn);\n el.addEventListener(\"focusout\", onFocusOut);\n el.addEventListener(\"input\", onInput);\n el.addEventListener(\"submit\", onSubmit);\n window.addEventListener(\"scroll\", onScroll, { passive: true });\n window.addEventListener(\"resize\", onResize);\n window.addEventListener(\"error\", onError);\n window.addEventListener(\"unhandledrejection\", onUnhandledRejection);\n\n // Custom listeners (plug-in)\n const cleanups: Array<() => void> = [];\n if (customListeners) {\n for (const listener of customListeners) {\n try {\n const cleanup = listener.attach(el, pushEvent);\n cleanups.push(cleanup);\n } catch {\n // Listener attach failure is non-fatal.\n }\n }\n }\n\n const interval = window.setInterval(() => {\n if (Date.now() - lastFlushAtRef.current >= flushAfterMs) {\n flush();\n }\n }, Math.max(250, Math.floor(flushAfterMs / 4)));\n\n return () => {\n el.removeEventListener(\"click\", onClick);\n el.removeEventListener(\"mouseover\", onMouseOver);\n el.removeEventListener(\"mouseout\", onMouseOut);\n el.removeEventListener(\"focusin\", onFocusIn);\n el.removeEventListener(\"focusout\", onFocusOut);\n el.removeEventListener(\"input\", onInput);\n el.removeEventListener(\"submit\", onSubmit);\n window.removeEventListener(\"scroll\", onScroll);\n window.removeEventListener(\"resize\", onResize);\n window.removeEventListener(\"error\", onError);\n window.removeEventListener(\"unhandledrejection\", onUnhandledRejection);\n window.clearInterval(interval);\n if (scrollRafRef.current != null) {\n cancelAnimationFrame(scrollRafRef.current);\n }\n if (resizeRafRef.current != null) {\n cancelAnimationFrame(resizeRafRef.current);\n }\n for (const cleanup of cleanups) {\n try {\n cleanup();\n } catch {\n // Tolerate cleanup errors.\n }\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [containerRef, flushAfterMs, flushEveryEvents, bufferSize]);\n\n return { recentEvents };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAwBA,iBAAkB;AAClB,gBAAgC;;;ACzBhC,YAAuB;;;ACAvB,mBAA4C;;;ADqOxC;AAnLJ,IAAM,eAAqB,oBAA6C,IAAI;AAErE,SAAS,kBAA6E;AAC3F,QAAM,MAAY,iBAAW,YAAY;AACzC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI,MAAM,qDAAqD;AAAA,EACvE;AACA,SAAO;AACT;;;AD3BA,IAAM,mBAAmB,aAAE,mBAAmB,QAAQ;AAAA,EACpD,aAAE,OAAO;AAAA,IACP,MAAM,aAAE,QAAQ,aAAa;AAAA,IAC7B,UAAU,aAAE,OAAO;AAAA,IACnB,MAAM,aAAE,OAAO;AAAA,IACf,OAAO,aAAE,OAAO;AAAA,EAClB,CAAC;AAAA,EACD,aAAE,OAAO;AAAA,IACP,MAAM,aAAE,QAAQ,WAAW;AAAA,IAC3B,UAAU,aAAE,OAAO;AAAA,IACnB,WAAW,aAAE,OAAO;AAAA,EACtB,CAAC;AAAA,EACD,aAAE,OAAO;AAAA,IACP,MAAM,aAAE,QAAQ,cAAc;AAAA,IAC9B,UAAU,aAAE,OAAO;AAAA,IACnB,WAAW,aAAE,OAAO;AAAA,EACtB,CAAC;AAAA,EACD,aAAE,OAAO;AAAA,IACP,MAAM,aAAE,QAAQ,WAAW;AAAA,IAC3B,UAAU,aAAE,OAAO;AAAA,IACnB,UAAU,aAAE,OAAO;AAAA,IACnB,OAAO,aAAE,OAAO;AAAA,EAClB,CAAC;AAAA,EACD,aAAE,OAAO;AAAA,IACP,MAAM,aAAE,QAAQ,eAAe;AAAA,IAC/B,UAAU,aAAE,OAAO;AAAA,IACnB,MAAM,aAAE,OAAO;AAAA,IACf,OAAO,aAAE,OAAO;AAAA,EAClB,CAAC;AAAA,EACD,aAAE,OAAO;AAAA,IACP,MAAM,aAAE,QAAQ,gBAAgB;AAAA,IAChC,UAAU,aAAE,OAAO;AAAA,IACnB,OAAO,aAAE,OAAO;AAAA,EAClB,CAAC;AACH,CAAC;AAED,IAAM,uBAAuB,aAAE,OAAO;AAAA,EACpC,aAAa,aAAE,MAAM,gBAAgB;AAAA,EACrC,YAAY,aAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC;AAAA,EACnC,WAAW,aAAE,OAAO;AAAA,EACpB,MAAM,aAAE,OAAO,aAAE,QAAQ,CAAC,EAAE,SAAS;AACvC,CAAC;AAWD,IAAM,sBAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkBrB,SAAS,kBACd,QAA+B,CAAC,GACL;AAC3B,uBAAqB,KAAK;AAC1B,SAAO;AACT;AAMO,SAAS,qBAAqB,OAA8B,CAAC,GAAS;AAC3E,QAAM,EAAE,WAAW,qBAAqB,kBAAkB,oBAAoB,IAAI;AAClF,QAAM,EAAE,UAAU,IAAI,gBAAgB;AAEtC,iCAAgB;AAAA,IACd,MAAM;AAAA,IACN,aAAa;AAAA,IACb,YAAY;AAAA,IACZ,SAAS,OAAO,SAAyB;AACvC,UAAI;AACF,kBAAU,IAAI;AACd,eAAO,WAAW,KAAK,YAAY,MAAM,cACvC,KAAK,YAAY,WAAW,IAAI,KAAK,GACvC,gBAAgB,KAAK,SAAS;AAAA,MAChC,SAAS,KAAK;AACZ,eAAO,4BACL,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CACjD;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AACH;","names":[]}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
type CopilotKitAdapterOpts = {
|
|
4
|
+
/** Override the tool name registered with the agent. Default: "apply_adaptations". */
|
|
5
|
+
toolName?: string;
|
|
6
|
+
/** Override the tool description shown to the agent. Default: see source. */
|
|
7
|
+
toolDescription?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* React component that registers begeniux's `apply_adaptations` frontend tool
|
|
11
|
+
* with CopilotKit. Mount once inside <BeGenProvider> and <CopilotKitProvider>.
|
|
12
|
+
*/
|
|
13
|
+
declare function CopilotKitAdapter(props?: CopilotKitAdapterOpts): React.ReactElement | null;
|
|
14
|
+
/**
|
|
15
|
+
* Hook variant of CopilotKitAdapter — call from your own component if you
|
|
16
|
+
* want to add custom behavior alongside the registration.
|
|
17
|
+
*/
|
|
18
|
+
declare function useCopilotKitAdapter(opts?: CopilotKitAdapterOpts): void;
|
|
19
|
+
|
|
20
|
+
export { CopilotKitAdapter, type CopilotKitAdapterOpts, useCopilotKitAdapter };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
type CopilotKitAdapterOpts = {
|
|
4
|
+
/** Override the tool name registered with the agent. Default: "apply_adaptations". */
|
|
5
|
+
toolName?: string;
|
|
6
|
+
/** Override the tool description shown to the agent. Default: see source. */
|
|
7
|
+
toolDescription?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* React component that registers begeniux's `apply_adaptations` frontend tool
|
|
11
|
+
* with CopilotKit. Mount once inside <BeGenProvider> and <CopilotKitProvider>.
|
|
12
|
+
*/
|
|
13
|
+
declare function CopilotKitAdapter(props?: CopilotKitAdapterOpts): React.ReactElement | null;
|
|
14
|
+
/**
|
|
15
|
+
* Hook variant of CopilotKitAdapter — call from your own component if you
|
|
16
|
+
* want to add custom behavior alongside the registration.
|
|
17
|
+
*/
|
|
18
|
+
declare function useCopilotKitAdapter(opts?: CopilotKitAdapterOpts): void;
|
|
19
|
+
|
|
20
|
+
export { CopilotKitAdapter, type CopilotKitAdapterOpts, useCopilotKitAdapter };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// src/adapters/copilotkit.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { useFrontendTool } from "@copilotkit/react-core/v2";
|
|
4
|
+
|
|
5
|
+
// src/BeGenProvider.tsx
|
|
6
|
+
import * as React from "react";
|
|
7
|
+
|
|
8
|
+
// src/useBehaviorTracker.ts
|
|
9
|
+
import { useEffect, useRef, useState } from "react";
|
|
10
|
+
|
|
11
|
+
// src/BeGenProvider.tsx
|
|
12
|
+
import { jsx } from "react/jsx-runtime";
|
|
13
|
+
var BeGenContext = React.createContext(null);
|
|
14
|
+
function useBeGenContext() {
|
|
15
|
+
const ctx = React.useContext(BeGenContext);
|
|
16
|
+
if (!ctx) {
|
|
17
|
+
throw new Error("useBeGenContext must be used inside <BeGenProvider>");
|
|
18
|
+
}
|
|
19
|
+
return ctx;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/adapters/copilotkit.ts
|
|
23
|
+
var adaptationSchema = z.discriminatedUnion("kind", [
|
|
24
|
+
z.object({
|
|
25
|
+
kind: z.literal("set-css-var"),
|
|
26
|
+
selector: z.string(),
|
|
27
|
+
name: z.string(),
|
|
28
|
+
value: z.string()
|
|
29
|
+
}),
|
|
30
|
+
z.object({
|
|
31
|
+
kind: z.literal("add-class"),
|
|
32
|
+
selector: z.string(),
|
|
33
|
+
className: z.string()
|
|
34
|
+
}),
|
|
35
|
+
z.object({
|
|
36
|
+
kind: z.literal("remove-class"),
|
|
37
|
+
selector: z.string(),
|
|
38
|
+
className: z.string()
|
|
39
|
+
}),
|
|
40
|
+
z.object({
|
|
41
|
+
kind: z.literal("set-style"),
|
|
42
|
+
selector: z.string(),
|
|
43
|
+
property: z.string(),
|
|
44
|
+
value: z.string()
|
|
45
|
+
}),
|
|
46
|
+
z.object({
|
|
47
|
+
kind: z.literal("set-attribute"),
|
|
48
|
+
selector: z.string(),
|
|
49
|
+
name: z.string(),
|
|
50
|
+
value: z.string()
|
|
51
|
+
}),
|
|
52
|
+
z.object({
|
|
53
|
+
kind: z.literal("set-aria-label"),
|
|
54
|
+
selector: z.string(),
|
|
55
|
+
value: z.string()
|
|
56
|
+
})
|
|
57
|
+
]);
|
|
58
|
+
var adaptationPlanSchema = z.object({
|
|
59
|
+
adaptations: z.array(adaptationSchema),
|
|
60
|
+
confidence: z.number().min(0).max(1),
|
|
61
|
+
reasoning: z.string(),
|
|
62
|
+
meta: z.record(z.unknown()).optional()
|
|
63
|
+
});
|
|
64
|
+
var DEFAULT_DESCRIPTION = `Apply CSS-only UI adaptations to the live page. Use this whenever you decide that the user would benefit from a UI change based on their behavior, current page state, and the design system the host app has declared.
|
|
65
|
+
|
|
66
|
+
Each adaptation is one of:
|
|
67
|
+
- set-css-var: change a CSS custom property on a target element
|
|
68
|
+
- add-class / remove-class: toggle a class on a target element
|
|
69
|
+
- set-style: set a single CSS declaration (no display/position/visibility/float/clear/z-index/overflow \u2014 those are denied for safety)
|
|
70
|
+
- set-attribute / set-aria-label: change an attribute, often for accessibility
|
|
71
|
+
|
|
72
|
+
You should:
|
|
73
|
+
- Use the design system manifest to know which CSS variables and classes are valid
|
|
74
|
+
- Target stable selectors (id, [data-begen-id], [data-testid], semantic tags) \u2014 avoid utility class soup
|
|
75
|
+
- Justify the plan in the reasoning field (one sentence)
|
|
76
|
+
- Set confidence \u2208 [0,1]; below 0.4 means "not sure, prefer no change"`;
|
|
77
|
+
function CopilotKitAdapter(props = {}) {
|
|
78
|
+
useCopilotKitAdapter(props);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
function useCopilotKitAdapter(opts = {}) {
|
|
82
|
+
const { toolName = "apply_adaptations", toolDescription = DEFAULT_DESCRIPTION } = opts;
|
|
83
|
+
const { applyPlan } = useBeGenContext();
|
|
84
|
+
useFrontendTool({
|
|
85
|
+
name: toolName,
|
|
86
|
+
description: toolDescription,
|
|
87
|
+
parameters: adaptationPlanSchema,
|
|
88
|
+
handler: async (plan) => {
|
|
89
|
+
try {
|
|
90
|
+
applyPlan(plan);
|
|
91
|
+
return `Applied ${plan.adaptations.length} adaptation${plan.adaptations.length === 1 ? "" : "s"}. Reasoning: ${plan.reasoning}`;
|
|
92
|
+
} catch (err) {
|
|
93
|
+
return `Adaptation engine error: ${err instanceof Error ? err.message : String(err)}`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
export {
|
|
99
|
+
CopilotKitAdapter,
|
|
100
|
+
useCopilotKitAdapter
|
|
101
|
+
};
|
|
102
|
+
//# sourceMappingURL=copilotkit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapters/copilotkit.ts","../src/BeGenProvider.tsx","../src/useBehaviorTracker.ts"],"sourcesContent":["// CopilotKit v2 adapter for begeniux.\n//\n// Pattern:\n// <CopilotKitProvider runtimeUrl=\"/api/copilotkit\">\n// <BeGenProvider designSystem={...} pageContext={...}>\n// <CopilotKitAdapter /> // mount once, anywhere inside both providers\n// <App />\n// </BeGenProvider>\n// </CopilotKitProvider>\n//\n// What it does:\n// 1. Registers `apply_adaptations` as a frontend tool. The agent calls this\n// tool with an AdaptationPlan; we route the plan into the provider's\n// AdaptationEngine which mutates the live DOM with full reversibility.\n//\n// Why a component, not a function: CopilotKit hooks (`useFrontendTool`) must\n// run inside a React render. Wrapping in `<CopilotKitAdapter />` keeps the\n// integration declarative.\n//\n// Peer-deps: `@copilotkit/react-core@>=2.0.0` and `zod@>=3` must be installed\n// by the consumer. Without them, importing this module throws at runtime; the\n// rest of begeniux (HTTP adapter, manual engine) still works.\n\nimport * as React from \"react\";\nimport { z } from \"zod\";\nimport { useFrontendTool } from \"@copilotkit/react-core/v2\";\nimport { useBeGenContext } from \"../BeGenProvider\";\nimport type { AdaptationPlan } from \"../types\";\n\n// ─── Zod schemas mirroring src/types.ts ─────────────────────────────\n\nconst adaptationSchema = z.discriminatedUnion(\"kind\", [\n z.object({\n kind: z.literal(\"set-css-var\"),\n selector: z.string(),\n name: z.string(),\n value: z.string(),\n }),\n z.object({\n kind: z.literal(\"add-class\"),\n selector: z.string(),\n className: z.string(),\n }),\n z.object({\n kind: z.literal(\"remove-class\"),\n selector: z.string(),\n className: z.string(),\n }),\n z.object({\n kind: z.literal(\"set-style\"),\n selector: z.string(),\n property: z.string(),\n value: z.string(),\n }),\n z.object({\n kind: z.literal(\"set-attribute\"),\n selector: z.string(),\n name: z.string(),\n value: z.string(),\n }),\n z.object({\n kind: z.literal(\"set-aria-label\"),\n selector: z.string(),\n value: z.string(),\n }),\n]);\n\nconst adaptationPlanSchema = z.object({\n adaptations: z.array(adaptationSchema),\n confidence: z.number().min(0).max(1),\n reasoning: z.string(),\n meta: z.record(z.unknown()).optional(),\n});\n\n// ─── Public API ─────────────────────────────────────────────────────\n\nexport type CopilotKitAdapterOpts = {\n /** Override the tool name registered with the agent. Default: \"apply_adaptations\". */\n toolName?: string;\n /** Override the tool description shown to the agent. Default: see source. */\n toolDescription?: string;\n};\n\nconst DEFAULT_DESCRIPTION = `Apply CSS-only UI adaptations to the live page. Use this whenever you decide that the user would benefit from a UI change based on their behavior, current page state, and the design system the host app has declared.\n\nEach adaptation is one of:\n- set-css-var: change a CSS custom property on a target element\n- add-class / remove-class: toggle a class on a target element\n- set-style: set a single CSS declaration (no display/position/visibility/float/clear/z-index/overflow — those are denied for safety)\n- set-attribute / set-aria-label: change an attribute, often for accessibility\n\nYou should:\n- Use the design system manifest to know which CSS variables and classes are valid\n- Target stable selectors (id, [data-begen-id], [data-testid], semantic tags) — avoid utility class soup\n- Justify the plan in the reasoning field (one sentence)\n- Set confidence ∈ [0,1]; below 0.4 means \"not sure, prefer no change\"`;\n\n/**\n * React component that registers begeniux's `apply_adaptations` frontend tool\n * with CopilotKit. Mount once inside <BeGenProvider> and <CopilotKitProvider>.\n */\nexport function CopilotKitAdapter(\n props: CopilotKitAdapterOpts = {},\n): React.ReactElement | null {\n useCopilotKitAdapter(props);\n return null;\n}\n\n/**\n * Hook variant of CopilotKitAdapter — call from your own component if you\n * want to add custom behavior alongside the registration.\n */\nexport function useCopilotKitAdapter(opts: CopilotKitAdapterOpts = {}): void {\n const { toolName = \"apply_adaptations\", toolDescription = DEFAULT_DESCRIPTION } = opts;\n const { applyPlan } = useBeGenContext();\n\n useFrontendTool({\n name: toolName,\n description: toolDescription,\n parameters: adaptationPlanSchema,\n handler: async (plan: AdaptationPlan) => {\n try {\n applyPlan(plan);\n return `Applied ${plan.adaptations.length} adaptation${\n plan.adaptations.length === 1 ? \"\" : \"s\"\n }. Reasoning: ${plan.reasoning}`;\n } catch (err) {\n return `Adaptation engine error: ${\n err instanceof Error ? err.message : String(err)\n }`;\n }\n },\n });\n}\n","import * as React from \"react\";\nimport type {\n Adaptation,\n AdaptationPlan,\n AdaptInput,\n BeGenContextValue,\n BehaviorEvent,\n BehaviorListener,\n BehaviorSummary,\n ClassifyFn,\n DesignSystem,\n ScopeOpts,\n} from \"./types\";\nimport { useBehaviorTracker } from \"./useBehaviorTracker\";\nimport { AdaptationEngine } from \"./AdaptationEngine\";\nimport { snapshotVisibleSelectors } from \"./domSnapshot\";\n\nexport type BeGenProviderProps<TContext = { route: string }> = {\n /** The agent's vocabulary — what CSS variables and classes it may emit. */\n designSystem: DesignSystem;\n /** Per-page state forwarded into every BehaviorSummary. */\n pageContext: TContext;\n /**\n * Single-pass agent. Receives behavior + DOM + design system, returns a plan.\n * If omitted, the provider does NOT call any agent — it only tracks behavior\n * and exposes summaries via context. (Pair with createCopilotKitAdapter or\n * createHttpAdapter for real adaptation.)\n */\n classify?: ClassifyFn<TContext>;\n /** Restrict where the agent's adaptations may target. */\n scope?: ScopeOpts;\n /** Minimum ms between agent calls. Default 5000. */\n rateLimitMs?: number;\n /** Don't trigger an agent call until at least this many events have flushed since the last call. Default 5. */\n triggerEveryEvents?: number;\n /** Tracker idle-flush window. */\n flushAfterMs?: number;\n /** Tracker count-flush. */\n flushEveryEvents?: number;\n /** Tracker ring-buffer cap. */\n bufferSize?: number;\n /** Plug-in event sources. */\n customListeners?: BehaviorListener[];\n /** Element to scope listeners to. Default: document.body. */\n containerRef?: React.RefObject<HTMLElement | null>;\n /** Pre-canned events injected on mount (for demos). */\n seedTrace?: BehaviorEvent[];\n children: React.ReactNode;\n};\n\nconst BeGenContext = React.createContext<BeGenContextValue<any> | null>(null);\n\nexport function useBeGenContext<TContext = { route: string }>(): BeGenContextValue<TContext> {\n const ctx = React.useContext(BeGenContext);\n if (!ctx) {\n throw new Error(\"useBeGenContext must be used inside <BeGenProvider>\");\n }\n return ctx as BeGenContextValue<TContext>;\n}\n\nexport function BeGenProvider<TContext = { route: string }>(\n props: BeGenProviderProps<TContext>,\n): React.ReactElement {\n const {\n designSystem,\n pageContext,\n classify,\n scope,\n rateLimitMs = 5000,\n triggerEveryEvents = 5,\n flushAfterMs = 5000,\n flushEveryEvents = 10,\n bufferSize = 50,\n customListeners,\n containerRef,\n seedTrace,\n children,\n } = props;\n\n // Engine is created lazily once document.body exists (post-mount).\n const engineRef = React.useRef<AdaptationEngine | null>(null);\n\n const [summary, setSummary] = React.useState<BehaviorSummary<TContext> | null>(null);\n const [lastPlan, setLastPlan] = React.useState<AdaptationPlan | null>(null);\n const [appliedAdaptations, setAppliedAdaptations] = React.useState<\n ReadonlyArray<Adaptation>\n >([]);\n\n const lastClassifyAtRef = React.useRef(0);\n const inFlightRef = React.useRef(false);\n const eventsSinceClassifyRef = React.useRef(0);\n const lastPlanHashRef = React.useRef<string | null>(null);\n\n const classifyRef = React.useRef(classify);\n React.useEffect(() => {\n classifyRef.current = classify;\n }, [classify]);\n\n const designSystemRef = React.useRef(designSystem);\n React.useEffect(() => {\n designSystemRef.current = designSystem;\n }, [designSystem]);\n\n const pageContextRef = React.useRef(pageContext);\n React.useEffect(() => {\n pageContextRef.current = pageContext;\n }, [pageContext]);\n\n const scopeRef = React.useRef(scope);\n React.useEffect(() => {\n scopeRef.current = scope;\n }, [scope]);\n\n // Mount the engine once we're in the browser.\n React.useEffect(() => {\n if (typeof document === \"undefined\") return;\n const root = containerRef?.current ?? document.body;\n if (!root) return;\n engineRef.current = new AdaptationEngine({\n root,\n scope: scopeRef.current,\n onEvent: () => {\n if (engineRef.current) {\n setAppliedAdaptations(engineRef.current.getApplied().slice());\n }\n },\n });\n return () => {\n engineRef.current?.revertAll();\n engineRef.current = null;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n const handleFlush = React.useCallback(\n async (s: BehaviorSummary<TContext>) => {\n setSummary(s);\n eventsSinceClassifyRef.current += 1;\n\n const fn = classifyRef.current;\n if (!fn) return;\n if (inFlightRef.current) return;\n const now = Date.now();\n if (now - lastClassifyAtRef.current < rateLimitMs) return;\n if (eventsSinceClassifyRef.current < triggerEveryEvents) return;\n\n lastClassifyAtRef.current = now;\n eventsSinceClassifyRef.current = 0;\n inFlightRef.current = true;\n\n try {\n const root =\n (containerRef?.current ?? (typeof document !== \"undefined\" ? document.body : null)) ??\n null;\n const visibleSelectors = root\n ? snapshotVisibleSelectors(root, { scope: scopeRef.current })\n : [];\n\n const route =\n (s.page_context as unknown as { route?: string })?.route ?? \"/\";\n\n const input: AdaptInput<TContext> = {\n summary: s,\n designSystem: designSystemRef.current,\n dom: { visibleSelectors, route },\n };\n\n const plan = await fn(input);\n if (!plan || !Array.isArray(plan.adaptations)) return;\n\n // Skip identical consecutive plans.\n const hash = hashPlan(plan);\n if (hash === lastPlanHashRef.current) return;\n lastPlanHashRef.current = hash;\n\n setLastPlan(plan);\n engineRef.current?.apply(plan);\n } catch {\n // Classifier failure: keep current state.\n } finally {\n inFlightRef.current = false;\n }\n },\n [rateLimitMs, triggerEveryEvents, containerRef],\n );\n\n useBehaviorTracker<TContext>({\n containerRef,\n onFlush: handleFlush,\n pageContext,\n seedTrace,\n customListeners,\n flushAfterMs,\n flushEveryEvents,\n bufferSize,\n });\n\n const recentEventsRef = React.useRef<ReadonlyArray<BehaviorEvent>>([]);\n // We don't expose recentEvents directly from tracker into context here to\n // avoid extra renders; consumers needing a live event stream can use\n // useBehaviorTracker directly.\n\n const applyPlan = React.useCallback((plan: AdaptationPlan) => {\n if (!plan || !Array.isArray(plan.adaptations)) return;\n const hash = hashPlan(plan);\n if (hash === lastPlanHashRef.current) return;\n lastPlanHashRef.current = hash;\n setLastPlan(plan);\n engineRef.current?.apply(plan);\n }, []);\n\n const getDesignSystem = React.useCallback(\n () => designSystemRef.current,\n [],\n );\n\n const value = React.useMemo<BeGenContextValue<TContext>>(\n () => ({\n summary,\n lastPlan,\n appliedAdaptations,\n recentEvents: recentEventsRef.current,\n applyPlan,\n getDesignSystem,\n }),\n [summary, lastPlan, appliedAdaptations, applyPlan, getDesignSystem],\n );\n\n return (\n <BeGenContext.Provider value={value as BeGenContextValue<any>}>\n {children}\n </BeGenContext.Provider>\n );\n}\n\nfunction hashPlan(plan: AdaptationPlan): string {\n // Cheap deterministic hash for skip-if-identical comparison.\n // Order-sensitive: agent emitting adaptations in different orders is treated as different intent.\n const parts = plan.adaptations.map((a) => {\n switch (a.kind) {\n case \"set-css-var\":\n return `v|${a.selector}|${a.name}|${a.value}`;\n case \"add-class\":\n return `+|${a.selector}|${a.className}`;\n case \"remove-class\":\n return `-|${a.selector}|${a.className}`;\n case \"set-style\":\n return `s|${a.selector}|${a.property}|${a.value}`;\n case \"set-attribute\":\n return `a|${a.selector}|${a.name}|${a.value}`;\n case \"set-aria-label\":\n return `l|${a.selector}|${a.value}`;\n }\n });\n return parts.join(\"\\n\");\n}\n","import { useEffect, useRef, useState } from \"react\";\nimport type {\n BehaviorEvent,\n BehaviorListener,\n BehaviorSummary,\n} from \"./types\";\n\nexport type UseBehaviorTrackerOpts<TContext = { route: string }> = {\n /** DOM element to scope listeners to. Defaults to document.body. */\n containerRef?: React.RefObject<HTMLElement | null>;\n /** Flush after this many events. Default 10. */\n flushEveryEvents?: number;\n /** Flush after this many ms of idle. Default 5000. */\n flushAfterMs?: number;\n /** Ring buffer cap. Default 50. */\n bufferSize?: number;\n /** Called on every flush with the computed summary. */\n onFlush: (summary: BehaviorSummary<TContext>) => void;\n /** Page context (route, etc.) — included in every summary. */\n pageContext: TContext;\n /** Optional pre-canned events injected on mount. */\n seedTrace?: BehaviorEvent[];\n /** Plug-in event sources beyond the built-ins. */\n customListeners?: BehaviorListener[];\n};\n\nconst RECENT_EVENTS_CAP = 10;\nconst HOVER_MIN_MS = 200;\nconst RAGE_CLICK_WINDOW_MS = 1000;\nconst RAGE_CLICK_THRESHOLD = 3;\nconst INPUT_THROTTLE_MS = 1000;\n\nfunction targetLabel(el: EventTarget | null): string {\n if (!(el instanceof HTMLElement)) return \"unknown\";\n const begenId = el.getAttribute(\"data-begen-id\");\n if (begenId) return `[data-begen-id=\"${begenId}\"]`;\n if (el.id) return `#${el.id}`;\n const testId = el.getAttribute(\"data-testid\");\n if (testId) return `[data-testid=\"${testId}\"]`;\n return el.tagName.toLowerCase();\n}\n\nfunction computeSummary<TContext>(\n buffer: BehaviorEvent[],\n bufferSize: number,\n pageContext: TContext,\n custom: Record<string, number | string | boolean>,\n): BehaviorSummary<TContext> {\n const now = Date.now();\n const windowStart = now - 60_000;\n\n let clicksLastMin = 0;\n let rageClicks = 0;\n let dwellSum = 0;\n let dwellCount = 0;\n let maxScroll = 0;\n let formInteractions = 0;\n let errorsSeen = 0;\n const hoverTargets = new Set<string>();\n let viewportW =\n typeof window !== \"undefined\" ? window.innerWidth || 0 : 0;\n let viewportH =\n typeof window !== \"undefined\" ? window.innerHeight || 0 : 0;\n\n for (const ev of buffer) {\n switch (ev.kind) {\n case \"click\":\n if (ev.t >= windowStart) clicksLastMin += 1;\n break;\n case \"rage-click\":\n rageClicks += 1;\n break;\n case \"dwell\":\n dwellSum += ev.durationMs;\n dwellCount += 1;\n break;\n case \"scroll\":\n if (ev.depth > maxScroll) maxScroll = ev.depth;\n break;\n case \"hover\":\n hoverTargets.add(ev.target);\n break;\n case \"input\":\n case \"submit\":\n formInteractions += 1;\n break;\n case \"error\":\n errorsSeen += 1;\n break;\n case \"viewport-change\":\n viewportW = ev.width;\n viewportH = ev.height;\n break;\n default:\n break;\n }\n }\n\n return {\n clicks_per_min: clicksLastMin,\n rage_clicks: rageClicks,\n avg_dwell_ms: dwellCount === 0 ? 0 : Math.round(dwellSum / dwellCount),\n scroll_depth: Math.max(0, Math.min(1, maxScroll)),\n hover_count: hoverTargets.size,\n form_interactions: formInteractions,\n errors_seen: errorsSeen,\n events_seen: Math.min(buffer.length, bufferSize),\n viewport: { width: viewportW, height: viewportH },\n custom,\n page_context: pageContext,\n };\n}\n\nexport function useBehaviorTracker<TContext = { route: string }>(\n opts: UseBehaviorTrackerOpts<TContext>,\n): { recentEvents: BehaviorEvent[] } {\n const {\n containerRef,\n flushEveryEvents = 10,\n flushAfterMs = 5000,\n bufferSize = 50,\n onFlush,\n pageContext,\n seedTrace,\n customListeners,\n } = opts;\n\n const bufferRef = useRef<BehaviorEvent[]>([]);\n const sinceFlushRef = useRef(0);\n const lastFlushAtRef = useRef(Date.now());\n const hoverStartRef = useRef<Map<string, number>>(new Map());\n const focusStartRef = useRef<Map<string, number>>(new Map());\n const lastInputAtRef = useRef<Map<string, number>>(new Map());\n const recentClicksRef = useRef<Map<string, number[]>>(new Map());\n const scrollRafRef = useRef<number | null>(null);\n const resizeRafRef = useRef<number | null>(null);\n const customSlotRef = useRef<Record<string, number | string | boolean>>({});\n\n const onFlushRef = useRef(onFlush);\n const pageContextRef = useRef(pageContext);\n useEffect(() => {\n onFlushRef.current = onFlush;\n }, [onFlush]);\n useEffect(() => {\n pageContextRef.current = pageContext;\n }, [pageContext]);\n\n const [recentEvents, setRecentEvents] = useState<BehaviorEvent[]>([]);\n\n const pushEvent = (ev: BehaviorEvent) => {\n const buf = bufferRef.current;\n buf.push(ev);\n if (buf.length > bufferSize) buf.splice(0, buf.length - bufferSize);\n sinceFlushRef.current += 1;\n\n setRecentEvents((prev) => {\n const next = [...prev, ev];\n if (next.length > RECENT_EVENTS_CAP) {\n next.splice(0, next.length - RECENT_EVENTS_CAP);\n }\n return next;\n });\n\n if (sinceFlushRef.current >= flushEveryEvents) {\n flush();\n }\n };\n\n const flush = () => {\n const summary = computeSummary(\n bufferRef.current,\n bufferSize,\n pageContextRef.current,\n customSlotRef.current,\n );\n sinceFlushRef.current = 0;\n lastFlushAtRef.current = Date.now();\n onFlushRef.current(summary);\n };\n\n // Seed trace handler (mount-only, rebases t to \"now\")\n useEffect(() => {\n if (!seedTrace || seedTrace.length === 0) return;\n const buf = bufferRef.current;\n const maxT = seedTrace.reduce((m, e) => (e.t > m ? e.t : m), -Infinity);\n const now = Date.now();\n const offset = Number.isFinite(maxT) ? now - maxT : 0;\n const rebased = seedTrace.map((e) => ({ ...e, t: e.t + offset }));\n buf.push(...rebased);\n if (buf.length > bufferSize) buf.splice(0, buf.length - bufferSize);\n const summary = computeSummary(\n buf,\n bufferSize,\n pageContextRef.current,\n customSlotRef.current,\n );\n sinceFlushRef.current = 0;\n lastFlushAtRef.current = Date.now();\n onFlushRef.current(summary);\n // mount-only seed\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Main listener wiring\n useEffect(() => {\n if (typeof window === \"undefined\" || typeof document === \"undefined\") return;\n const el =\n containerRef?.current ??\n (typeof document !== \"undefined\" ? document.body : null);\n if (!el) return;\n\n const onClick = (e: MouseEvent) => {\n const label = targetLabel(e.target);\n const t = Date.now();\n pushEvent({ kind: \"click\", target: label, t });\n\n // Rage-click detection\n const recent = recentClicksRef.current.get(label) ?? [];\n const filtered = recent.filter(\n (then) => t - then < RAGE_CLICK_WINDOW_MS,\n );\n filtered.push(t);\n recentClicksRef.current.set(label, filtered);\n if (filtered.length >= RAGE_CLICK_THRESHOLD) {\n pushEvent({\n kind: \"rage-click\",\n target: label,\n count: filtered.length,\n t,\n });\n recentClicksRef.current.set(label, []);\n }\n };\n\n const onMouseOver = (e: MouseEvent) => {\n const label = targetLabel(e.target);\n if (!hoverStartRef.current.has(label)) {\n hoverStartRef.current.set(label, Date.now());\n }\n };\n\n const onMouseOut = (e: MouseEvent) => {\n const label = targetLabel(e.target);\n const started = hoverStartRef.current.get(label);\n if (started == null) return;\n hoverStartRef.current.delete(label);\n const durationMs = Date.now() - started;\n if (durationMs < HOVER_MIN_MS) return;\n const t = Date.now();\n pushEvent({ kind: \"hover\", target: label, durationMs, t });\n pushEvent({ kind: \"dwell\", target: label, durationMs, t });\n };\n\n const onFocusIn = (e: FocusEvent) => {\n const label = targetLabel(e.target);\n focusStartRef.current.set(label, Date.now());\n pushEvent({ kind: \"focus\", target: label, t: Date.now() });\n };\n\n const onFocusOut = (e: FocusEvent) => {\n const label = targetLabel(e.target);\n const started = focusStartRef.current.get(label);\n if (started == null) return;\n focusStartRef.current.delete(label);\n const durationMs = Date.now() - started;\n if (durationMs < HOVER_MIN_MS) return;\n pushEvent({\n kind: \"blur\",\n target: label,\n durationMs,\n t: Date.now(),\n });\n };\n\n const onInput = (e: Event) => {\n const target = e.target;\n if (\n !(\n target instanceof HTMLInputElement ||\n target instanceof HTMLTextAreaElement ||\n target instanceof HTMLSelectElement ||\n (target instanceof HTMLElement &&\n (target as HTMLElement).isContentEditable)\n )\n ) {\n return;\n }\n const label = targetLabel(target);\n const now = Date.now();\n const last = lastInputAtRef.current.get(label) ?? 0;\n if (now - last < INPUT_THROTTLE_MS) return;\n lastInputAtRef.current.set(label, now);\n pushEvent({ kind: \"input\", target: label, t: now });\n };\n\n const onSubmit = (e: Event) => {\n const label = targetLabel(e.target);\n pushEvent({ kind: \"submit\", target: label, t: Date.now() });\n };\n\n const computeScrollDepth = () => {\n const rect = el.getBoundingClientRect();\n const viewportHCalc = window.innerHeight || 1;\n const elementH = el.scrollHeight || rect.height || 1;\n const visibleBottom = Math.min(rect.bottom, viewportHCalc);\n const scrolledPast = Math.max(0, visibleBottom - rect.top);\n return Math.max(0, Math.min(1, scrolledPast / elementH));\n };\n\n const onScroll = () => {\n if (scrollRafRef.current != null) return;\n scrollRafRef.current = requestAnimationFrame(() => {\n scrollRafRef.current = null;\n pushEvent({\n kind: \"scroll\",\n depth: computeScrollDepth(),\n t: Date.now(),\n });\n });\n };\n\n const onResize = () => {\n if (resizeRafRef.current != null) return;\n resizeRafRef.current = requestAnimationFrame(() => {\n resizeRafRef.current = null;\n pushEvent({\n kind: \"viewport-change\",\n width: window.innerWidth,\n height: window.innerHeight,\n t: Date.now(),\n });\n });\n };\n\n const onError = (e: ErrorEvent) => {\n pushEvent({\n kind: \"error\",\n message: String(e.message ?? \"unknown\"),\n t: Date.now(),\n });\n };\n\n const onUnhandledRejection = (e: PromiseRejectionEvent) => {\n pushEvent({\n kind: \"error\",\n message: `unhandled-rejection: ${String(e.reason ?? \"unknown\")}`,\n t: Date.now(),\n });\n };\n\n el.addEventListener(\"click\", onClick);\n el.addEventListener(\"mouseover\", onMouseOver);\n el.addEventListener(\"mouseout\", onMouseOut);\n el.addEventListener(\"focusin\", onFocusIn);\n el.addEventListener(\"focusout\", onFocusOut);\n el.addEventListener(\"input\", onInput);\n el.addEventListener(\"submit\", onSubmit);\n window.addEventListener(\"scroll\", onScroll, { passive: true });\n window.addEventListener(\"resize\", onResize);\n window.addEventListener(\"error\", onError);\n window.addEventListener(\"unhandledrejection\", onUnhandledRejection);\n\n // Custom listeners (plug-in)\n const cleanups: Array<() => void> = [];\n if (customListeners) {\n for (const listener of customListeners) {\n try {\n const cleanup = listener.attach(el, pushEvent);\n cleanups.push(cleanup);\n } catch {\n // Listener attach failure is non-fatal.\n }\n }\n }\n\n const interval = window.setInterval(() => {\n if (Date.now() - lastFlushAtRef.current >= flushAfterMs) {\n flush();\n }\n }, Math.max(250, Math.floor(flushAfterMs / 4)));\n\n return () => {\n el.removeEventListener(\"click\", onClick);\n el.removeEventListener(\"mouseover\", onMouseOver);\n el.removeEventListener(\"mouseout\", onMouseOut);\n el.removeEventListener(\"focusin\", onFocusIn);\n el.removeEventListener(\"focusout\", onFocusOut);\n el.removeEventListener(\"input\", onInput);\n el.removeEventListener(\"submit\", onSubmit);\n window.removeEventListener(\"scroll\", onScroll);\n window.removeEventListener(\"resize\", onResize);\n window.removeEventListener(\"error\", onError);\n window.removeEventListener(\"unhandledrejection\", onUnhandledRejection);\n window.clearInterval(interval);\n if (scrollRafRef.current != null) {\n cancelAnimationFrame(scrollRafRef.current);\n }\n if (resizeRafRef.current != null) {\n cancelAnimationFrame(resizeRafRef.current);\n }\n for (const cleanup of cleanups) {\n try {\n cleanup();\n } catch {\n // Tolerate cleanup errors.\n }\n }\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [containerRef, flushAfterMs, flushEveryEvents, bufferSize]);\n\n return { recentEvents };\n}\n"],"mappings":";AAwBA,SAAS,SAAS;AAClB,SAAS,uBAAuB;;;ACzBhC,YAAY,WAAW;;;ACAvB,SAAS,WAAW,QAAQ,gBAAgB;;;ADqOxC;AAnLJ,IAAM,eAAqB,oBAA6C,IAAI;AAErE,SAAS,kBAA6E;AAC3F,QAAM,MAAY,iBAAW,YAAY;AACzC,MAAI,CAAC,KAAK;AACR,UAAM,IAAI,MAAM,qDAAqD;AAAA,EACvE;AACA,SAAO;AACT;;;AD3BA,IAAM,mBAAmB,EAAE,mBAAmB,QAAQ;AAAA,EACpD,EAAE,OAAO;AAAA,IACP,MAAM,EAAE,QAAQ,aAAa;AAAA,IAC7B,UAAU,EAAE,OAAO;AAAA,IACnB,MAAM,EAAE,OAAO;AAAA,IACf,OAAO,EAAE,OAAO;AAAA,EAClB,CAAC;AAAA,EACD,EAAE,OAAO;AAAA,IACP,MAAM,EAAE,QAAQ,WAAW;AAAA,IAC3B,UAAU,EAAE,OAAO;AAAA,IACnB,WAAW,EAAE,OAAO;AAAA,EACtB,CAAC;AAAA,EACD,EAAE,OAAO;AAAA,IACP,MAAM,EAAE,QAAQ,cAAc;AAAA,IAC9B,UAAU,EAAE,OAAO;AAAA,IACnB,WAAW,EAAE,OAAO;AAAA,EACtB,CAAC;AAAA,EACD,EAAE,OAAO;AAAA,IACP,MAAM,EAAE,QAAQ,WAAW;AAAA,IAC3B,UAAU,EAAE,OAAO;AAAA,IACnB,UAAU,EAAE,OAAO;AAAA,IACnB,OAAO,EAAE,OAAO;AAAA,EAClB,CAAC;AAAA,EACD,EAAE,OAAO;AAAA,IACP,MAAM,EAAE,QAAQ,eAAe;AAAA,IAC/B,UAAU,EAAE,OAAO;AAAA,IACnB,MAAM,EAAE,OAAO;AAAA,IACf,OAAO,EAAE,OAAO;AAAA,EAClB,CAAC;AAAA,EACD,EAAE,OAAO;AAAA,IACP,MAAM,EAAE,QAAQ,gBAAgB;AAAA,IAChC,UAAU,EAAE,OAAO;AAAA,IACnB,OAAO,EAAE,OAAO;AAAA,EAClB,CAAC;AACH,CAAC;AAED,IAAM,uBAAuB,EAAE,OAAO;AAAA,EACpC,aAAa,EAAE,MAAM,gBAAgB;AAAA,EACrC,YAAY,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,IAAI,CAAC;AAAA,EACnC,WAAW,EAAE,OAAO;AAAA,EACpB,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS;AACvC,CAAC;AAWD,IAAM,sBAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAkBrB,SAAS,kBACd,QAA+B,CAAC,GACL;AAC3B,uBAAqB,KAAK;AAC1B,SAAO;AACT;AAMO,SAAS,qBAAqB,OAA8B,CAAC,GAAS;AAC3E,QAAM,EAAE,WAAW,qBAAqB,kBAAkB,oBAAoB,IAAI;AAClF,QAAM,EAAE,UAAU,IAAI,gBAAgB;AAEtC,kBAAgB;AAAA,IACd,MAAM;AAAA,IACN,aAAa;AAAA,IACb,YAAY;AAAA,IACZ,SAAS,OAAO,SAAyB;AACvC,UAAI;AACF,kBAAU,IAAI;AACd,eAAO,WAAW,KAAK,YAAY,MAAM,cACvC,KAAK,YAAY,WAAW,IAAI,KAAK,GACvC,gBAAgB,KAAK,SAAS;AAAA,MAChC,SAAS,KAAK;AACZ,eAAO,4BACL,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CACjD;AAAA,MACF;AAAA,IACF;AAAA,EACF,CAAC;AACH;","names":[]}
|