@agent-html/react 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +20 -0
  2. package/src/index.tsx +264 -0
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@agent-html/react",
3
+ "version": "0.0.1",
4
+ "license": "Apache-2.0",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "files": [
8
+ "src",
9
+ "!src/**/*.test.ts",
10
+ "!src/**/*.test.tsx",
11
+ "!src/**/*.spec.ts",
12
+ "!src/**/*.spec.tsx"
13
+ ],
14
+ "exports": {
15
+ ".": "./src/index.tsx"
16
+ },
17
+ "peerDependencies": {
18
+ "react": "^19.0.0"
19
+ }
20
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,264 @@
1
+ import * as React from "react"
2
+ import type { ReactNode, RefObject } from "react"
3
+
4
+ export const artifactInteractionEventName = "agent-html:state-change"
5
+
6
+ export type ArtifactStateChangeKind =
7
+ | "set"
8
+ | "toggle"
9
+ | "select"
10
+ | "open"
11
+ | "action"
12
+ | "move"
13
+ | "resize"
14
+ | (string & {})
15
+
16
+ export type ArtifactStateChange = {
17
+ after: unknown
18
+ before: unknown
19
+ blockId?: string
20
+ component: string
21
+ controlId: string
22
+ kind: ArtifactStateChangeKind
23
+ label?: string
24
+ semantic?: string
25
+ timestamp: number
26
+ }
27
+
28
+ export type ArtifactStateChangeInput = Omit<
29
+ ArtifactStateChange,
30
+ "timestamp"
31
+ > & {
32
+ timestamp?: number
33
+ }
34
+
35
+ export type ArtifactInteractionSnapshot = {
36
+ blockId?: string
37
+ currentState: Record<string, unknown>
38
+ recentChanges: ArtifactStateChange[]
39
+ }
40
+
41
+ export type ArtifactInteractionRuntime = {
42
+ emitChange: (change: ArtifactStateChangeInput) => void
43
+ getSnapshot: (blockId?: string) => ArtifactInteractionSnapshot
44
+ }
45
+
46
+ const emptyInteractionSnapshot: ArtifactInteractionSnapshot = {
47
+ currentState: {},
48
+ recentChanges: [],
49
+ }
50
+
51
+ const noopInteractionRuntime: ArtifactInteractionRuntime = {
52
+ emitChange: dispatchArtifactStateChange,
53
+ getSnapshot: () => emptyInteractionSnapshot,
54
+ }
55
+
56
+ const ArtifactInteractionContext =
57
+ React.createContext<ArtifactInteractionRuntime>(noopInteractionRuntime)
58
+
59
+
60
+ export type ArtifactProps = {
61
+ children?: ReactNode
62
+ title: string
63
+ }
64
+
65
+ export type BlockProps = {
66
+ children?: ReactNode
67
+ id: string
68
+ title?: string
69
+ }
70
+
71
+ export function createArtifactStateChange(
72
+ change: ArtifactStateChangeInput
73
+ ): ArtifactStateChange {
74
+ return {
75
+ ...change,
76
+ timestamp: change.timestamp ?? Date.now(),
77
+ }
78
+ }
79
+
80
+ export function dispatchArtifactStateChange(change: ArtifactStateChangeInput) {
81
+ const detail = createArtifactStateChange(change)
82
+
83
+ if (typeof window === "undefined") {
84
+ return detail
85
+ }
86
+
87
+ window.dispatchEvent(
88
+ new CustomEvent<ArtifactStateChange>(artifactInteractionEventName, {
89
+ detail,
90
+ })
91
+ )
92
+
93
+ return detail
94
+ }
95
+
96
+ export function InteractionProvider({
97
+ children,
98
+ onChange,
99
+ }: {
100
+ children?: ReactNode
101
+ onChange?: (change: ArtifactStateChange) => void
102
+ }) {
103
+ const snapshotsRef = React.useRef(new Map<string, ArtifactInteractionSnapshot>())
104
+
105
+ const emitChange = React.useCallback(
106
+ (input: ArtifactStateChangeInput) => {
107
+ const change = createArtifactStateChange(input)
108
+ const key = change.blockId ?? ""
109
+ const previous = snapshotsRef.current.get(key) ?? {
110
+ blockId: change.blockId,
111
+ currentState: {},
112
+ recentChanges: [],
113
+ }
114
+
115
+ snapshotsRef.current.set(key, {
116
+ blockId: change.blockId,
117
+ currentState: {
118
+ ...previous.currentState,
119
+ [change.controlId]: change.after,
120
+ },
121
+ recentChanges: [...previous.recentChanges, change].slice(-20),
122
+ })
123
+
124
+ onChange?.(change)
125
+ dispatchArtifactStateChange(change)
126
+ },
127
+ [onChange]
128
+ )
129
+
130
+ const getSnapshot = React.useCallback((blockId?: string) => {
131
+ return snapshotsRef.current.get(blockId ?? "") ?? emptyInteractionSnapshot
132
+ }, [])
133
+
134
+ const runtime = React.useMemo(
135
+ () => ({
136
+ emitChange,
137
+ getSnapshot,
138
+ }),
139
+ [emitChange, getSnapshot]
140
+ )
141
+
142
+ return (
143
+ <ArtifactInteractionContext.Provider value={runtime}>
144
+ {children}
145
+ </ArtifactInteractionContext.Provider>
146
+ )
147
+ }
148
+
149
+ export function useArtifactInteraction() {
150
+ return React.useContext(ArtifactInteractionContext)
151
+ }
152
+
153
+ export function findNearestBlockId(element: Element | null | undefined) {
154
+ return (
155
+ element
156
+ ?.closest("[data-agent-html-block='true']")
157
+ ?.getAttribute("data-agent-html-block-id") ?? undefined
158
+ )
159
+ }
160
+
161
+ export function useNearestBlockId<T extends Element>(
162
+ ref: RefObject<T | null>
163
+ ) {
164
+ const [blockId, setBlockId] = React.useState<string | undefined>()
165
+
166
+ React.useLayoutEffect(() => {
167
+ setBlockId(findNearestBlockId(ref.current))
168
+ })
169
+
170
+ return blockId
171
+ }
172
+
173
+ export function useEmitArtifactStateChange({
174
+ blockId,
175
+ elementRef,
176
+ }: {
177
+ blockId?: string
178
+ elementRef?: RefObject<Element | null>
179
+ } = {}) {
180
+ const runtime = useArtifactInteraction()
181
+
182
+ return React.useCallback(
183
+ (change: Omit<ArtifactStateChangeInput, "blockId"> & { blockId?: string }) => {
184
+ runtime.emitChange({
185
+ ...change,
186
+ blockId: change.blockId ?? blockId ?? findNearestBlockId(elementRef?.current),
187
+ })
188
+ },
189
+ [blockId, elementRef, runtime]
190
+ )
191
+ }
192
+
193
+ export function useInstrumentedValueChange<T>({
194
+ blockId,
195
+ component,
196
+ controlId,
197
+ elementRef,
198
+ kind = "set",
199
+ label,
200
+ onChange,
201
+ semantic,
202
+ value,
203
+ }: {
204
+ blockId?: string
205
+ component: string
206
+ controlId: string
207
+ elementRef?: RefObject<Element | null>
208
+ kind?: ArtifactStateChangeKind
209
+ label?: string
210
+ onChange?: (value: T) => void
211
+ semantic?: string
212
+ value: T
213
+ }) {
214
+ const emitChange = useEmitArtifactStateChange({ blockId, elementRef })
215
+ const previousValueRef = React.useRef(value)
216
+
217
+ React.useEffect(() => {
218
+ previousValueRef.current = value
219
+ }, [value])
220
+
221
+ return React.useCallback(
222
+ (nextValue: T) => {
223
+ const before = previousValueRef.current
224
+ previousValueRef.current = nextValue
225
+ onChange?.(nextValue)
226
+ emitChange({
227
+ after: nextValue,
228
+ before,
229
+ component,
230
+ controlId,
231
+ kind,
232
+ label,
233
+ semantic,
234
+ })
235
+ },
236
+ [component, controlId, emitChange, kind, label, onChange, semantic]
237
+ )
238
+ }
239
+
240
+ export const useInstrumentedCheckedChange = useInstrumentedValueChange
241
+
242
+ export function Artifact({ children, title }: ArtifactProps) {
243
+ return (
244
+ <main
245
+ data-agent-html-artifact="true"
246
+ data-agent-html-title={title}
247
+ className="agent-html-artifact"
248
+ >
249
+ {children}
250
+ </main>
251
+ )
252
+ }
253
+
254
+ export function Block({ children, id, title }: BlockProps) {
255
+ return (
256
+ <section
257
+ data-agent-html-block="true"
258
+ data-agent-html-block-id={id}
259
+ data-agent-html-block-title={title ?? id}
260
+ >
261
+ {children}
262
+ </section>
263
+ )
264
+ }