Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 49 additions & 30 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,14 @@ export class MyConnector implements IConnector {
this.genUICallback = callback;
}

receiveComponentEvent(streamId: string, eventType: string, payload: unknown): void { }
// `opts` carries the interaction's routing metadata (`scope`, `component`) for
// protocols that model it; ignoring it is fine.
receiveComponentEvent(
streamId: string,
eventType: string,
payload: unknown,
opts?: GenUIEventOptions,
): void { }
}
```

Expand Down Expand Up @@ -288,54 +295,47 @@ MessageTypeRegistry.register("my-type", MyMessage);
## When Creating a New GenUI Component

1. Create `packages/genui/src/components/[Name].ts`
2. Extend `LitElement` (not ChatbotMixin — GenUI components are standalone)
3. Declare optional `GenUIComponentAPI` properties to receive injected methods
4. Self-register in `GenUIRegistry`
5. Add tests
2. Extend `GenUIElement` from `@chativa/core` (not ChatbotMixin — GenUI components are standalone)
3. Self-register in `GenUIRegistry`
4. Add tests

`GenUIElement` extends `ChativaElement` (i18n + auto re-render on locale switch) and implements the full
`GenUIComponentAPI` with working defaults, so `this.sendEvent(...)`, `this.listenEvent(...)` and
`this.tFn(...)` are always callable. `GenUIMessage` shadows them with message-scoped versions at mount
time. Do NOT redeclare those four as class fields — that shadows the defaults with `undefined`.

**Template:**
```ts
import { LitElement, html, css } from "lit";
import { html, css } from "lit";
import { customElement, property } from "lit/decorators.js";
import { GenUIElement } from "@chativa/core";
import { GenUIRegistry } from "../registry/GenUIRegistry";
import type { GenUIComponentAPI } from "../types";

@customElement("my-genui-widget")
export class MyGenUIWidget extends LitElement {
// Injected by GenUIMessage at render time
sendEvent?: GenUIComponentAPI["sendEvent"];
listenEvent?: GenUIComponentAPI["listenEvent"];
tFn?: GenUIComponentAPI["tFn"];
onLangChange?: GenUIComponentAPI["onLangChange"];
export class MyGenUIWidget extends GenUIElement {
static override styles = css`
:host { display: block; }
`;

@property({ type: String }) title = "";

private _unsubLang?: () => void;

connectedCallback() {
override connectedCallback() {
super.connectedCallback();
this._unsubLang = this.onLangChange?.(() => this.requestUpdate());
}

disconnectedCallback() {
super.disconnectedCallback();
this._unsubLang?.();
// Server-pushed event chunks targeting this component
this.listenEvent("widget_updated", (payload) => {
this.title = (payload as { title: string }).title;
});
}

static styles = css`
:host { display: block; }
`;

private _handleAction() {
this.sendEvent?.("widget_action", { title: this.title });
this.sendEvent("widget_action", { title: this.title });
}

render() {
const label = this.tFn?.("widget.submit", "Submit") ?? "Submit";
override render() {
return html`
<div>
<h3>${this.title}</h3>
<button @click=${this._handleAction}>${label}</button>
<button @click=${this._handleAction}>${this.tFn("widget.submit", "Submit")}</button>
</div>
`;
}
Expand All @@ -345,6 +345,25 @@ export class MyGenUIWidget extends LitElement {
GenUIRegistry.register("my-widget", MyGenUIWidget);
```

### Backend-authored components — `<chativa-html>`

When the markup should come from the backend instead of a compiled widget, stream the built-in
`GenUIHtmlElement` (registered as both `genui-html` and `html`):

```json
{ "type": "ui", "component": "html",
"props": { "html": "<button data-event='track' data-payload='{\"id\":1}'>Track</button>",
"css": "button { border-radius: 8px; }" } }
```

Markup is sanitized by default (`sanitizeHtml` — drops `<script>`/`<iframe>`, `on*` attributes and
`javascript:` URLs); the `unsafe` prop opts out. Clicks and form submits round-trip to
`IConnector.receiveComponentEvent`. Three attributes trigger one, and which one you write says who
the interaction is addressed to: `component-event` (scope `"component"` — the widget's own
conversation with the backend node that mounted it), `mekik-event` (scope `"graph"` — the app),
`data-event` (no scope — the backend decides). Most specific wins. Outside a `GenUIMessage` the
element dispatches a bubbling, composed `genui-component-event` instead.

---

## Testing Requirements
Expand Down
1 change: 1 addition & 0 deletions docs/genui/built-in.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Auto-registered by importing `@chativa/genui`.
| `genui-steps` | Vertical step list | `steps: { label, status, description? }[]` | — |
| `genui-image-gallery` | Grid of images | `columns?, images: { src, alt?, caption? }[]` | — |
| `genui-appointment-form` | Specialised appointment form | `fields[]` | `form_submit` |
| `genui-html` / `html` | Backend-authored raw markup ([details](./custom-component.md)) | `html: string, css?: string, unsafe?: boolean` | any `component-event` / `mekik-event` / `data-event` on a clicked element or submitted `<form>` |

## Visual reference

Expand Down
Loading
Loading