@gnomondigital/nebulas-kit-react
v0.10.4
Published
React components for A2A chat. Requires `@gnomondigital/nebulas-kit-core` and a server proxy.
Readme
@gnomondigital/nebulas-kit-react
React components for A2A chat. Requires @gnomondigital/nebulas-kit-core and a server proxy.
Install
npm install @gnomondigital/nebulas-kit-react @gnomondigital/nebulas-kit-coreUsage
import { A2AChat } from "@gnomondigital/nebulas-kit-react";
export default function ChatPage() {
return (
<A2AChat
a2aEndpoint="/api/a2a"
configEndpoint="/api/a2a/config"
user={{ name: "John", picture: "https://..." }}
labels={{
placeholder: "Ask anything...",
newChat: "New chat",
howCanIHelp: "How can I help?",
askAbout: "Ask me anything.",
}}
suggestedPrompts={[
"Find top 5 Python developers",
"Summarize the pipeline",
]}
/>
);
}File attachments
The composer’s attach-file control (paperclip) is hidden by default. Set showAttachFileButton on A2AChat or A2AChatFloating to show it and allow PDF, Word, Markdown, and plain-text uploads (same accepted types as the underlying input).
<A2AChat
a2aEndpoint="/api/a2a"
configEndpoint="/api/a2a/config"
showAttachFileButton
/>Auth0 session (cookies)
Same-origin fetch sends session cookies to your /api/a2a route. Optionally pass user for avatars. No getAuthHeaders required when the proxy resolves the user on the server.
Env ROP (server-side service user)
When your API uses createA2AProxyHandlerWithServiceAuth, the browser does not send a token. Use A2AChat or A2AChatFloating with only a2aEndpoint and configEndpoint.
Resource Owner Password (browser token)
Use getAuthHeaders so config and chat requests include Authorization: Bearer …. Pass authDependency (e.g. the token string) so config reloads after login without relying on a stable function reference.
import { A2AChat, useA2AAuth } from "@gnomondigital/nebulas-kit-react";
export default function ChatPage() {
const { token, login, logout } = useA2AAuth({ authEndpoint: "/api/a2a/auth" });
if (!token) {
return (
<form
onSubmit={(e) => {
e.preventDefault();
const form = e.target as HTMLFormElement;
login(
(form.elements.namedItem("username") as HTMLInputElement).value,
(form.elements.namedItem("password") as HTMLInputElement).value
);
}}
>
<input name="username" />
<input name="password" type="password" />
<button type="submit">Login</button>
</form>
);
}
return (
<>
<button type="button" onClick={logout}>
Log out
</button>
<A2AChat
a2aEndpoint="/api/a2a"
configEndpoint="/api/a2a/config"
getAuthHeaders={() => ({ Authorization: `Bearer ${token}` })}
authDependency={token}
/>
</>
);
}Floating widget
Add once in a root layout: fixed chat button (bottom-right), panel with the same chat UI.
Auth0 session — optional user from your auth library; endpoints use your session proxy.
import { A2AChatFloating } from "@gnomondigital/nebulas-kit-react";
export function RootChat() {
return (
<A2AChatFloating
a2aEndpoint="/api/a2a"
configEndpoint="/api/a2a/config"
nebulasBaseUrl="/api/nebulas"
user={{ name: "Jane", picture: "https://..." }}
panelTitle="Support"
suggestedPrompts={["What can you help with?"]}
/>
);
}Env ROP — no extra client props beyond endpoints (and optional suggestedPrompts / labels).
<A2AChatFloating a2aEndpoint="/api/a2a" configEndpoint="/api/a2a/config" />Browser ROP — built-in sign-in form in the panel; wire your token endpoint at authEndpoint.
<A2AChatFloating
authEndpoint="/api/a2a/auth"
a2aEndpoint="/api/a2a"
configEndpoint="/api/a2a/config"
/>A2AChatFloating renders into document.body by default (usePortal) so parent stacking contexts do not clip it. Set usePortal={false} to keep it in the React tree.
Requirements
- Tailwind CSS (configure in your app)
- Server proxy (Next.js or Express) — see
@gnomondigital/nebulas-kit-core
Surfaces (NebulasSurface)
A surface is a widget authored in Nebulas — most often a form. NebulasSurface
renders a stored one on its own, outside any chat, and submits it back to the
backend where its declared handler runs.
import { NebulasSurface } from "@gnomondigital/nebulas-kit-react";
<NebulasSurface
surfaceName="contact_sales"
onResult={(result) => console.log(result.status, result.message)}
/>Address it by surfaceName or by surfaceId — one or the other, never both.
A name is what its author knows, and it survives moving between
environments: staging and production hold the same contact_sales form under
different ids. It costs one lookup before the first render, done by the Kit's
backend so the definition never reaches the browser. Submissions then use the
id the render answered with, so a form is looked up once, not once per click.
An id is exact and skips the lookup. It is the definition id, not an A2UI instance id — the instance is created by the render call.
<NebulasSurface surfaceId="66b0c1f2e4b0a1c2d3e4f5a6" />Either way it needs the surface handler mounted at /api/nebulas-surfaces
(see @gnomondigital/nebulas-kit-core); point endpoint elsewhere if you mount
it on another path.
| Prop | Purpose |
|---|---|
| inputs | Values for the definition's declared inputs, including hidden ones (utm_source, a tenant id, …) |
| trackingParams | Attribution parameters to lift off the page URL and pass as hidden inputs (see below) |
| searchParams | Where to read those from, when not window.location.search |
| data | Prefill merged over the initial data model |
| theme | brand (default) applies the surface's brand tokens; product leaves your page's theme in charge |
| sessionId | Conversation id, when the form belongs to one — the side effect is then recorded in that transcript |
| onResult | Every action's raw backend result |
| onChatMessage | A chat handler produces a message meant for a conversation; standing alone, the host decides what to do with it |
| t | Resolves i18n:-prefixed labels not covered by the surface's own labels; keys pass through by default |
| locale | Which language of the surface's own labels to read (default "en") |
| resolveImageSrc | Turns an image src into a loadable URL (e.g. neb:// artifact ids) |
| successDialog | Shows a dialog instead of the inline note on a successful submit, then reloads to a blank form (see below) |
| captcha | Gates a submit behind a solved challenge (see below) |
Validation declared on the surface is checked in the browser before a submit and
again by the server before any handler runs. What comes back lands in place: a
refresh patches the data, a follow-up surface renders beneath, field errors
appear under their fields, and a one-shot handler (chat, script,
datatable) leaves the form read-only.
Hidden UTM / attribution parameters
A lead should carry where it came from, without the visitor seeing a field for
it. Point trackingParams at the keys to lift from the page URL:
// ?utm_source=newsletter&utm_campaign=spring
<NebulasSurface surfaceId="66b0…" trackingParams /> // the five standard utm_* keys
<NebulasSurface surfaceId="66b0…" trackingParams={["utm_source", "gclid", "ref"]} />They are read once on mount and sent as inputs to the render call, so the
surface's data can bind them into the submitted model. Only parameters
actually present in the URL are sent, and an explicit inputs value for the
same key wins.
Each key must be declared as an input on the surface definition, normally
with hidden: true so an agent never invents it. The backend rejects an input
it does not declare — that is why this is opt-in rather than automatic: a form
declaring no utm_* input would otherwise fail to render on any page whose URL
happened to carry one. A misconfiguration shows the backend's own message,
which names the unknown keys and the declared ones.
When the host owns the query string — Next's useSearchParams(), a router
location, or a landing URL captured before the visitor navigated to the form —
pass it explicitly:
const params = useSearchParams();
<NebulasSurface surfaceId="66b0…" trackingParams searchParams={params.toString()} />A dialog on success, instead of the inline note
By default a successful one-shot submit (chat, script, datatable) leaves
the form disabled with a note underneath. Pass successDialog to show a modal
instead, then reload to a blank form once it closes:
<NebulasSurface
surfaceName="contact_form"
successDialog={{
title: "Thank you!",
message: "We've received your inquiry.",
}}
/>An error note (a rejected submission) still shows inline — only the success note is replaced, since the dialog now carries that role.
Captcha
NebulasSurface does not bundle a captcha library — pass captcha with a
render function that mounts your own widget (reCAPTCHA, Turnstile, hCaptcha,
…) and calls back with the verified token:
<NebulasSurface
surfaceName="contact_form"
captcha={{
render: (onVerify) => (
<Turnstile siteKey="…" onSuccess={onVerify} onExpire={() => onVerify(null)} />
),
}}
/>A submit attempted before the token arrives is blocked client-side (shown via
labels.captchaRequired). Once solved, the token rides along with the action
as a JSON Pointer into the submitted data — /captcha_token by default,
override with dataPath — independently of whether the surface sends its full
data model, so the backend can validate it. The token is single-use: it clears
once a submit succeeds (a rejected submission keeps it, so fixing a field does
not mean solving the challenge again), and the widget is expected to remount
and ask again after a reload.
Rendering it yourself
SurfaceRenderer (a SurfaceState in, actions out), useSurfaceActions,
useSurfaceModel and renderCatalogComponent are exported for hosts that fetch
and lay out surfaces themselves.
Optional dependency
Chart components and fenced ```vega blocks need vega-embed, an optional
peer. Without it the chart renders as nothing and the rest of the surface is
unaffected.
