chore: initial import of standalone agentscope project
Some checks failed
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_en (ubuntu-latest, 3.10) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_zh (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.12) (push) Has been cancelled
Some checks failed
Pre-commit / run (ubuntu-latest) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_en (ubuntu-latest, 3.10) (push) Has been cancelled
Deploy Sphinx documentation to Pages / build_zh (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (macos-15, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (ubuntu-latest, 3.12) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.10) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.11) (push) Has been cancelled
Python Unittest Coverage / test (windows-latest, 3.12) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
# A2UI Generator
|
||||
|
||||
This is a UI to generate and visualize A2UI responses.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. [nodejs](https://nodejs.org/en)
|
||||
|
||||
## Running
|
||||
|
||||
This sample depends on the Lit renderer. Before running this sample, you need to build the renderer.
|
||||
|
||||
1. **Build the renderer:**
|
||||
```bash
|
||||
cd ../../../renderers/lit
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
2. **Run this sample:**
|
||||
```bash
|
||||
cd - # back to the sample directory
|
||||
npm install
|
||||
```
|
||||
|
||||
3. **Run the servers:**
|
||||
- Run the [A2A server](../../../agent/adk/contact_lookup/)
|
||||
- Run the dev server: `npm run dev`
|
||||
|
||||
After starting the dev server, you can open http://localhost:5173/ to view the sample.
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { v0_8 } from "@a2ui/lit";
|
||||
import { registerContactComponents } from "./ui/custom-components/register-components.js";
|
||||
type A2TextPayload = {
|
||||
kind: "text";
|
||||
text: string;
|
||||
};
|
||||
|
||||
type A2DataPayload = {
|
||||
kind: "data";
|
||||
data: v0_8.Types.ServerToClientMessage;
|
||||
};
|
||||
|
||||
type A2AServerPayload =
|
||||
| Array<A2DataPayload | A2TextPayload>
|
||||
| { error: string };
|
||||
|
||||
export class A2UIClient {
|
||||
#ready: Promise<void> = Promise.resolve();
|
||||
get ready() {
|
||||
return this.#ready;
|
||||
}
|
||||
|
||||
async send(
|
||||
message: v0_8.Types.A2UIClientEventMessage
|
||||
): Promise<v0_8.Types.ServerToClientMessage[]> {
|
||||
const response = await fetch("/a2a", {
|
||||
body: JSON.stringify(message),
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = (await response.json()) as A2AServerPayload;
|
||||
const messages: v0_8.Types.ServerToClientMessage[] = [];
|
||||
if ("error" in data) {
|
||||
throw new Error(data.error);
|
||||
} else {
|
||||
for (const item of data) {
|
||||
if (item.kind === "text") continue;
|
||||
messages.push(item.data);
|
||||
}
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
const error = (await response.json()) as { error: string };
|
||||
throw new Error(error.error);
|
||||
}
|
||||
}
|
||||
registerContactComponents();
|
||||
376
examples/agent/a2ui_agent/samples/client/lit/contact/contact.ts
Normal file
376
examples/agent/a2ui_agent/samples/client/lit/contact/contact.ts
Normal file
@@ -0,0 +1,376 @@
|
||||
/*
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { SignalWatcher } from "@lit-labs/signals";
|
||||
import { provide } from "@lit/context";
|
||||
import {
|
||||
LitElement,
|
||||
html,
|
||||
css,
|
||||
nothing,
|
||||
HTMLTemplateResult,
|
||||
unsafeCSS,
|
||||
} from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { theme as uiTheme } from "./theme/theme.js";
|
||||
import { A2UIClient } from "./client.js";
|
||||
import {
|
||||
SnackbarAction,
|
||||
SnackbarMessage,
|
||||
SnackbarUUID,
|
||||
SnackType,
|
||||
} from "./types/types.js";
|
||||
import { type Snackbar } from "./ui/snackbar.js";
|
||||
import { repeat } from "lit/directives/repeat.js";
|
||||
import { v0_8 } from "@a2ui/lit";
|
||||
import * as UI from "@a2ui/lit/ui";
|
||||
|
||||
// Demo elements.
|
||||
import "./ui/ui.js";
|
||||
import { registerContactComponents } from "./ui/custom-components/register-components.js";
|
||||
|
||||
// Register custom components for the contact app
|
||||
registerContactComponents();
|
||||
|
||||
@customElement("a2ui-contact")
|
||||
export class A2UIContactFinder extends SignalWatcher(LitElement) {
|
||||
@provide({ context: UI.Context.themeContext })
|
||||
accessor theme: v0_8.Types.Theme = uiTheme;
|
||||
|
||||
@state()
|
||||
accessor #requesting = false;
|
||||
|
||||
@state()
|
||||
accessor #error: string | null = null;
|
||||
|
||||
@state()
|
||||
accessor #lastMessages: v0_8.Types.ServerToClientMessage[] = [];
|
||||
|
||||
static styles = [
|
||||
unsafeCSS(v0_8.Styles.structuralStyles),
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
#surfaces {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: var(--bb-grid-size-3) 0;
|
||||
animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 0.3s backwards;
|
||||
|
||||
& a2ui-surface {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
padding: 16px 0;
|
||||
animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 1s backwards;
|
||||
|
||||
& > div {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
& > input {
|
||||
display: block;
|
||||
flex: 1;
|
||||
border-radius: 32px;
|
||||
padding: 16px 24px;
|
||||
border: 1px solid var(--p-60);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
& > button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--p-40);
|
||||
color: var(--n-100);
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 32px;
|
||||
opacity: 0.5;
|
||||
|
||||
&:not([disabled]) {
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rotate {
|
||||
animation: rotate 1s linear infinite;
|
||||
}
|
||||
|
||||
.pending {
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 0.3s backwards;
|
||||
|
||||
& .g-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--e-40);
|
||||
background-color: var(--e-95);
|
||||
border: 1px solid var(--e-80);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
rotate: 0deg;
|
||||
}
|
||||
|
||||
to {
|
||||
rotate: 360deg;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
#processor = v0_8.Data.createSignalA2uiMessageProcessor();
|
||||
#a2uiClient = new A2UIClient();
|
||||
#snackbar: Snackbar | undefined = undefined;
|
||||
#pendingSnackbarMessages: Array<{
|
||||
message: SnackbarMessage;
|
||||
replaceAll: boolean;
|
||||
}> = [];
|
||||
|
||||
render() {
|
||||
return [
|
||||
this.#maybeRenderForm(),
|
||||
this.#maybeRenderData(),
|
||||
this.#maybeRenderError(),
|
||||
];
|
||||
}
|
||||
|
||||
#maybeRenderError() {
|
||||
if (!this.#error) return nothing;
|
||||
|
||||
return html`<div class="error">${this.#error}</div>`;
|
||||
}
|
||||
|
||||
#maybeRenderForm() {
|
||||
if (this.#requesting) return nothing;
|
||||
if (this.#lastMessages.length > 0) return nothing;
|
||||
return html`<form
|
||||
@submit=${async (evt: Event) => {
|
||||
evt.preventDefault();
|
||||
if (!(evt.target instanceof HTMLFormElement)) {
|
||||
return;
|
||||
}
|
||||
const data = new FormData(evt.target);
|
||||
const body = data.get("body") ?? null;
|
||||
if (!body) {
|
||||
return;
|
||||
}
|
||||
const message = body as v0_8.Types.A2UIClientEventMessage;
|
||||
await this.#sendAndProcessMessage(message);
|
||||
}}
|
||||
>
|
||||
<h1 class="typography-f-sf typography-v-r typography-w-400 color-c-p30">
|
||||
Contact Finder
|
||||
</h1>
|
||||
<div>
|
||||
<input
|
||||
required
|
||||
value="Casey Smith"
|
||||
autocomplete="off"
|
||||
id="body"
|
||||
name="body"
|
||||
type="text"
|
||||
?disabled=${this.#requesting}
|
||||
/>
|
||||
<button type="submit" ?disabled=${this.#requesting}>
|
||||
<span class="g-icon filled-heavy">send</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>`;
|
||||
}
|
||||
|
||||
#maybeRenderData() {
|
||||
if (this.#requesting) {
|
||||
return html` <div class="pending">
|
||||
<span class="g-icon filled-heavy rotate">progress_activity</span>
|
||||
Awaiting an answer...
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const surfaces = this.#processor.getSurfaces();
|
||||
if (surfaces.size === 0) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<section id="surfaces">
|
||||
${repeat(
|
||||
this.#processor.getSurfaces(),
|
||||
([surfaceId]) => surfaceId,
|
||||
([surfaceId, surface]) => {
|
||||
return html`<a2ui-surface
|
||||
@a2uiaction=${async (
|
||||
evt: v0_8.Events.StateEvent<"a2ui.action">
|
||||
) => {
|
||||
const [target] = evt.composedPath();
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context: v0_8.Types.A2UIClientEventMessage["userAction"]["context"] =
|
||||
{};
|
||||
if (evt.detail.action.context) {
|
||||
const srcContext = evt.detail.action.context;
|
||||
for (const item of srcContext) {
|
||||
if (item.value.literalBoolean) {
|
||||
context[item.key] = item.value.literalBoolean;
|
||||
} else if (item.value.literalNumber) {
|
||||
context[item.key] = item.value.literalNumber;
|
||||
} else if (item.value.literalString) {
|
||||
context[item.key] = item.value.literalString;
|
||||
} else if (item.value.path) {
|
||||
const path = this.#processor.resolvePath(
|
||||
item.value.path,
|
||||
evt.detail.dataContextPath
|
||||
);
|
||||
const value = this.#processor.getData(
|
||||
evt.detail.sourceComponent,
|
||||
path,
|
||||
surfaceId
|
||||
);
|
||||
context[item.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const message: v0_8.Types.A2UIClientEventMessage = {
|
||||
userAction: {
|
||||
surfaceId: surfaceId,
|
||||
name: evt.detail.action.name,
|
||||
sourceComponentId: target.id,
|
||||
timestamp: new Date().toISOString(),
|
||||
context,
|
||||
},
|
||||
};
|
||||
|
||||
await this.#sendAndProcessMessage(message);
|
||||
}}
|
||||
.surfaceId=${surfaceId}
|
||||
.surface=${surface}
|
||||
.processor=${this.#processor}
|
||||
></a2-uisurface>`;
|
||||
}
|
||||
)}
|
||||
</section>`;
|
||||
}
|
||||
|
||||
async #sendAndProcessMessage(request) {
|
||||
const messages = await this.#sendMessage(request);
|
||||
|
||||
this.#lastMessages = messages;
|
||||
this.#processor.clearSurfaces();
|
||||
this.#processor.processMessages(messages);
|
||||
}
|
||||
|
||||
async #sendMessage(
|
||||
message: v0_8.Types.A2UIClientEventMessage
|
||||
): Promise<v0_8.Types.ServerToClientMessage[]> {
|
||||
try {
|
||||
this.#requesting = true;
|
||||
const response = this.#a2uiClient.send(message);
|
||||
await response;
|
||||
this.#requesting = false;
|
||||
|
||||
return response;
|
||||
} catch (err) {
|
||||
this.snackbar(err as string, SnackType.ERROR);
|
||||
} finally {
|
||||
this.#requesting = false;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
snackbar(
|
||||
message: string | HTMLTemplateResult,
|
||||
type: SnackType,
|
||||
actions: SnackbarAction[] = [],
|
||||
persistent = false,
|
||||
id = globalThis.crypto.randomUUID(),
|
||||
replaceAll = false
|
||||
) {
|
||||
if (!this.#snackbar) {
|
||||
this.#pendingSnackbarMessages.push({
|
||||
message: {
|
||||
id,
|
||||
message,
|
||||
type,
|
||||
persistent,
|
||||
actions,
|
||||
},
|
||||
replaceAll,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
return this.#snackbar.show(
|
||||
{
|
||||
id,
|
||||
message,
|
||||
type,
|
||||
persistent,
|
||||
actions,
|
||||
},
|
||||
replaceAll
|
||||
);
|
||||
}
|
||||
|
||||
unsnackbar(id?: SnackbarUUID) {
|
||||
if (!this.#snackbar) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.#snackbar.hide(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { HTMLTemplateResult } from "lit";
|
||||
|
||||
const eventInit = {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
composed: true,
|
||||
};
|
||||
|
||||
export class SnackbarActionEvent extends Event {
|
||||
static eventName = "snackbaraction";
|
||||
|
||||
constructor(
|
||||
public readonly action: string,
|
||||
public readonly value?: HTMLTemplateResult | string,
|
||||
public readonly callback?: () => void
|
||||
) {
|
||||
super(SnackbarActionEvent.eventName, { ...eventInit });
|
||||
}
|
||||
}
|
||||
196
examples/agent/a2ui_agent/samples/client/lit/contact/index.html
Normal file
196
examples/agent/a2ui_agent/samples/client/lit/contact/index.html
Normal file
@@ -0,0 +1,196 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Contact Finder Agent</title>
|
||||
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200&icon_names=account_circle,add,arrow_back,arrow_drop_down,arrow_forward,attach_file,calendar_today,call,camera,check,check_circle,close,communication,content_copy,delete,download,draw,edit,error,event,favorite,favorite_off,folder,help,home,info,location_on,lock,lock_open,mail,menu,mobile_layout,more_horiz,more_vert,notifications,notifications_off,payment,pen_size_1,person,phone,photo,print,progress_activity,rectangle,refresh,search,send,settings,share,shopping_cart,star,star_half,star_off,upload,visibility,visibility_off,warning"
|
||||
/>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--n-100: #ffffff;
|
||||
--n-99: #fcfcfc;
|
||||
--n-98: #f9f9f9;
|
||||
--n-95: #f1f1f1;
|
||||
--n-90: #e2e2e2;
|
||||
--n-80: #c6c6c6;
|
||||
--n-70: #ababab;
|
||||
--n-60: #919191;
|
||||
--n-50: #777777;
|
||||
--n-40: #5e5e5e;
|
||||
--n-35: #525252;
|
||||
--n-30: #474747;
|
||||
--n-25: #3b3b3b;
|
||||
--n-20: #303030;
|
||||
--n-15: #262626;
|
||||
--n-10: #1b1b1b;
|
||||
--n-5: #111111;
|
||||
--n-0: #000000;
|
||||
|
||||
--p-100: #ffffff;
|
||||
--p-99: #fffbff;
|
||||
--p-98: #fcf8ff;
|
||||
--p-95: #f2efff;
|
||||
--p-90: #e1e0ff;
|
||||
--p-80: #c0c1ff;
|
||||
--p-70: #a0a3ff;
|
||||
--p-60: #8487ea;
|
||||
--p-50: #6a6dcd;
|
||||
--p-40: #5154b3;
|
||||
--p-35: #4447a6;
|
||||
--p-30: #383b99;
|
||||
--p-25: #2c2e8d;
|
||||
--p-20: #202182;
|
||||
--p-15: #131178;
|
||||
--p-10: #06006c;
|
||||
--p-5: #03004d;
|
||||
--p-0: #000000;
|
||||
|
||||
--s-100: #ffffff;
|
||||
--s-99: #fffbff;
|
||||
--s-98: #fcf8ff;
|
||||
--s-95: #f2efff;
|
||||
--s-90: #e2e0f9;
|
||||
--s-80: #c6c4dd;
|
||||
--s-70: #aaa9c1;
|
||||
--s-60: #8f8fa5;
|
||||
--s-50: #75758b;
|
||||
--s-40: #5d5c72;
|
||||
--s-35: #515165;
|
||||
--s-30: #454559;
|
||||
--s-25: #393a4d;
|
||||
--s-20: #2e2f42;
|
||||
--s-15: #242437;
|
||||
--s-10: #191a2c;
|
||||
--s-5: #0f0f21;
|
||||
--s-0: #000000;
|
||||
|
||||
--t-100: #ffffff;
|
||||
--t-99: #fffbff;
|
||||
--t-98: #fff8f9;
|
||||
--t-95: #ffecf4;
|
||||
--t-90: #ffd8ec;
|
||||
--t-80: #e9b9d3;
|
||||
--t-70: #cc9eb8;
|
||||
--t-60: #af849d;
|
||||
--t-50: #946b83;
|
||||
--t-40: #79536a;
|
||||
--t-35: #6c475d;
|
||||
--t-30: #5f3c51;
|
||||
--t-25: #523146;
|
||||
--t-20: #46263a;
|
||||
--t-15: #3a1b2f;
|
||||
--t-10: #2e1125;
|
||||
--t-5: #22071a;
|
||||
--t-0: #000000;
|
||||
|
||||
--nv-100: #ffffff;
|
||||
--nv-99: #fffbff;
|
||||
--nv-98: #fcf8ff;
|
||||
--nv-95: #f2effa;
|
||||
--nv-90: #e4e1ec;
|
||||
--nv-80: #c8c5d0;
|
||||
--nv-70: #acaab4;
|
||||
--nv-60: #918f9a;
|
||||
--nv-50: #777680;
|
||||
--nv-40: #5e5d67;
|
||||
--nv-35: #52515b;
|
||||
--nv-30: #46464f;
|
||||
--nv-25: #3b3b43;
|
||||
--nv-20: #303038;
|
||||
--nv-15: #25252d;
|
||||
--nv-10: #1b1b23;
|
||||
--nv-5: #101018;
|
||||
--nv-0: #000000;
|
||||
|
||||
--e-100: #ffffff;
|
||||
--e-99: #fffbff;
|
||||
--e-98: #fff8f7;
|
||||
--e-95: #ffedea;
|
||||
--e-90: #ffdad6;
|
||||
--e-80: #ffb4ab;
|
||||
--e-70: #ff897d;
|
||||
--e-60: #ff5449;
|
||||
--e-50: #de3730;
|
||||
--e-40: #ba1a1a;
|
||||
--e-35: #a80710;
|
||||
--e-30: #93000a;
|
||||
--e-25: #7e0007;
|
||||
--e-20: #690005;
|
||||
--e-15: #540003;
|
||||
--e-10: #410002;
|
||||
--e-5: #2d0001;
|
||||
--e-0: #000000;
|
||||
|
||||
--primary: #137fec;
|
||||
--text-color: #fff;
|
||||
--background-light: #f6f7f8;
|
||||
--background-dark: #101922;
|
||||
--border-color: oklch(
|
||||
from var(--background-light) l c h / calc(alpha * 0.15)
|
||||
);
|
||||
--elevated-background-light: oklch(
|
||||
from var(--background-light) l c h / calc(alpha * 0.05)
|
||||
);
|
||||
--bb-grid-size: 4px;
|
||||
--bb-grid-size-2: calc(var(--bb-grid-size) * 2);
|
||||
--bb-grid-size-3: calc(var(--bb-grid-size) * 3);
|
||||
--bb-grid-size-4: calc(var(--bb-grid-size) * 4);
|
||||
--bb-grid-size-5: calc(var(--bb-grid-size) * 5);
|
||||
--bb-grid-size-6: calc(var(--bb-grid-size) * 6);
|
||||
--bb-grid-size-7: calc(var(--bb-grid-size) * 7);
|
||||
--bb-grid-size-8: calc(var(--bb-grid-size) * 8);
|
||||
--bb-grid-size-9: calc(var(--bb-grid-size) * 9);
|
||||
--bb-grid-size-10: calc(var(--bb-grid-size) * 10);
|
||||
--bb-grid-size-11: calc(var(--bb-grid-size) * 11);
|
||||
--bb-grid-size-12: calc(var(--bb-grid-size) * 12);
|
||||
--bb-grid-size-13: calc(var(--bb-grid-size) * 13);
|
||||
--bb-grid-size-14: calc(var(--bb-grid-size) * 14);
|
||||
--bb-grid-size-15: calc(var(--bb-grid-size) * 15);
|
||||
--bb-grid-size-16: calc(var(--bb-grid-size) * 16);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
--font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
--font-family-flex: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
--font-family-mono: monospace;
|
||||
|
||||
background: var(--background-light);
|
||||
font-family: var(--font-family);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100svw;
|
||||
height: 100svh;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a2ui-contact></a2ui-contact>
|
||||
<script src="./contact.ts" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { IncomingMessage, ServerResponse } from "http";
|
||||
import { Plugin, ViteDevServer } from "vite";
|
||||
import { A2AClient } from "@a2a-js/sdk/client";
|
||||
import {
|
||||
MessageSendParams,
|
||||
Part,
|
||||
SendMessageSuccessResponse,
|
||||
Task,
|
||||
} from "@a2a-js/sdk";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
const A2AUI_MIME_TYPE = "application/json+a2aui";
|
||||
|
||||
const fetchWithCustomHeader: typeof fetch = async (url, init) => {
|
||||
const headers = new Headers(init?.headers);
|
||||
headers.set("X-A2A-Extensions", "https://a2ui.org/a2a-extension/a2ui/v0.8");
|
||||
|
||||
const newInit = { ...init, headers };
|
||||
return fetch(url, newInit);
|
||||
};
|
||||
|
||||
const isJson = (str: string) => {
|
||||
try {
|
||||
const parsed = JSON.parse(str);
|
||||
return (
|
||||
typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
||||
);
|
||||
} catch (err) {
|
||||
console.warn(err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let client: A2AClient | null = null;
|
||||
const createOrGetClient = async () => {
|
||||
if (!client) {
|
||||
// Create a client pointing to the agent's Agent Card URL.
|
||||
client = await A2AClient.fromCardUrl(
|
||||
"http://localhost:10002/.well-known/agent-card.json",
|
||||
{ fetchImpl: fetchWithCustomHeader }
|
||||
);
|
||||
}
|
||||
|
||||
return client;
|
||||
};
|
||||
|
||||
export const plugin = (): Plugin => {
|
||||
return {
|
||||
name: "a2a-handler",
|
||||
configureServer(server: ViteDevServer) {
|
||||
server.middlewares.use(
|
||||
"/a2a",
|
||||
async (req: IncomingMessage, res: ServerResponse, next: () => void) => {
|
||||
if (req.method === "POST") {
|
||||
let originalBody = "";
|
||||
|
||||
req.on("data", (chunk) => {
|
||||
originalBody += chunk.toString();
|
||||
});
|
||||
|
||||
req.on("end", async () => {
|
||||
let sendParams: MessageSendParams;
|
||||
|
||||
if (isJson(originalBody)) {
|
||||
console.log(
|
||||
"[a2a-middleware] Received JSON UI event:",
|
||||
originalBody
|
||||
);
|
||||
|
||||
const clientEvent = JSON.parse(originalBody);
|
||||
sendParams = {
|
||||
message: {
|
||||
messageId: uuidv4(),
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
kind: "data",
|
||||
data: clientEvent,
|
||||
metadata: { 'mimeType': A2AUI_MIME_TYPE },
|
||||
} as Part,
|
||||
],
|
||||
kind: "message",
|
||||
},
|
||||
};
|
||||
} else {
|
||||
console.log(
|
||||
"[a2a-middleware] Received text query:",
|
||||
originalBody
|
||||
);
|
||||
sendParams = {
|
||||
message: {
|
||||
messageId: uuidv4(),
|
||||
role: "user",
|
||||
parts: [
|
||||
{
|
||||
kind: "text",
|
||||
text: originalBody,
|
||||
},
|
||||
],
|
||||
kind: "message",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const client = await createOrGetClient();
|
||||
const response = await client.sendMessage(sendParams);
|
||||
if ("error" in response) {
|
||||
console.error("Error:", response.error.message);
|
||||
res.statusCode = 500;
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.end(JSON.stringify({ error: response.error.message }));
|
||||
return;
|
||||
} else {
|
||||
const result = (response as SendMessageSuccessResponse)
|
||||
.result as Task;
|
||||
if (result.kind === "task") {
|
||||
res.statusCode = 200;
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.end(JSON.stringify(result.status.message?.parts));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.statusCode = 200;
|
||||
res.setHeader("Content-Type", "application/json");
|
||||
res.end(JSON.stringify([]));
|
||||
});
|
||||
|
||||
return;
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export * as A2AMiddleware from "./a2a.js";
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"name": "@a2ui/contact",
|
||||
"private": true,
|
||||
"version": "0.8.1",
|
||||
"description": "A2UI Contact Demo",
|
||||
"main": "./dist/contact.js",
|
||||
"types": "./dist/contact.d.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"prepack": "npm run build",
|
||||
"build": "wireit",
|
||||
"build:tsc": "wireit",
|
||||
"dev": "npm run serve --watch",
|
||||
"test": "wireit",
|
||||
"serve": "wireit"
|
||||
},
|
||||
"wireit": {
|
||||
"serve": {
|
||||
"command": "vite dev",
|
||||
"dependencies": [
|
||||
"build"
|
||||
],
|
||||
"service": true
|
||||
},
|
||||
"test": {
|
||||
"command": "node --test --enable-source-maps --test-reporter spec dist/src/0.8/tests/**/*.test.js",
|
||||
"dependencies": [
|
||||
"build"
|
||||
]
|
||||
},
|
||||
"build": {
|
||||
"dependencies": [
|
||||
"build:tsc"
|
||||
]
|
||||
},
|
||||
"build:tsc": {
|
||||
"command": "tsc -b --pretty",
|
||||
"env": {
|
||||
"FORCE_COLOR": "1"
|
||||
},
|
||||
"dependencies": [
|
||||
"../../../../renderers/lit:build:tsc"
|
||||
],
|
||||
"files": [
|
||||
"**/*.ts",
|
||||
"tsconfig.json"
|
||||
],
|
||||
"output": [
|
||||
"dist/",
|
||||
"!dist/**/*.min.js{,.map}"
|
||||
],
|
||||
"clean": "if-file-deleted"
|
||||
}
|
||||
},
|
||||
"repository": {
|
||||
"directory": "samples/client/lit/contact",
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/google/A2UI.git"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"keywords": [],
|
||||
"author": "Google",
|
||||
"license": "Apache-2.0",
|
||||
"bugs": {
|
||||
"url": "https://github.com/google/A2UI/issues"
|
||||
},
|
||||
"homepage": "https://github.com/google/A2UI/tree/main/web#readme",
|
||||
"devDependencies": {
|
||||
"dotenv": "^17.2.3",
|
||||
"typescript": "^5.8.3",
|
||||
"uuid": "^13.0.0",
|
||||
"vite": "^7.1.11",
|
||||
"wireit": "^0.15.0-pre.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@a2a-js/sdk": "^0.3.4",
|
||||
"@a2ui/lit": "file:../../../../renderers/lit",
|
||||
"@lit-labs/signals": "^0.1.3",
|
||||
"@lit/context": "^1.1.4",
|
||||
"lit": "^3.3.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
/*
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { v0_8 } from "@a2ui/lit";
|
||||
|
||||
/** Elements */
|
||||
|
||||
const a = {
|
||||
"typography-f-sf": true,
|
||||
"typography-fs-n": true,
|
||||
"typography-w-500": true,
|
||||
"layout-as-n": true,
|
||||
"layout-dis-iflx": true,
|
||||
"layout-al-c": true,
|
||||
};
|
||||
|
||||
const audio = {
|
||||
"layout-w-100": true,
|
||||
};
|
||||
|
||||
const body = {
|
||||
"typography-f-s": true,
|
||||
"typography-fs-n": true,
|
||||
"typography-w-400": true,
|
||||
"layout-mt-0": true,
|
||||
"layout-mb-2": true,
|
||||
"typography-sz-bm": true,
|
||||
"color-c-n10": true,
|
||||
};
|
||||
|
||||
const button = {
|
||||
"typography-f-sf": true,
|
||||
"typography-fs-n": true,
|
||||
"typography-w-500": true,
|
||||
"layout-pt-3": true,
|
||||
"layout-pb-3": true,
|
||||
"layout-pl-5": true,
|
||||
"layout-pr-5": true,
|
||||
"layout-mb-1": true,
|
||||
"border-br-16": true,
|
||||
"border-bw-0": true,
|
||||
"border-c-n70": true,
|
||||
"border-bs-s": true,
|
||||
"color-bgc-s30": true,
|
||||
"color-c-n100": true,
|
||||
"behavior-ho-80": true,
|
||||
};
|
||||
|
||||
const heading = {
|
||||
"typography-f-sf": true,
|
||||
"typography-fs-n": true,
|
||||
"typography-w-500": true,
|
||||
"layout-mt-0": true,
|
||||
"layout-mb-2": true,
|
||||
"color-c-n10": true,
|
||||
};
|
||||
|
||||
const h1 = {
|
||||
...heading,
|
||||
"typography-sz-tl": true,
|
||||
};
|
||||
|
||||
const h2 = {
|
||||
...heading,
|
||||
"typography-sz-tm": true,
|
||||
};
|
||||
|
||||
const h3 = {
|
||||
...heading,
|
||||
"typography-sz-ts": true,
|
||||
};
|
||||
|
||||
const iframe = {
|
||||
"behavior-sw-n": true,
|
||||
};
|
||||
|
||||
const input = {
|
||||
"typography-f-sf": true,
|
||||
"typography-fs-n": true,
|
||||
"typography-w-400": true,
|
||||
"layout-pl-4": true,
|
||||
"layout-pr-4": true,
|
||||
"layout-pt-2": true,
|
||||
"layout-pb-2": true,
|
||||
"border-br-6": true,
|
||||
"border-bw-1": true,
|
||||
"color-bc-s70": true,
|
||||
"border-bs-s": true,
|
||||
"layout-as-n": true,
|
||||
"color-c-n10": true,
|
||||
};
|
||||
|
||||
const p = {
|
||||
"typography-f-s": true,
|
||||
"typography-fs-n": true,
|
||||
"typography-w-400": true,
|
||||
"layout-m-0": true,
|
||||
"typography-sz-bm": true,
|
||||
"layout-as-n": true,
|
||||
"color-c-n10": true,
|
||||
};
|
||||
|
||||
const orderedList = {
|
||||
"typography-f-s": true,
|
||||
"typography-fs-n": true,
|
||||
"typography-w-400": true,
|
||||
"layout-m-0": true,
|
||||
"typography-sz-bm": true,
|
||||
"layout-as-n": true,
|
||||
};
|
||||
|
||||
const unorderedList = {
|
||||
"typography-f-s": true,
|
||||
"typography-fs-n": true,
|
||||
"typography-w-400": true,
|
||||
"layout-m-0": true,
|
||||
"typography-sz-bm": true,
|
||||
"layout-as-n": true,
|
||||
};
|
||||
|
||||
const listItem = {
|
||||
"typography-f-s": true,
|
||||
"typography-fs-n": true,
|
||||
"typography-w-400": true,
|
||||
"layout-m-0": true,
|
||||
"typography-sz-bm": true,
|
||||
"layout-as-n": true,
|
||||
};
|
||||
|
||||
const pre = {
|
||||
"typography-f-c": true,
|
||||
"typography-fs-n": true,
|
||||
"typography-w-400": true,
|
||||
"typography-sz-bm": true,
|
||||
"typography-ws-p": true,
|
||||
"layout-as-n": true,
|
||||
};
|
||||
|
||||
const textarea = {
|
||||
...input,
|
||||
"layout-r-none": true,
|
||||
"layout-fs-c": true,
|
||||
};
|
||||
|
||||
const video = {
|
||||
"layout-el-cv": true,
|
||||
};
|
||||
|
||||
const aLight = v0_8.Styles.merge(a, { "color-c-p30": true });
|
||||
const inputLight = v0_8.Styles.merge(input, { "color-c-n5": true });
|
||||
const textareaLight = v0_8.Styles.merge(textarea, { "color-c-n5": true });
|
||||
const buttonLight = v0_8.Styles.merge(button, { "color-c-n100": true });
|
||||
const h1Light = v0_8.Styles.merge(h1, { "color-c-n5": true });
|
||||
const h2Light = v0_8.Styles.merge(h2, { "color-c-n5": true });
|
||||
const h3Light = v0_8.Styles.merge(h3, { "color-c-n5": true });
|
||||
const bodyLight = v0_8.Styles.merge(body, { "color-c-n5": true });
|
||||
const pLight = v0_8.Styles.merge(p, { "color-c-n60": true });
|
||||
const preLight = v0_8.Styles.merge(pre, { "color-c-n35": true });
|
||||
const orderedListLight = v0_8.Styles.merge(orderedList, {
|
||||
"color-c-n35": true,
|
||||
});
|
||||
const unorderedListLight = v0_8.Styles.merge(unorderedList, {
|
||||
"color-c-n35": true,
|
||||
});
|
||||
const listItemLight = v0_8.Styles.merge(listItem, {
|
||||
"color-c-n35": true,
|
||||
});
|
||||
|
||||
export const theme: v0_8.Types.Theme = {
|
||||
additionalStyles: {
|
||||
Card: {
|
||||
"min-width": "320px",
|
||||
},
|
||||
Button: {
|
||||
"--n-60": "var(--n-100)",
|
||||
},
|
||||
Image: {
|
||||
"max-width": "120px",
|
||||
"max-height": "120px",
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
},
|
||||
},
|
||||
components: {
|
||||
AudioPlayer: {},
|
||||
Button: {
|
||||
"layout-pt-2": true,
|
||||
"layout-pb-2": true,
|
||||
"layout-pl-5": true,
|
||||
"layout-pr-5": true,
|
||||
"border-br-2": true,
|
||||
"border-bw-0": true,
|
||||
"border-bs-s": true,
|
||||
"color-bgc-p30": true,
|
||||
"color-c-n100": true,
|
||||
"behavior-ho-70": true,
|
||||
},
|
||||
Card: {
|
||||
"border-br-4": true,
|
||||
"color-bgc-p100": true,
|
||||
"color-bc-n90": true,
|
||||
"border-bw-1": true,
|
||||
"border-bs-s": true,
|
||||
"layout-pt-10": true,
|
||||
"layout-pb-10": true,
|
||||
"layout-pl-4": true,
|
||||
"layout-pr-4": true,
|
||||
},
|
||||
CheckBox: {
|
||||
element: {
|
||||
"layout-m-0": true,
|
||||
"layout-mr-2": true,
|
||||
"layout-p-2": true,
|
||||
"border-br-12": true,
|
||||
"border-bw-1": true,
|
||||
"border-bs-s": true,
|
||||
"color-bgc-p100": true,
|
||||
"color-bc-p60": true,
|
||||
"color-c-n30": true,
|
||||
"color-c-p30": true,
|
||||
},
|
||||
label: {
|
||||
"color-c-p30": true,
|
||||
"typography-f-sf": true,
|
||||
"typography-v-r": true,
|
||||
"typography-w-400": true,
|
||||
"layout-flx-1": true,
|
||||
"typography-sz-ll": true,
|
||||
},
|
||||
container: {
|
||||
"layout-dsp-iflex": true,
|
||||
"layout-al-c": true,
|
||||
},
|
||||
},
|
||||
Column: {},
|
||||
DateTimeInput: {
|
||||
container: {},
|
||||
label: {},
|
||||
element: {
|
||||
"layout-pt-2": true,
|
||||
"layout-pb-2": true,
|
||||
"layout-pl-3": true,
|
||||
"layout-pr-3": true,
|
||||
"border-br-12": true,
|
||||
"border-bw-1": true,
|
||||
"border-bs-s": true,
|
||||
"color-bgc-p100": true,
|
||||
"color-bc-p60": true,
|
||||
"color-c-n30": true,
|
||||
},
|
||||
},
|
||||
Divider: {
|
||||
"color-bgc-n90": true,
|
||||
"layout-mt-6": true,
|
||||
"layout-mb-6": true,
|
||||
},
|
||||
Image: {
|
||||
all: {
|
||||
"border-br-50pc": true,
|
||||
"layout-el-cv": true,
|
||||
"layout-w-100": true,
|
||||
"layout-h-100": true,
|
||||
"layout-dsp-flexhor": true,
|
||||
"layout-al-c": true,
|
||||
"layout-sp-c": true,
|
||||
"layout-mb-3": true,
|
||||
},
|
||||
avatar: {},
|
||||
header: {},
|
||||
icon: {},
|
||||
largeFeature: {},
|
||||
mediumFeature: {},
|
||||
smallFeature: {},
|
||||
},
|
||||
Icon: {
|
||||
"border-br-1": true,
|
||||
"layout-p-2": true,
|
||||
"color-bgc-n98": true,
|
||||
"layout-dsp-flexhor": true,
|
||||
"layout-al-c": true,
|
||||
"layout-sp-c": true,
|
||||
},
|
||||
List: {
|
||||
"layout-g-4": true,
|
||||
"layout-p-2": true,
|
||||
},
|
||||
Modal: {
|
||||
backdrop: { "color-bbgc-p60_20": true },
|
||||
element: {
|
||||
"border-br-2": true,
|
||||
"color-bgc-p100": true,
|
||||
"layout-p-4": true,
|
||||
"border-bw-1": true,
|
||||
"border-bs-s": true,
|
||||
"color-bc-p80": true,
|
||||
},
|
||||
},
|
||||
MultipleChoice: {
|
||||
container: {},
|
||||
label: {},
|
||||
element: {},
|
||||
},
|
||||
Row: {
|
||||
"layout-g-4": true,
|
||||
"layout-mb-3": true,
|
||||
},
|
||||
Slider: {
|
||||
container: {},
|
||||
label: {},
|
||||
element: {},
|
||||
},
|
||||
Tabs: {
|
||||
container: {},
|
||||
controls: { all: {}, selected: {} },
|
||||
element: {},
|
||||
},
|
||||
Text: {
|
||||
all: {
|
||||
"layout-w-100": true,
|
||||
"layout-g-2": true,
|
||||
"color-c-p30": true,
|
||||
},
|
||||
h1: {
|
||||
"typography-f-sf": true,
|
||||
"typography-ta-c": true,
|
||||
"typography-v-r": true,
|
||||
"typography-w-500": true,
|
||||
"layout-mt-0": true,
|
||||
"layout-mr-0": true,
|
||||
"layout-ml-0": true,
|
||||
"layout-mb-2": true,
|
||||
"layout-p-0": true,
|
||||
"typography-sz-tl": true,
|
||||
},
|
||||
h2: {
|
||||
"typography-f-sf": true,
|
||||
"typography-ta-c": true,
|
||||
"typography-v-r": true,
|
||||
"typography-w-500": true,
|
||||
"layout-mt-0": true,
|
||||
"layout-mr-0": true,
|
||||
"layout-ml-0": true,
|
||||
"layout-mb-2": true,
|
||||
"layout-p-0": true,
|
||||
"typography-sz-tl": true,
|
||||
},
|
||||
h3: {
|
||||
"typography-f-sf": true,
|
||||
"typography-ta-c": true,
|
||||
"typography-v-r": true,
|
||||
"typography-w-500": true,
|
||||
"layout-mt-0": true,
|
||||
"layout-mr-0": true,
|
||||
"layout-ml-0": true,
|
||||
"layout-mb-0": true,
|
||||
"layout-p-0": true,
|
||||
"typography-sz-ts": true,
|
||||
},
|
||||
h4: {
|
||||
"typography-f-sf": true,
|
||||
"typography-ta-c": true,
|
||||
"typography-v-r": true,
|
||||
"typography-w-500": true,
|
||||
"layout-mt-0": true,
|
||||
"layout-mr-0": true,
|
||||
"layout-ml-0": true,
|
||||
"layout-mb-0": true,
|
||||
"layout-p-0": true,
|
||||
"typography-sz-bl": true,
|
||||
},
|
||||
h5: {
|
||||
"typography-f-sf": true,
|
||||
"typography-ta-c": true,
|
||||
"typography-v-r": true,
|
||||
"typography-w-500": true,
|
||||
"layout-mt-0": true,
|
||||
"layout-mr-0": true,
|
||||
"layout-ml-0": true,
|
||||
"layout-mb-0": true,
|
||||
"layout-p-0": true,
|
||||
"color-c-n30": true,
|
||||
"typography-sz-bm": true,
|
||||
"layout-mb-1": true,
|
||||
},
|
||||
body: {},
|
||||
caption: {},
|
||||
},
|
||||
TextField: {
|
||||
container: {
|
||||
"typography-sz-bm": true,
|
||||
"layout-w-100": true,
|
||||
"layout-g-2": true,
|
||||
"layout-dsp-flexhor": true,
|
||||
"layout-al-c": true,
|
||||
},
|
||||
label: {
|
||||
"layout-flx-0": true,
|
||||
},
|
||||
element: {
|
||||
"typography-sz-bm": true,
|
||||
"layout-pt-2": true,
|
||||
"layout-pb-2": true,
|
||||
"layout-pl-3": true,
|
||||
"layout-pr-3": true,
|
||||
"border-br-12": true,
|
||||
"border-bw-1": true,
|
||||
"border-bs-s": true,
|
||||
"color-bgc-p100": true,
|
||||
"color-bc-p60": true,
|
||||
"color-c-n30": true,
|
||||
"color-c-p30": true,
|
||||
},
|
||||
},
|
||||
Video: {
|
||||
"border-br-5": true,
|
||||
"layout-el-cv": true,
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
a: aLight,
|
||||
audio,
|
||||
body: bodyLight,
|
||||
button: buttonLight,
|
||||
h1: h1Light,
|
||||
h2: h2Light,
|
||||
h3: h3Light,
|
||||
h4: {},
|
||||
h5: {},
|
||||
iframe,
|
||||
input: inputLight,
|
||||
p: pLight,
|
||||
pre: preLight,
|
||||
textarea: textareaLight,
|
||||
video,
|
||||
},
|
||||
markdown: {
|
||||
p: [...Object.keys(pLight)],
|
||||
h1: [...Object.keys(h1Light)],
|
||||
h2: [...Object.keys(h2Light)],
|
||||
h3: [...Object.keys(h3Light)],
|
||||
h4: [],
|
||||
h5: [],
|
||||
ul: [...Object.keys(unorderedListLight)],
|
||||
ol: [...Object.keys(orderedListLight)],
|
||||
li: [...Object.keys(listItemLight)],
|
||||
a: [...Object.keys(aLight)],
|
||||
strong: [],
|
||||
em: [],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"incremental": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"inlineSources": false,
|
||||
"preserveWatchOutput": true,
|
||||
"sourceMap": true,
|
||||
"target": "es2022",
|
||||
"module": "es2022",
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
"useDefineForClassFields": false,
|
||||
"rootDir": ".",
|
||||
"outDir": "dist",
|
||||
"tsBuildInfoFile": "dist/.tsbuildinfo",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"strict": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"references": [{ "path": "../../../../renderers/lit" }]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { HTMLTemplateResult } from "lit";
|
||||
|
||||
export enum SnackType {
|
||||
NONE = "none",
|
||||
INFORMATION = "information",
|
||||
WARNING = "warning",
|
||||
ERROR = "error",
|
||||
PENDING = "pending",
|
||||
}
|
||||
|
||||
export type SnackbarUUID = ReturnType<typeof globalThis.crypto.randomUUID>;
|
||||
|
||||
export type SnackbarAction = {
|
||||
title: string;
|
||||
action: string;
|
||||
value?: HTMLTemplateResult | string;
|
||||
callback?: () => void;
|
||||
};
|
||||
|
||||
export type SnackbarMessage = {
|
||||
id: SnackbarUUID;
|
||||
type: SnackType;
|
||||
persistent: boolean;
|
||||
message: string | HTMLTemplateResult;
|
||||
actions?: SnackbarAction[];
|
||||
};
|
||||
@@ -0,0 +1,167 @@
|
||||
# A2UI custom component integration guide
|
||||
|
||||
This guide details how to create, register, and use a custom component in the A2UI client.
|
||||
|
||||
## Create the component
|
||||
|
||||
Create a new Lit component file in `lib/src/0.8/ui/custom-components/`.
|
||||
Example: `my-component.ts`
|
||||
|
||||
```typescript
|
||||
import { html, css } from "lit";
|
||||
import { property } from "lit/decorators.js";
|
||||
|
||||
import { Root } from "../root.js";
|
||||
|
||||
export class MyComponent extends Root {
|
||||
@property() accessor myProp: string = "Default";
|
||||
|
||||
static styles = [
|
||||
...Root.styles, // Inherit base styles
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div>
|
||||
<h2>My Custom Component</h2>
|
||||
<p>Prop value: ${this.myProp}</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Register the component
|
||||
|
||||
Update `lib/src/0.8/ui/custom-components/index.ts` to register your new component.
|
||||
You must pass the desired tag name as the third argument.
|
||||
|
||||
```typescript
|
||||
import { componentRegistry } from "../component-registry.js";
|
||||
import { MyComponent } from "./my-component.js"; // Import your component
|
||||
|
||||
export function registerCustomComponents() {
|
||||
// Register with explicit tag name
|
||||
componentRegistry.register("MyComponent", MyComponent, "my-component");
|
||||
}
|
||||
|
||||
export { MyComponent }; // Export for type usage if needed
|
||||
```
|
||||
|
||||
## Define the schema (server-side)
|
||||
|
||||
Create a JSON schema for your component properties. This will be used by the server to validate messages.
|
||||
Example: `lib/my_component_schema.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"type": { "const": "object" },
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"myProp": {
|
||||
"type": "string",
|
||||
"description": "A sample property."
|
||||
}
|
||||
},
|
||||
"required": ["myProp"]
|
||||
}
|
||||
},
|
||||
"required": ["type", "properties"]
|
||||
}
|
||||
```
|
||||
|
||||
## Use in client application
|
||||
|
||||
In your client application (e.g., `contact` sample), ensure you import and call the registration function.
|
||||
|
||||
```typescript
|
||||
import { registerCustomComponents } from "@a2ui/lit/ui";
|
||||
|
||||
// Call this once at startup
|
||||
registerCustomComponents();
|
||||
```
|
||||
|
||||
## Overriding standard components
|
||||
|
||||
You can replace standard A2UI components (like `TextField`, `Video`, `Button`) with your own custom implementations.
|
||||
|
||||
### Steps to override
|
||||
|
||||
1. **Create your component** extending `Root` (just like a custom component).
|
||||
|
||||
2. **Ensure it accepts the standard properties** for that component type (e.g., `label` and `text` for `TextField`).
|
||||
|
||||
3. **Register it** using the **standard type name** (e.g., `"TextField"`).
|
||||
|
||||
```typescript
|
||||
// 1. Define your override
|
||||
class MyPremiumTextField extends Root {
|
||||
@property() accessor label = "";
|
||||
@property() accessor text = "";
|
||||
|
||||
static styles = [
|
||||
...Root.styles,
|
||||
css`
|
||||
/* your premium styles */
|
||||
`,
|
||||
];
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div class="premium-field">
|
||||
<label>${this.label}</label>
|
||||
<input .value="${this.text}" />
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Register with the STANDARD type name
|
||||
import { componentRegistry } from "@a2ui/lit/ui";
|
||||
componentRegistry.register(
|
||||
"TextField",
|
||||
MyPremiumTextField,
|
||||
"my-premium-textfield"
|
||||
);
|
||||
```
|
||||
|
||||
**Result:**
|
||||
When the server sends a `TextField` component, the client will now render `<my-premium-textfield>` instead of the default `<a2ui-textfield>`.
|
||||
|
||||
## Verify
|
||||
|
||||
You can verify the component by creating a simple HTML test file or by sending a server message with the new component type.
|
||||
|
||||
**Server message example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"surfaceId": "main",
|
||||
"component": {
|
||||
"type": "MyComponent",
|
||||
"id": "comp-1",
|
||||
"properties": {
|
||||
"myProp": "Hello World"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **`NotSupportedError`**: If you see "constructor has already been used", ensure you **removed** the `@customElement` decorator from your component class.
|
||||
- **Component not rendering**: Check if `registerCustomComponents()` is actually called. Verify the tag name in the DOM matches what you registered (e.g., `<my-component>` vs `<a2ui-custom-mycomponent>`).
|
||||
- **Styles missing**: Ensure `static styles` includes `...Root.styles`.
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Root } from '@a2ui/lit/ui';
|
||||
import { v0_8 } from '@a2ui/lit';
|
||||
import { html, css, TemplateResult } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { map } from 'lit/directives/map.js';
|
||||
|
||||
// Use aliases for convenience
|
||||
const StateEvent = v0_8.Events.StateEvent;
|
||||
type Action = v0_8.Types.Action;
|
||||
|
||||
export interface OrgChartNode {
|
||||
title: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
@customElement('org-chart')
|
||||
export class OrgChart extends Root {
|
||||
@property({ type: Array }) accessor chain: OrgChartNode[] = [];
|
||||
@property({ type: Object }) accessor action: Action | null = null;
|
||||
|
||||
static styles = [
|
||||
...Root.styles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.node {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 12px 24px;
|
||||
background: #fff;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
min-width: 200px;
|
||||
position: relative;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.node:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.node:focus {
|
||||
outline: 2px solid #1a73e8;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.node.current {
|
||||
background: #e8f0fe;
|
||||
border-color: #1a73e8;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 0.85rem;
|
||||
color: #5f6368;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 500;
|
||||
color: #202124;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
color: #9aa0a6;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
}
|
||||
`];
|
||||
|
||||
render() {
|
||||
if (!this.chain || this.chain.length === 0) {
|
||||
return html`<div class="empty">No hierarchy data</div>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="container">
|
||||
${map(this.chain, (node, index) => {
|
||||
const isLast = index === this.chain.length - 1;
|
||||
return html`
|
||||
<button
|
||||
class="node ${isLast ? 'current' : ''}"
|
||||
@click=${() => this.handleNodeClick(node)}
|
||||
aria-label="Select ${node.name} (${node.title})"
|
||||
>
|
||||
<span class="title">${node.title}</span>
|
||||
<span class="name">${node.name}</span>
|
||||
</button>
|
||||
${!isLast ? html`<div class="arrow">↓</div>` : ''}
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private handleNodeClick(node: OrgChartNode) {
|
||||
if (!this.action) return;
|
||||
|
||||
// Create a new action with the node's context merged in
|
||||
const newContext = [
|
||||
...(this.action.context || []),
|
||||
{
|
||||
key: 'clickedNodeTitle',
|
||||
value: { literalString: node.title }
|
||||
},
|
||||
{
|
||||
key: 'clickedNodeName',
|
||||
value: { literalString: node.name }
|
||||
}
|
||||
];
|
||||
|
||||
const actionWithContext: Action = {
|
||||
...this.action,
|
||||
context: newContext as Action['context']
|
||||
};
|
||||
|
||||
const evt = new StateEvent<"a2ui.action">({
|
||||
eventType: "a2ui.action",
|
||||
action: actionWithContext,
|
||||
dataContextPath: this.dataContextPath,
|
||||
sourceComponentId: this.id,
|
||||
sourceComponent: this.component,
|
||||
});
|
||||
this.dispatchEvent(evt);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { Root } from '@a2ui/lit/ui';
|
||||
import { html, css } from 'lit';
|
||||
import { property } from 'lit/decorators.js';
|
||||
|
||||
export class PremiumTextField extends Root {
|
||||
@property() accessor label = '';
|
||||
@property() accessor text = '';
|
||||
|
||||
static styles = [
|
||||
...Root.styles,
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
|
||||
border: 1px solid #e0e0e0;
|
||||
transition: all 0.2s ease;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
:host(:hover) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.12);
|
||||
}
|
||||
.input-container {
|
||||
position: relative;
|
||||
margin-top: 8px;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
font-size: 16px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
box-sizing: border-box;
|
||||
background: #fafafa;
|
||||
}
|
||||
input:focus {
|
||||
border-color: #6200ee;
|
||||
background: #fff;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.hint {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.badge {
|
||||
background: #6200ee;
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
`
|
||||
];
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<label>${this.label}</label>
|
||||
<div class="input-container">
|
||||
<input type="text" .value="${this.text}" placeholder="Type here...">
|
||||
</div>
|
||||
<div class="hint">
|
||||
<span class="badge">Custom</span>
|
||||
<span>This is a premium override of the standard TextField.</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { componentRegistry } from "@a2ui/lit/ui";
|
||||
import { OrgChart } from "./org-chart.js";
|
||||
import { PremiumTextField } from "./premium-text-field.js";
|
||||
|
||||
export function registerContactComponents() {
|
||||
// Register OrgChart
|
||||
componentRegistry.register("OrgChart", OrgChart, "org-chart");
|
||||
|
||||
// Register PremiumTextField as an override for TextField
|
||||
componentRegistry.register(
|
||||
"TextField",
|
||||
PremiumTextField,
|
||||
"premium-text-field"
|
||||
);
|
||||
|
||||
console.log("Registered Contact App Custom Components");
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
# Contact sample verification tests
|
||||
|
||||
This directory contains tests to verify custom component integration specifically within the `contact` sample application environment.
|
||||
|
||||
## How to run
|
||||
|
||||
These tests run via the Vite development server used by the contact sample.
|
||||
|
||||
### 1. Start the dev server
|
||||
From the `web/lit/samples/contact` directory, run:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 2. Access the tests
|
||||
Open your browser and navigate to the local server (usually port 5173):
|
||||
|
||||
- **Component override test**:
|
||||
[http://localhost:5173/ui/custom-components/test/override-test.html](http://localhost:5173/ui/custom-components/test/override-test.html)
|
||||
*Verifies that a standard component (TextField) can be overridden by a custom implementation.*
|
||||
|
||||
- **Hierarchy graph integration test**:
|
||||
[http://localhost:5173/ui/custom-components/test/hierarchy-test.html](http://localhost:5173/ui/custom-components/test/hierarchy-test.html)
|
||||
*Verifies that the HierarchyGraph component renders correctly within the contact app's build setup.*
|
||||
|
||||
## Files
|
||||
|
||||
- `override-test.html` & `override-test.ts`: Implements and tests a custom `TextField` override.
|
||||
- `hierarchy-test.html`: Tests the `HierarchyGraph` component.
|
||||
@@ -0,0 +1,86 @@
|
||||
<!--
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>A2UI Org Chart Test (Contact Sample)</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
<script type="module">
|
||||
import { registerContactComponents } from '../register-components.js';
|
||||
|
||||
// Register custom components
|
||||
registerContactComponents();
|
||||
console.log('Custom components registered in Contact Sample');
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>A2UI Org Chart Test (Contact Sample)</h1>
|
||||
|
||||
<div class="container">
|
||||
<org-chart id="graph"></org-chart>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
customElements.whenDefined('org-chart').then(() => {
|
||||
const graph = document.getElementById('graph');
|
||||
|
||||
graph.chain = [
|
||||
{ title: 'CEO', name: 'Alice Johnson' },
|
||||
{ title: 'SVP', name: 'Bob Smith' },
|
||||
{ title: 'VP', name: 'Charlie Brown' },
|
||||
{ title: 'Director', name: 'Diana Prince' },
|
||||
{ title: 'Software Engineer', name: 'Evan Wright' }
|
||||
];
|
||||
|
||||
graph.action = {
|
||||
name: 'showContactCard',
|
||||
context: []
|
||||
};
|
||||
|
||||
graph.addEventListener('a2uiaction', (e) => {
|
||||
const status = document.createElement('div');
|
||||
const clickedNodeTitle = e.detail.action.context.find(c => c.key === 'clickedNodeTitle')?.value.literalString;
|
||||
const clickedNodeName = e.detail.action.context.find(c => c.key === 'clickedNodeName')?.value.literalString;
|
||||
status.textContent = `Clicked: ${clickedNodeTitle} - ${clickedNodeName}`;
|
||||
status.style.marginTop = '20px';
|
||||
status.style.padding = '10px';
|
||||
status.style.background = '#e0f7fa';
|
||||
status.style.border = '1px solid #006064';
|
||||
status.id = 'action-status';
|
||||
document.body.appendChild(status);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,43 @@
|
||||
<!--
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>A2UI Component Override Test</title>
|
||||
<style>
|
||||
body {
|
||||
padding: 20px;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin-top: 20px;
|
||||
border: 1px dashed #ccc;
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
<script type="module" src="./override-test.ts"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Component Override Test</h1>
|
||||
<div id="app" class="container"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { componentRegistry, Root } from "@a2ui/lit/ui";
|
||||
import { html, css } from "lit";
|
||||
import { property } from "lit/decorators.js";
|
||||
// 1. Define the override
|
||||
import { PremiumTextField } from "../premium-text-field.js";
|
||||
|
||||
// 2. Register it as "TextField"
|
||||
componentRegistry.register("TextField", PremiumTextField, "premium-text-field");
|
||||
console.log("Registered PremiumTextField override");
|
||||
|
||||
// 3. Render a standard TextField component node
|
||||
const container = document.getElementById("app");
|
||||
if (container) {
|
||||
const root = document.createElement("a2ui-root") as Root;
|
||||
|
||||
const textFieldComponent = {
|
||||
type: "TextField",
|
||||
id: "tf-1",
|
||||
properties: {
|
||||
label: "Enter your name",
|
||||
text: "John Doe",
|
||||
},
|
||||
};
|
||||
|
||||
// Root renders its *children*, so we must pass the component as a child.
|
||||
root.childComponents = [textFieldComponent];
|
||||
|
||||
root.enableCustomElements = true; // Enable the feature
|
||||
container.appendChild(root);
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
/*
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import { LitElement, html, css, nothing, unsafeCSS } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { SnackbarMessage, SnackbarUUID, SnackType } from "../types/types";
|
||||
import { repeat } from "lit/directives/repeat.js";
|
||||
import { SnackbarActionEvent } from "../events/events";
|
||||
import { classMap } from "lit/directives/class-map.js";
|
||||
import { v0_8 } from "@a2ui/lit";
|
||||
|
||||
const DEFAULT_TIMEOUT = 8000;
|
||||
|
||||
@customElement("ui-snackbar")
|
||||
export class Snackbar extends LitElement {
|
||||
@property({ reflect: true, type: Boolean })
|
||||
accessor active = false;
|
||||
|
||||
@property({ reflect: true, type: Boolean })
|
||||
accessor error = false;
|
||||
|
||||
@property()
|
||||
accessor timeout = DEFAULT_TIMEOUT;
|
||||
|
||||
#messages: SnackbarMessage[] = [];
|
||||
#timeout = 0;
|
||||
|
||||
static styles = [
|
||||
unsafeCSS(v0_8.Styles.structuralStyles),
|
||||
css`
|
||||
:host {
|
||||
--text-color: var(--n-0);
|
||||
--bb-body-medium: 16px;
|
||||
--bb-body-line-height-medium: 24px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
bottom: var(--bb-grid-size-7);
|
||||
left: 50%;
|
||||
translate: -50% 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
border-radius: var(--bb-grid-size-2);
|
||||
background: var(--n-90);
|
||||
padding: var(--bb-grid-size-3) var(--bb-grid-size-6);
|
||||
width: 60svw;
|
||||
max-width: 720px;
|
||||
z-index: 1800;
|
||||
scrollbar-width: none;
|
||||
overflow-x: scroll;
|
||||
font: 400 var(--bb-body-medium) / var(--bb-body-line-height-medium)
|
||||
var(--bb-font-family);
|
||||
}
|
||||
|
||||
:host([active]) {
|
||||
transition: opacity 0.3s cubic-bezier(0, 0, 0.3, 1) 0.2s;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
:host([error]) {
|
||||
background: var(--e-90);
|
||||
--text-color: var(--e-40);
|
||||
}
|
||||
|
||||
.g-icon {
|
||||
flex: 0 0 auto;
|
||||
color: var(--text-color);
|
||||
margin-right: var(--bb-grid-size-4);
|
||||
|
||||
&.rotate {
|
||||
animation: 1s linear 0s infinite normal forwards running rotate;
|
||||
}
|
||||
}
|
||||
|
||||
#messages {
|
||||
color: var(--text-color);
|
||||
flex: 1 1 auto;
|
||||
margin-right: var(--bb-grid-size-11);
|
||||
|
||||
a,
|
||||
a:visited {
|
||||
color: var(--bb-ui-600);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: var(--bb-ui-500);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#actions {
|
||||
flex: 0 1 auto;
|
||||
width: fit-content;
|
||||
margin-right: var(--bb-grid-size-3);
|
||||
|
||||
& button {
|
||||
font: 500 var(--bb-body-medium) / var(--bb-body-line-height-medium)
|
||||
var(--bb-font-family);
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
margin: 0 var(--bb-grid-size-4);
|
||||
color: var(--text-color);
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s cubic-bezier(0, 0, 0.3, 1);
|
||||
|
||||
&:not([disabled]) {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
color: var(--text-color);
|
||||
background: transparent;
|
||||
border: none;
|
||||
margin: 0 0 0 var(--bb-grid-size-2);
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s cubic-bezier(0, 0, 0.3, 1);
|
||||
|
||||
.g-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
&:not([disabled]) {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
from {
|
||||
rotate: 0deg;
|
||||
}
|
||||
|
||||
to {
|
||||
rotate: 360deg;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
show(message: SnackbarMessage, replaceAll = false) {
|
||||
const existingMessage = this.#messages.findIndex(
|
||||
(msg) => msg.id === message.id
|
||||
);
|
||||
if (existingMessage === -1) {
|
||||
if (replaceAll) {
|
||||
this.#messages.length = 0;
|
||||
}
|
||||
|
||||
this.#messages.push(message);
|
||||
} else {
|
||||
this.#messages[existingMessage] = message;
|
||||
}
|
||||
|
||||
window.clearTimeout(this.#timeout);
|
||||
if (!this.#messages.every((msg) => msg.persistent)) {
|
||||
this.#timeout = window.setTimeout(() => {
|
||||
this.hide();
|
||||
}, this.timeout);
|
||||
}
|
||||
|
||||
this.error = this.#messages.some((msg) => msg.type === SnackType.ERROR);
|
||||
this.active = true;
|
||||
this.requestUpdate();
|
||||
|
||||
return message.id;
|
||||
}
|
||||
|
||||
hide(id?: SnackbarUUID) {
|
||||
if (id) {
|
||||
const idx = this.#messages.findIndex((msg) => msg.id === id);
|
||||
if (idx !== -1) {
|
||||
this.#messages.splice(idx, 1);
|
||||
}
|
||||
} else {
|
||||
this.#messages.length = 0;
|
||||
}
|
||||
|
||||
this.active = this.#messages.length !== 0;
|
||||
this.updateComplete.then((avoidedUpdate) => {
|
||||
if (!avoidedUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let rotate = false;
|
||||
let icon = "";
|
||||
for (let i = this.#messages.length - 1; i >= 0; i--) {
|
||||
if (
|
||||
!this.#messages[i].type ||
|
||||
this.#messages[i].type === SnackType.NONE
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
icon = this.#messages[i].type;
|
||||
if (this.#messages[i].type === SnackType.PENDING) {
|
||||
icon = "progress_activity";
|
||||
rotate = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return html` ${icon
|
||||
? html`<span
|
||||
class=${classMap({
|
||||
"g-icon": true,
|
||||
round: true,
|
||||
filled: true,
|
||||
rotate,
|
||||
})}
|
||||
>${icon}</span
|
||||
>`
|
||||
: nothing}
|
||||
<div id="messages">
|
||||
${repeat(
|
||||
this.#messages,
|
||||
(message) => message.id,
|
||||
(message) => {
|
||||
return html`<div>${message.message}</div>`;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<div id="actions">
|
||||
${repeat(
|
||||
this.#messages,
|
||||
(message) => message.id,
|
||||
(message) => {
|
||||
if (!message.actions) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`${repeat(
|
||||
message.actions,
|
||||
(action) => action.value,
|
||||
(action) => {
|
||||
return html`<button
|
||||
@click=${() => {
|
||||
this.hide();
|
||||
this.dispatchEvent(
|
||||
new SnackbarActionEvent(
|
||||
action.action,
|
||||
action.value,
|
||||
action.callback
|
||||
)
|
||||
);
|
||||
}}
|
||||
>
|
||||
${action.title}
|
||||
</button>`;
|
||||
}
|
||||
)}`;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
id="close"
|
||||
@click=${() => {
|
||||
this.hide();
|
||||
this.dispatchEvent(new SnackbarActionEvent("dismiss"));
|
||||
}}
|
||||
>
|
||||
<span class="g-icon">close</span>
|
||||
</button>`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export { Snackbar } from "./snackbar";
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
Copyright 2025 Google LLC
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
https://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { config } from "dotenv";
|
||||
import { UserConfig } from "vite";
|
||||
import * as Middleware from "./middleware";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export default async () => {
|
||||
config();
|
||||
|
||||
const entry: Record<string, string> = {
|
||||
contact: resolve(__dirname, "index.html"),
|
||||
};
|
||||
|
||||
return {
|
||||
plugins: [Middleware.A2AMiddleware.plugin()],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: entry,
|
||||
},
|
||||
target: "esnext",
|
||||
},
|
||||
define: {},
|
||||
resolve: {
|
||||
dedupe: ["lit"],
|
||||
},
|
||||
} satisfies UserConfig;
|
||||
};
|
||||
Reference in New Issue
Block a user