@appilots/web-sdk 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 ADDED
@@ -0,0 +1,216 @@
1
+ # @appilots/web-sdk
2
+
3
+ The Appilots client for web applications.
4
+
5
+ It observes the page through the **DOM and its accessibility tree** —
6
+ never through a framework's internals — so the same runtime drives a
7
+ React, Vue, Svelte, or hand-written app without knowing which it is.
8
+
9
+ The agent loop itself is not here. Turns, continuation, recovery
10
+ counters and the confirm gate live in `@appilots/client-core` and are
11
+ shared with the React Native SDK. This package supplies the two things
12
+ that cannot be shared: **eyes** (`captureSnapshot`) and **hands**
13
+ (`executeAction`), plus a navigation adapter that maps a URL onto the
14
+ agent contract's `navigationState`.
15
+
16
+ ## Why DOM and ARIA, not React internals
17
+
18
+ The React Native SDK reads React's Fiber tree. That is the right call
19
+ there — RN has no DOM and Fiber is the only complete view of what is
20
+ mounted. On the web it would be the wrong call:
21
+
22
+ - **It would only ever work for React.** The point of a web client is
23
+ to work for any app. The DOM is the one tree every framework agrees on.
24
+ - **Fiber is a private API.** It has no compatibility guarantee, differs
25
+ between development and production builds, and has changed shape
26
+ across React versions.
27
+ - **The accessibility tree already answers our questions.** "What is
28
+ this control, what is it called, is it disabled, is it selected, is it
29
+ behind a modal" are exactly what ARIA encodes. An app that is
30
+ accessible is automatically legible to the agent, and an app that is
31
+ not gets a concrete reason to improve.
32
+
33
+ The output shape is identical to the RN walker's (`ScreenSnapshot`), and
34
+ the interaction-graph derivation is literally the same shared code, so
35
+ equivalent content yields identical `el:` ids on both platforms.
36
+
37
+ ## Quick start (React)
38
+
39
+ ```tsx
40
+ import { AppilotsWebProvider, AppilotsChatFab } from '@appilots/web-sdk/react';
41
+ import { useNavigate } from 'react-router-dom';
42
+
43
+ function Root() {
44
+ const navigate = useNavigate();
45
+ return (
46
+ <AppilotsWebProvider
47
+ projectId="proj_..."
48
+ apiKey="ak_..."
49
+ apiBaseUrl="https://your-api/api/v1"
50
+ routes={[
51
+ { name: 'VehicleList', path: '/vehicles' },
52
+ { name: 'VehicleDetails', path: '/vehicles/:id' },
53
+ ]}
54
+ screens={[
55
+ {
56
+ name: 'VehicleList',
57
+ actions: [{ id: 'delete-vehicle', label: 'Excluir veículo', requiresConfirmation: true }],
58
+ },
59
+ ]}
60
+ navigate={(path, options) => navigate(path, { replace: options?.replace })}
61
+ >
62
+ <App />
63
+ <AppilotsChatFab />
64
+ </AppilotsWebProvider>
65
+ );
66
+ }
67
+ ```
68
+
69
+ `AppilotsChatFab` adds a floating button and chat panel to your site. It starts
70
+ closed, opens with focus in the composer, and closes with the button, the
71
+ header close control or Escape. Closing preserves the conversation, draft,
72
+ pending approvals and human support; it does not stop an active response.
73
+ Use “Stop response” or “New conversation” for that.
74
+
75
+ ```tsx
76
+ <AppilotsChatFab
77
+ locale="pt-BR"
78
+ title="Assistente"
79
+ position="bottom-right"
80
+ accentColor="#285b45"
81
+ suggestedPrompts={['O que posso fazer por aqui?']}
82
+ />
83
+ ```
84
+
85
+ Mount it inside `AppilotsWebProvider`. The React binding requires `react` and
86
+ `react-dom` 18 or newer. The widget renders in a portal under `document.body`
87
+ after hydration, so host containers with `overflow` or transforms cannot clip
88
+ it. Its entire UI is excluded from the agent's observations. No CSS import or
89
+ application-owned open/close state is needed.
90
+
91
+ `position` also accepts `bottom-left`; `defaultOpen` defaults to `false`.
92
+ `zIndex` defaults to `1000`: keep your app's blocking dialogs above the widget,
93
+ or lower this value to fit the site's stacking order. The panel adapts to the
94
+ viewport and safe area. `style` customizes the chat panel, as with `AppilotsChat`.
95
+
96
+ For a chat embedded in your own layout, keep using `AppilotsChat`:
97
+
98
+ `AppilotsChat` includes message bubbles, timestamps, Markdown (the same subset
99
+ as mobile), multiline input, Enter to send / Shift+Enter for a new line,
100
+ stop response, new conversation, and inline action confirmations. It follows
101
+ new messages while you are at the bottom and lets you return to the latest
102
+ messages when reading older replies.
103
+
104
+ ```tsx
105
+ <AppilotsChat
106
+ locale="pt-BR"
107
+ title="Assistente"
108
+ placeholder="Escreva uma mensagem…"
109
+ suggestedPrompts={['O que posso fazer por aqui?']}
110
+ style={{ height: 600 }}
111
+ />
112
+ ```
113
+
114
+ `locale` supports `en` (default) and `pt-BR`. Suggestions prefill the composer;
115
+ they never send automatically. Markdown supports headings, lists, bold, italic,
116
+ inline/fenced code and HTTP(S) links. HTML is rendered as text.
117
+
118
+ “Talk to a person” uses the existing escalation API: waiting/connected status,
119
+ operator replies, and user messages all stay in the same transcript. The API
120
+ and an operator in the dashboard must be available for a person to answer.
121
+ Starting a new conversation abandons open human support. Stop and handoff
122
+ cancel pending actions and ignore late model replies; an action already
123
+ executing is allowed to settle.
124
+
125
+ Styles are scoped and included with the component; no CSS import is required.
126
+ For a custom surface, use `useAppilotsChat()` / `useAppilotsActions()`.
127
+
128
+ Not using React? `AppilotsWebRuntime` is a plain class:
129
+
130
+ ```ts
131
+ const runtime = new AppilotsWebRuntime({ projectId, apiKey, routes, screens, navigate });
132
+ await runtime.chat.sendMessage('Cadastre um Corolla 2023');
133
+ ```
134
+
135
+ ## Markup conventions
136
+
137
+ None of these are required — the walker falls back to `data-testid`,
138
+ `id`, `name`, `aria-label` and the accessible name. They are how you get
139
+ _precision_ when defaults are ambiguous.
140
+
141
+ | Attribute | Purpose |
142
+ | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
143
+ | `data-appilots-id` | The agent-facing id, checked before everything else. Use it when `data-testid` is already spoken for. |
144
+ | `data-appilots-action` | Points a control at a **declared screen action**. Essential for list rows: each row's delete button needs a unique id to be targetable, but all of them are the same declared action and must inherit its `requiresConfirmation`. |
145
+ | `data-appilots-skip` | Excludes a subtree from observation entirely. |
146
+ | `data-appilots-sensitive="true"` | Masks a field's value even if it isn't `type="password"`. |
147
+ | `data-appilots-list-id` | Marks a list container and names it. |
148
+ | `data-appilots-list-item` | Marks a row. |
149
+ | `data-appilots-item-key` | A row's stable domain id. |
150
+ | `data-appilots-item-index` | A row's 0-based index in the backing data (for virtualized lists). |
151
+ | `data-appilots-item-count` | Total data-set size when the DOM only holds a page of it. |
152
+
153
+ The SDK's own chat UI carries `data-appilots-chat`, which the walker
154
+ treats as a skip marker — without it the agent would observe its own
155
+ transcript and try to press its own buttons.
156
+
157
+ ## What the agent sees
158
+
159
+ ```ts
160
+ const snapshot = captureSnapshot();
161
+ // { route, texts, inputs, buttons, toggles, sliders, lists,
162
+ // choiceGroups, elements, loading, modalOpen }
163
+ ```
164
+
165
+ Semantics worth knowing:
166
+
167
+ - **Classification is an else-if chain**, so an element is exactly one
168
+ of text/input/slider/toggle/button. Sliders are checked before
169
+ pressables so a range track never becomes a button.
170
+ - **List containers are not descended in the main pass.** A row's
171
+ content lives on that row, not flattened into the top-level arrays —
172
+ that association is the whole reason the shape exists.
173
+ - **Password and marked-sensitive values never leave the page.** They
174
+ are replaced with `<hidden>` before the snapshot is built.
175
+ - **A modal marks its contents `inModal`.** While one is open the
176
+ executor refuses to press anything behind it, because such a press
177
+ would silently do nothing.
178
+
179
+ ## Executing actions
180
+
181
+ `executeAction` handles all six action types with the same
182
+ `ActionDiagnose` categories as the RN executor, so server-side recovery
183
+ behaves identically.
184
+
185
+ The subtle one is `form_fill` against a **controlled React input**.
186
+ Assigning `element.value` does not work: React installs its own value
187
+ setter on the element instance and tracks the last value it wrote, so a
188
+ direct assignment is invisible to it and the app reverts the field on
189
+ the next render. The executor calls the _prototype's_ native setter
190
+ (which updates React's tracker) and then dispatches a bubbling `input`
191
+ event, which is what React's `onChange` actually listens for. The same
192
+ sequence satisfies Vue's `v-model` and plain listeners.
193
+
194
+ ## Navigation
195
+
196
+ `WebNavigationAdapter` maps the URL onto the contract: the path becomes
197
+ `activePath`, the matched route becomes `currentRouteName`, and route +
198
+ params become the signature the settle loop uses to detect a navigate to
199
+ the same route with different params.
200
+
201
+ Pass your router's `navigate` so agent-driven navigation stays
202
+ client-side. Without one it falls back to `history.pushState` plus a
203
+ synthetic `popstate`, which most routers observe.
204
+
205
+ Route matching prefers the **most specific** pattern, so `/vehicles/new`
206
+ resolves to `VehicleCreate` rather than `VehicleDetails` with
207
+ `id: 'new'`.
208
+
209
+ ## Testing
210
+
211
+ `pnpm --filter @appilots/web-sdk test` — unit tests for the walker,
212
+ locators, executor and navigation, plus integration tests that run the
213
+ whole loop against a mocked backend in a real DOM.
214
+
215
+ `pnpm qa:web:agent` — Playwright missions against `apps/example-web-app`
216
+ in a real browser.