chore: initialize sandbox and overwrite remote content
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:
156
examples/agent/a2ui_agent/samples/client/a2a_client.py
Normal file
156
examples/agent/a2ui_agent/samples/client/a2a_client.py
Normal file
@@ -0,0 +1,156 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""A2A client example demonstrating agent card fetching and message sending."""
|
||||
import logging
|
||||
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
import httpx
|
||||
|
||||
from a2a.client import A2ACardResolver, A2AClient
|
||||
from a2a.types import (
|
||||
AgentCard,
|
||||
MessageSendParams,
|
||||
SendMessageRequest,
|
||||
)
|
||||
from a2a.utils.constants import (
|
||||
AGENT_CARD_WELL_KNOWN_PATH,
|
||||
EXTENDED_AGENT_CARD_PATH,
|
||||
)
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
"""Main function demonstrating A2A client usage."""
|
||||
# Configure logging to show INFO level messages
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__) # Get a logger instance
|
||||
|
||||
# --8<-- [start:A2ACardResolver]
|
||||
|
||||
base_url = "http://localhost:10002"
|
||||
|
||||
async with httpx.AsyncClient(
|
||||
timeout=httpx.Timeout(timeout=300.0),
|
||||
) as httpx_client:
|
||||
# Initialize A2ACardResolver
|
||||
resolver = A2ACardResolver(
|
||||
httpx_client=httpx_client,
|
||||
base_url=base_url,
|
||||
# agent_card_path uses default,
|
||||
# extended_agent_card_path also uses default
|
||||
)
|
||||
# --8<-- [end:A2ACardResolver]
|
||||
|
||||
# Fetch Public Agent Card and Initialize Client
|
||||
final_agent_card_to_use: AgentCard | None = None
|
||||
|
||||
try:
|
||||
logger.info(
|
||||
"Attempting to fetch public agent card from: %s%s",
|
||||
base_url,
|
||||
AGENT_CARD_WELL_KNOWN_PATH,
|
||||
)
|
||||
_public_card = (
|
||||
await resolver.get_agent_card()
|
||||
) # Fetches from default public path
|
||||
logger.info("Successfully fetched public agent card:")
|
||||
logger.info(
|
||||
_public_card.model_dump_json(indent=2, exclude_none=True),
|
||||
)
|
||||
final_agent_card_to_use = _public_card
|
||||
logger.info(
|
||||
"\nUsing PUBLIC agent card for client initialization "
|
||||
"(default).",
|
||||
)
|
||||
|
||||
if _public_card.supports_authenticated_extended_card:
|
||||
try:
|
||||
logger.info(
|
||||
"\nPublic card supports authenticated extended card. "
|
||||
"Attempting to fetch from: %s%s",
|
||||
base_url,
|
||||
EXTENDED_AGENT_CARD_PATH,
|
||||
)
|
||||
auth_headers_dict = {
|
||||
"Authorization": (
|
||||
"Bearer dummy-token-for-extended-card"
|
||||
),
|
||||
}
|
||||
_extended_card = await resolver.get_agent_card(
|
||||
relative_card_path=EXTENDED_AGENT_CARD_PATH,
|
||||
http_kwargs={"headers": auth_headers_dict},
|
||||
)
|
||||
logger.info(
|
||||
"Successfully fetched authenticated extended "
|
||||
"agent card:",
|
||||
)
|
||||
logger.info(
|
||||
_extended_card.model_dump_json(
|
||||
indent=2,
|
||||
exclude_none=True,
|
||||
),
|
||||
)
|
||||
final_agent_card_to_use = (
|
||||
_extended_card # Update to use the extended card
|
||||
)
|
||||
logger.info(
|
||||
"\nUsing AUTHENTICATED EXTENDED agent card "
|
||||
"for client initialization.",
|
||||
)
|
||||
except Exception as e_extended:
|
||||
logger.warning(
|
||||
"Failed to fetch extended agent card: %s. "
|
||||
"Will proceed with public card.",
|
||||
e_extended,
|
||||
exc_info=True,
|
||||
)
|
||||
elif (
|
||||
_public_card
|
||||
): # supports_authenticated_extended_card is False or None
|
||||
logger.info(
|
||||
"\nPublic card does not indicate support for an "
|
||||
"extended card. Using public card.",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Critical error fetching public agent card: %s",
|
||||
e,
|
||||
exc_info=True,
|
||||
)
|
||||
raise RuntimeError(
|
||||
"Failed to fetch the public agent card. Cannot continue.",
|
||||
) from e
|
||||
|
||||
# --8<-- [start:send_message]
|
||||
client = A2AClient(
|
||||
httpx_client=httpx_client,
|
||||
agent_card=final_agent_card_to_use,
|
||||
)
|
||||
logger.info("A2AClient initialized.")
|
||||
|
||||
send_message_payload: dict[str, Any] = {
|
||||
"message": {
|
||||
"role": "user",
|
||||
"parts": [
|
||||
{
|
||||
"kind": "text",
|
||||
"text": "find top 5 chinese restaurants in new york",
|
||||
},
|
||||
],
|
||||
"messageId": uuid4().hex,
|
||||
},
|
||||
}
|
||||
request = SendMessageRequest(
|
||||
id=str(uuid4()),
|
||||
params=MessageSendParams(**send_message_payload),
|
||||
)
|
||||
|
||||
response = await client.send_message(request)
|
||||
print(response.model_dump(mode="json", exclude_none=True))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import asyncio
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -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;
|
||||
};
|
||||
24
examples/agent/a2ui_agent/samples/client/lit/package.json
Normal file
24
examples/agent/a2ui_agent/samples/client/lit/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@a2ui/lit-samples",
|
||||
"private": true,
|
||||
"version": "0.8.1",
|
||||
"description": "A2UI Lit Samples",
|
||||
"workspaces": [
|
||||
"contact",
|
||||
"restaurant",
|
||||
"shell"
|
||||
],
|
||||
"scripts": {
|
||||
"serve:agent:restaurant": "cd ../../general_agent && uv run .",
|
||||
"serve:agent:contact_example": "cd ../../agent/as/contact_example && uv run .",
|
||||
"serve:agent:contact_multi_surface": "cd ../../agent/adk/contact_multiple_surfaces && uv run .",
|
||||
"serve:shell": "cd shell && npm run dev",
|
||||
"build:renderer": "cd ../../../renderers/web_core && npm install && npm run build && cd ../lit && npm install && npm run build",
|
||||
"demo:all": "npm install && npm run build:renderer && concurrently -k -n \"SHELL,REST,CONT1\" -c \"magenta,blue,green\" \"npm run serve:shell\" \"npm run serve:agent:restaurant\" \"npm run serve:agent:contact_example\"",
|
||||
"demo:restaurant": "npm install && npm run build:renderer && concurrently -k -n \"SHELL,REST\" -c \"magenta,blue\" \"npm run serve:shell\" \"npm run serve:agent:restaurant\"",
|
||||
"demo:contact_example": "npm install && npm run build:renderer && concurrently -k -n \"SHELL,CONT1\" -c \"magenta,green\" \"npm run serve:shell\" \"npm run serve:agent:contact_example\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "9.2.1"
|
||||
}
|
||||
}
|
||||
38
examples/agent/a2ui_agent/samples/client/lit/shell/README.md
Normal file
38
examples/agent/a2ui_agent/samples/client/lit/shell/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# 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](../../../general_agent/)
|
||||
- Run the dev server: `npm run dev`
|
||||
|
||||
After starting the dev server, you can open http://localhost:5173/ to view the sample.
|
||||
|
||||
Important: The sample code provided is for demonstration purposes and illustrates the mechanics of A2UI and the Agent-to-Agent (A2A) protocol. When building production applications, it is critical to treat any agent operating outside of your direct control as a potentially untrusted entity.
|
||||
|
||||
All operational data received from an external agent—including its AgentCard, messages, artifacts, and task statuses—should be handled as untrusted input. For example, a malicious agent could provide crafted data in its fields (e.g., name, skills.description) that, if used without sanitization to construct prompts for a Large Language Model (LLM), could expose your application to prompt injection attacks.
|
||||
|
||||
Similarly, any UI definition or data stream received must be treated as untrusted. Malicious agents could attempt to spoof legitimate interfaces to deceive users (phishing), inject malicious scripts via property values (XSS), or generate excessive layout complexity to degrade client performance (DoS). If your application supports optional embedded content (such as iframes or web views), additional care must be taken to prevent exposure to malicious external sites.
|
||||
|
||||
Developer Responsibility: Failure to properly validate data and strictly sandbox rendered content can introduce severe vulnerabilities. Developers are responsible for implementing appropriate security measures—such as input sanitization, Content Security Policies (CSP), strict isolation for optional embedded content, and secure credential handling—to protect their systems and users.
|
||||
163
examples/agent/a2ui_agent/samples/client/lit/shell/THEMING.md
Normal file
163
examples/agent/a2ui_agent/samples/client/lit/shell/THEMING.md
Normal file
@@ -0,0 +1,163 @@
|
||||
# A2UI Theming & Configuration Guide
|
||||
|
||||
This guide explains how the Universal App Shell handles theming and how to add new sample applications seamlessly.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The styling system is built on two distinct layers:
|
||||
|
||||
### 1. **Base Layer (`default-theme.ts`)**
|
||||
|
||||
- **Role**: Structural & Functional Styles.
|
||||
- **What it does**: Maps A2UI components (like `Text`, `Card`, `Row`) to functional CSS utility classes (e.g., `layout-w-100`, `typography-f-sf`).
|
||||
- **When to touch**: Rarely. Only if you need to change the fundamental layout behavior of a component across all shell apps.
|
||||
|
||||
### 2. **Configuration Layer (`configs/*.ts`)**
|
||||
|
||||
- **Role**: App Identity & Brand Overrides.
|
||||
- **What it does**: Allows for app-level theme overrides.
|
||||
- **Key Mechanism**: The `AppConfig` interface allows you to provide a new theme by setting items in the `theme` property.
|
||||
- **When to touch**: Whenever you add a new app and want to change an app's theme from the default theme provided with the shell.
|
||||
|
||||
---
|
||||
|
||||
## How to Add a New Sample App
|
||||
|
||||
Follow these steps to add a new application (e.g., "Flight Booker") with its own unique theme.
|
||||
|
||||
### Step 1: Create the Config
|
||||
|
||||
Create a new file `configs/flights.ts`:
|
||||
|
||||
```typescript
|
||||
import { AppConfig } from "./types.js";
|
||||
import { cloneDefaultTheme } from "../theme/clone-default-theme.js";
|
||||
|
||||
const theme = cloneDefaultTheme();
|
||||
// Set your variables, e.g., theme.components.Card = { 'color-bgc-n100': true }
|
||||
|
||||
export const config: AppConfig = {
|
||||
key: "flights",
|
||||
title: "Flight Booker",
|
||||
heroImage: "/hero-flights.png",
|
||||
heroImageDark: "/hero-flights-dark.png", // Optional
|
||||
placeholder: "Where do you want to go?",
|
||||
loadingText: ["Checking availability...", "Finding best rates..."],
|
||||
serverUrl: "http://localhost:10004", // Your agent's URL
|
||||
theme, // Apply the theme.
|
||||
};
|
||||
```
|
||||
|
||||
### Step 2: Register the Config
|
||||
|
||||
Update `app.ts` to include your new config:
|
||||
|
||||
```typescript
|
||||
import { config as flightsConfig } from "./configs/flights.js";
|
||||
|
||||
const configs: Record<string, AppConfig> = {
|
||||
restaurant: restaurantConfig,
|
||||
contacts: contactsConfig,
|
||||
flights: flightsConfig, // Add this line
|
||||
};
|
||||
```
|
||||
|
||||
### Step 3: Run It
|
||||
|
||||
Access your new app by adding the `app` query parameter:
|
||||
`http://localhost:5173/?app=flights`
|
||||
|
||||
The App Shell will automatically:
|
||||
|
||||
1. Load your `flights` config.
|
||||
2. Apply your theme to the A2UI root's theme context.
|
||||
3. Connect to your specified `serverUrl`.
|
||||
|
||||
---
|
||||
|
||||
## Reference: Styling Levers
|
||||
|
||||
This section lists the available styling "levers" (utility classes) you can use in your `theme.ts` file or directly in your components. These are defined in the core library (`renderers/lit/src/0.8/styles`).
|
||||
|
||||
### 1. Layout (`layout-`)
|
||||
|
||||
**Source:** `styles/layout.ts`
|
||||
|
||||
| Category | Prefix | Scale/Values | Examples |
|
||||
| :-------------- | :------------ | :------------------------------------------ | :---------------------------------------------------------- |
|
||||
| **Padding** | `layout-p-` | 0-24 (1 = 4px) | `layout-p-4` (16px), `layout-pt-2` (Top 8px), `layout-px-4` |
|
||||
| **Margin** | `layout-m-` | 0-24 (1 = 4px) | `layout-m-0`, `layout-mb-4` (Bottom 16px), `layout-mx-auto` |
|
||||
| **Gap** | `layout-g-` | 0-24 (1 = 4px) | `layout-g-2` (8px), `layout-g-4` (16px) |
|
||||
| **Width** | `layout-w-` | 10-100 (Percentage) | `layout-w-100` (100%), `layout-w-50` (50%) |
|
||||
| **Width (Px)** | `layout-wp-` | 0-15 (1 = 4px) | `layout-wp-10` (40px) |
|
||||
| **Height** | `layout-h-` | 10-100 (Percentage) | `layout-h-100` (100%) |
|
||||
| **Height (Px)** | `layout-hp-` | 0-15 (1 = 4px) | `layout-hp-10` (40px) |
|
||||
| **Display** | `layout-dsp-` | `none`, `block`, `grid`, `flex`, `iflex` | `layout-dsp-flexhor` (Row), `layout-dsp-flexvert` (Col) |
|
||||
| **Alignment** | `layout-al-` | `fs` (Start), `fe` (End), `c` (Center) | `layout-al-c` (Align Items Center) |
|
||||
| **Justify** | `layout-sp-` | `c` (Center), `bt` (Between), `ev` (Evenly) | `layout-sp-bt` (Justify Content Space Between) |
|
||||
| **Flex** | `layout-flx-` | `0` (None), `1` (Grow) | `layout-flx-1` (Flex Grow 1) |
|
||||
| **Position** | `layout-pos-` | `a` (Absolute), `rel` (Relative) | `layout-pos-rel` |
|
||||
|
||||
### 2. Colors (`color-`)
|
||||
|
||||
**Source:** `styles/colors.ts`
|
||||
|
||||
| Category | Prefix | Scale/Values | Examples |
|
||||
| :--------------- | :----------- | :------------------ | :-------------------------------------------------------------------- |
|
||||
| **Text Color** | `color-c-` | Palette Key + Shade | `color-c-p50` (Primary), `color-c-n10` (Black), `color-c-e40` (Error) |
|
||||
| **Background** | `color-bgc-` | Palette Key + Shade | `color-bgc-p100` (White/Lightest), `color-bgc-s30` (Secondary Dark) |
|
||||
| **Border Color** | `color-bc-` | Palette Key + Shade | `color-bc-p60` (Primary Border) |
|
||||
|
||||
**Palette Keys:**
|
||||
|
||||
- `p` = Primary (Brand)
|
||||
- `s` = Secondary
|
||||
- `t` = Tertiary
|
||||
- `n` = Neutral (Grays)
|
||||
- `nv` = Neutral Variant
|
||||
- `e` = Error
|
||||
|
||||
**Shades:** 0, 5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 95, 98, 99, 100
|
||||
|
||||
### 3. Typography (`typography-`)
|
||||
|
||||
**Source:** `styles/type.ts`
|
||||
|
||||
| Category | Prefix | Scale/Values | Examples |
|
||||
| :------------------ | :--------------- | :---------------------------------------- | :----------------------------------------------------------------------------------- |
|
||||
| **Font Family** | `typography-f-` | `sf` (Sans/Flex), `s` (Serif), `c` (Code) | `typography-f-sf` (System UI / Outfit) |
|
||||
| **Weight** | `typography-w-` | 100-900 | `typography-w-400` (Regular), `typography-w-500` (Medium), `typography-w-700` (Bold) |
|
||||
| **Size (Body)** | `typography-sz-` | `bs`, `bm`, `bl` | `typography-sz-bm` (Body Medium - 14px) |
|
||||
| **Size (Title)** | `typography-sz-` | `ts`, `tm`, `tl` | `typography-sz-tl` (Title Large - 22px) |
|
||||
| **Size (Headline)** | `typography-sz-` | `hs`, `hm`, `hl` | `typography-sz-hl` (Headline Large - 32px) |
|
||||
| **Size (Display)** | `typography-sz-` | `ds`, `dm`, `dl` | `typography-sz-dl` (Display Large - 57px) |
|
||||
| **Align** | `typography-ta-` | `s` (Start), `c` (Center) | `typography-ta-c` |
|
||||
|
||||
### 4. Borders (`border-`)
|
||||
|
||||
**Source:** `styles/border.ts`
|
||||
|
||||
| Category | Prefix | Scale/Values | Examples |
|
||||
| :--------- | :----------- | :------------- | :---------------------------------------------------- |
|
||||
| **Radius** | `border-br-` | 0-24 (1 = 4px) | `border-br-4` (16px), `border-br-50pc` (50% / Circle) |
|
||||
| **Width** | `border-bw-` | 0-24 (Pixels) | `border-bw-1` (1px), `border-bw-2` (2px) |
|
||||
| **Style** | `border-bs-` | `s` (Solid) | `border-bs-s` |
|
||||
|
||||
### 5. Behavior & Opacity
|
||||
|
||||
**Source:** `styles/behavior.ts`, `styles/opacity.ts`
|
||||
|
||||
| Category | Prefix | Scale/Values | Examples |
|
||||
| :---------------- | :------------- | :------------------------------------- | :-------------------------------------- |
|
||||
| **Hover Opacity** | `behavior-ho-` | 0-100 (Step 5) | `behavior-ho-80` (Opacity 0.8 on hover) |
|
||||
| **Opacity** | `opacity-el-` | 0-100 (Step 5) | `opacity-el-50` (Opacity 0.5) |
|
||||
| **Overflow** | `behavior-o-` | `s` (Scroll), `a` (Auto), `h` (Hidden) | `behavior-o-h` |
|
||||
| **Scrollbar** | `behavior-sw-` | `n` (None) | `behavior-sw-n` |
|
||||
|
||||
### 6. Icons
|
||||
|
||||
**Source:** `styles/icons.ts`
|
||||
|
||||
- Class: `.g-icon`
|
||||
- Variants: `.filled`, `.filled-heavy`
|
||||
- Usage: `<span class="g-icon">icon_name</span>`
|
||||
560
examples/agent/a2ui_agent/samples/client/lit/shell/app.ts
Normal file
560
examples/agent/a2ui_agent/samples/client/lit/shell/app.ts
Normal file
@@ -0,0 +1,560 @@
|
||||
/*
|
||||
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/default-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";
|
||||
|
||||
// App elements.
|
||||
import "./ui/ui.js";
|
||||
|
||||
// Configurations
|
||||
import { AppConfig } from "./configs/types.js";
|
||||
import { config as restaurantConfig } from "./configs/restaurant.js";
|
||||
import { config as contactsConfig } from "./configs/contacts.js";
|
||||
import { styleMap } from "lit/directives/style-map.js";
|
||||
|
||||
const configs: Record<string, AppConfig> = {
|
||||
restaurant: restaurantConfig,
|
||||
contacts: contactsConfig,
|
||||
};
|
||||
|
||||
@customElement("a2ui-shell")
|
||||
export class A2UILayoutEditor 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[] = [];
|
||||
|
||||
@state()
|
||||
accessor config: AppConfig = configs.restaurant;
|
||||
|
||||
@state()
|
||||
accessor #loadingTextIndex = 0;
|
||||
#loadingInterval: number | undefined;
|
||||
|
||||
static styles = [
|
||||
unsafeCSS(v0_8.Styles.structuralStyles),
|
||||
css`
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
min-height: 100%;
|
||||
color: light-dark(var(--n-10), var(--n-90));
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
#hero-img {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
aspect-ratio: 1280/720;
|
||||
height: auto;
|
||||
margin-bottom: var(--bb-grid-size-6);
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
background: var(--background-image-light) center center / contain
|
||||
no-repeat;
|
||||
}
|
||||
|
||||
#surfaces {
|
||||
width: 100%;
|
||||
max-width: 100svw;
|
||||
padding: var(--bb-grid-size-3);
|
||||
animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 0.3s backwards;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
& h1 {
|
||||
color: light-dark(var(--p-40), var(--n-90));
|
||||
}
|
||||
|
||||
& > 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);
|
||||
background: light-dark(var(--n-100), var(--n-10));
|
||||
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;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: fadeIn 1s cubic-bezier(0, 0, 0.3, 1) 0.3s backwards;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid rgba(255, 255, 255, 0.1);
|
||||
border-left-color: var(--p-60);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: fixed;
|
||||
top: var(--bb-grid-size-3);
|
||||
right: var(--bb-grid-size-4);
|
||||
background: light-dark(var(--n-100), var(--n-0));
|
||||
border-radius: 50%;
|
||||
color: var(--p-30);
|
||||
cursor: pointer;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
font-size: 32px;
|
||||
|
||||
& .g-icon {
|
||||
pointer-events: none;
|
||||
|
||||
&::before {
|
||||
content: "dark_mode";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@container style(--color-scheme: dark) {
|
||||
.theme-toggle .g-icon::before {
|
||||
content: "light_mode";
|
||||
color: var(--n-90);
|
||||
}
|
||||
|
||||
#hero-img {
|
||||
background-image: var(--background-image-dark);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}> = [];
|
||||
|
||||
#maybeRenderError() {
|
||||
if (!this.#error) return nothing;
|
||||
|
||||
return html`<div class="error">${this.#error}</div>`;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
// Load config from URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const appKey = urlParams.get("app") || "restaurant";
|
||||
this.config = configs[appKey] || configs.restaurant;
|
||||
|
||||
// Apply the theme directly, which will use the Lit context.
|
||||
if (this.config.theme) {
|
||||
this.theme = this.config.theme;
|
||||
}
|
||||
|
||||
window.document.title = this.config.title;
|
||||
window.document.documentElement.style.setProperty(
|
||||
"--background",
|
||||
this.config.background
|
||||
);
|
||||
|
||||
// Initialize client with configured URL
|
||||
this.#a2uiClient = new A2UIClient(this.config.serverUrl);
|
||||
}
|
||||
|
||||
render() {
|
||||
return [
|
||||
this.#renderThemeToggle(),
|
||||
this.#maybeRenderForm(),
|
||||
this.#maybeRenderData(),
|
||||
this.#maybeRenderError(),
|
||||
];
|
||||
}
|
||||
|
||||
#renderThemeToggle() {
|
||||
return html` <div>
|
||||
<button
|
||||
@click=${(evt: Event) => {
|
||||
if (!(evt.target instanceof HTMLButtonElement)) return;
|
||||
const { colorScheme } = window.getComputedStyle(evt.target);
|
||||
if (colorScheme === "dark") {
|
||||
document.body.classList.add("light");
|
||||
document.body.classList.remove("dark");
|
||||
} else {
|
||||
document.body.classList.add("dark");
|
||||
document.body.classList.remove("light");
|
||||
}
|
||||
}}
|
||||
class="theme-toggle"
|
||||
>
|
||||
<span class="g-icon filled-heavy"></span>
|
||||
</button>
|
||||
</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);
|
||||
}}
|
||||
>
|
||||
${this.config.heroImage
|
||||
? html`<div
|
||||
style=${styleMap({
|
||||
"--background-image-light": `url(${this.config.heroImage})`,
|
||||
"--background-image-dark": `url(${this.config.heroImageDark ?? this.config.heroImage
|
||||
})`,
|
||||
})}
|
||||
id="hero-img"
|
||||
></div>`
|
||||
: nothing}
|
||||
<h1 class="app-title">${this.config.title}</h1>
|
||||
<div>
|
||||
<input
|
||||
required
|
||||
value="${this.config.placeholder}"
|
||||
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>`;
|
||||
}
|
||||
|
||||
#startLoadingAnimation() {
|
||||
if (
|
||||
Array.isArray(this.config.loadingText) &&
|
||||
this.config.loadingText.length > 1
|
||||
) {
|
||||
this.#loadingTextIndex = 0;
|
||||
this.#loadingInterval = window.setInterval(() => {
|
||||
this.#loadingTextIndex =
|
||||
(this.#loadingTextIndex + 1) %
|
||||
(this.config.loadingText as string[]).length;
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
#stopLoadingAnimation() {
|
||||
if (this.#loadingInterval) {
|
||||
clearInterval(this.#loadingInterval);
|
||||
this.#loadingInterval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async #sendMessage(
|
||||
message: v0_8.Types.A2UIClientEventMessage
|
||||
): Promise<v0_8.Types.ServerToClientMessage[]> {
|
||||
try {
|
||||
this.#requesting = true;
|
||||
this.#startLoadingAnimation();
|
||||
const response = this.#a2uiClient.send(message);
|
||||
await response;
|
||||
this.#requesting = false;
|
||||
this.#stopLoadingAnimation();
|
||||
|
||||
return response;
|
||||
} catch (err) {
|
||||
this.snackbar(err as string, SnackType.ERROR);
|
||||
} finally {
|
||||
this.#requesting = false;
|
||||
this.#stopLoadingAnimation();
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
#maybeRenderData() {
|
||||
if (this.#requesting) {
|
||||
let text = "Awaiting an answer...";
|
||||
if (this.config.loadingText) {
|
||||
if (Array.isArray(this.config.loadingText)) {
|
||||
text = this.config.loadingText[this.#loadingTextIndex];
|
||||
} else {
|
||||
text = this.config.loadingText;
|
||||
}
|
||||
}
|
||||
|
||||
return html` <div class="pending">
|
||||
<div class="spinner"></div>
|
||||
<div class="loading-text">${text}</div>
|
||||
</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: {
|
||||
name: evt.detail.action.name,
|
||||
surfaceId,
|
||||
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);
|
||||
|
||||
console.log(messages);
|
||||
|
||||
this.#lastMessages = messages;
|
||||
this.#processor.clearSurfaces();
|
||||
this.#processor.processMessages(messages);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
127
examples/agent/a2ui_agent/samples/client/lit/shell/client.ts
Normal file
127
examples/agent/a2ui_agent/samples/client/lit/shell/client.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
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 { Message, Part, SendMessageSuccessResponse, Task } from "@a2a-js/sdk";
|
||||
import { A2AClient } from "@a2a-js/sdk/client";
|
||||
import { v0_8 } from "@a2ui/lit";
|
||||
|
||||
const A2AUI_MIME_TYPE = "application/json+a2aui";
|
||||
|
||||
export class A2UIClient {
|
||||
#serverUrl: string;
|
||||
#client: A2AClient | null = null;
|
||||
|
||||
constructor(serverUrl: string = "") {
|
||||
this.#serverUrl = serverUrl;
|
||||
}
|
||||
|
||||
#ready: Promise<void> = Promise.resolve();
|
||||
get ready() {
|
||||
return this.#ready;
|
||||
}
|
||||
|
||||
async #getClient() {
|
||||
if (!this.#client) {
|
||||
// Default to localhost:10002 if no URL provided (fallback for restaurant app default)
|
||||
const baseUrl = this.#serverUrl || "http://localhost:10002";
|
||||
|
||||
this.#client = await A2AClient.fromCardUrl(
|
||||
`${baseUrl}/.well-known/agent-card.json`,
|
||||
{
|
||||
fetchImpl: async (url, init) => {
|
||||
const headers = new Headers(init?.headers);
|
||||
headers.set("X-A2A-Extensions", "https://a2ui.org/a2a-extension/a2ui/v0.8");
|
||||
return fetch(url, { ...init, headers });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
return this.#client;
|
||||
}
|
||||
|
||||
async send(
|
||||
message: v0_8.Types.A2UIClientEventMessage | string
|
||||
): Promise<v0_8.Types.ServerToClientMessage[]> {
|
||||
const client = await this.#getClient();
|
||||
|
||||
let parts: Part[] = [];
|
||||
|
||||
if (typeof message === 'string') {
|
||||
// Try to parse as JSON first, just in case
|
||||
try {
|
||||
const parsed = JSON.parse(message);
|
||||
if (typeof parsed === 'object' && parsed !== null) {
|
||||
parts = [{
|
||||
kind: "data",
|
||||
data: parsed as unknown as Record<string, unknown>,
|
||||
mimeType: A2AUI_MIME_TYPE,
|
||||
} as Part];
|
||||
} else {
|
||||
parts = [{ kind: "text", text: message }];
|
||||
}
|
||||
} catch {
|
||||
parts = [{ kind: "text", text: message }];
|
||||
}
|
||||
} else {
|
||||
parts = [{
|
||||
kind: "data",
|
||||
data: message as unknown as Record<string, unknown>,
|
||||
mimeType: A2AUI_MIME_TYPE,
|
||||
} as Part];
|
||||
}
|
||||
|
||||
const response = await client.sendMessage({
|
||||
message: {
|
||||
messageId: crypto.randomUUID(),
|
||||
role: "user",
|
||||
parts: parts,
|
||||
kind: "message",
|
||||
},
|
||||
});
|
||||
|
||||
if ("error" in response) {
|
||||
throw new Error(response.error.message);
|
||||
}
|
||||
|
||||
const result = (response as SendMessageSuccessResponse).result as Task;
|
||||
if (result.kind === "task" && result.status.message?.parts) {
|
||||
const messages: v0_8.Types.ServerToClientMessage[] = [];
|
||||
for (const part of result.status.message.parts) {
|
||||
if (part.kind === 'data') {
|
||||
messages.push(part.data as v0_8.Types.ServerToClientMessage);
|
||||
}
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
// const result = (response as SendMessageSuccessResponse).result as Message;
|
||||
// if (result.kind === "message" && result.parts) {
|
||||
// const messages: v0_8.Types.ServerToClientMessage[] = [];
|
||||
// for (const part of result.parts) {
|
||||
// if (part.kind === 'data') {
|
||||
// // Parse data.arguments (JSON string) to object
|
||||
// const parsedData = typeof part.data?.arguments === 'string'
|
||||
// ? JSON.parse(part.data.arguments)
|
||||
// : part.data?.arguments;
|
||||
// messages.push(parsedData as v0_8.Types.ServerToClientMessage);
|
||||
// }
|
||||
// }
|
||||
// return messages;
|
||||
// }
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
/*
|
||||
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 { cloneDefaultTheme } from "../theme/clone-default-theme.js";
|
||||
import { AppConfig } from "./types.js";
|
||||
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 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 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 theme: v0_8.Types.Theme = {
|
||||
...cloneDefaultTheme(),
|
||||
additionalStyles: {
|
||||
Card: {
|
||||
"min-width": "320px",
|
||||
"max-width": "400px",
|
||||
margin: "0 auto",
|
||||
background:
|
||||
"linear-gradient(135deg, light-dark(#ffffff99, #ffffff44) 0%, light-dark(#ffffff, #ffffff04) 100%)",
|
||||
border: "1px solid light-dark(transparent, #ffffff35)",
|
||||
boxShadow:
|
||||
"inset 0 20px 48px light-dark(rgba(0, 0, 0, 0.02), rgba(255, 255, 255, 0.08))",
|
||||
},
|
||||
Button: {
|
||||
"--p-70": "light-dark(var(--p-60), var(--n-10))",
|
||||
"--n-60": "light-dark(var(--n-100), var(--n-0))",
|
||||
},
|
||||
Image: {
|
||||
"max-width": "120px",
|
||||
"max-height": "120px",
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
},
|
||||
Text: {
|
||||
"--n-40": "light-dark(var(--p-60), var(--n-90))",
|
||||
},
|
||||
},
|
||||
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,
|
||||
"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,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const config: AppConfig = {
|
||||
key: "contacts",
|
||||
title: "Contact Manager",
|
||||
background: `radial-gradient(at 0% 0%, light-dark(rgba(45, 212, 191, 0.4), rgba(20, 184, 166, 0.2)) 0px, transparent 50%),
|
||||
radial-gradient(at 100% 0%, light-dark(rgba(56, 189, 248, 0.4), rgba(14, 165, 233, 0.2)) 0px, transparent 50%),
|
||||
radial-gradient(at 100% 100%, light-dark(rgba(163, 230, 53, 0.4), rgba(132, 204, 22, 0.2)) 0px, transparent 50%),
|
||||
radial-gradient(at 0% 100%, light-dark(rgba(52, 211, 153, 0.4), rgba(16, 185, 129, 0.2)) 0px, transparent 50%),
|
||||
linear-gradient(120deg, light-dark(#f0fdf4, #022c22) 0%, light-dark(#dcfce7, #064e3b) 100%)`,
|
||||
placeholder: "Alex Jordan",
|
||||
loadingText: [
|
||||
"Searching contacts...",
|
||||
"Looking up details...",
|
||||
"Verifying information...",
|
||||
"Just a moment...",
|
||||
],
|
||||
serverUrl: "http://localhost:10003",
|
||||
theme,
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
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 { AppConfig } from "./types.js";
|
||||
|
||||
export const config: AppConfig = {
|
||||
key: "restaurant",
|
||||
title: "A2UI Agent",
|
||||
heroImage: "/hero.png",
|
||||
heroImageDark: "/hero-dark.png",
|
||||
background: `radial-gradient(
|
||||
at 0% 0%,
|
||||
light-dark(rgba(161, 196, 253, 0.3), rgba(6, 182, 212, 0.15)) 0px,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
at 100% 0%,
|
||||
light-dark(rgba(255, 226, 226, 0.3), rgba(59, 130, 246, 0.15)) 0px,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
at 100% 100%,
|
||||
light-dark(rgba(162, 210, 255, 0.3), rgba(20, 184, 166, 0.15)) 0px,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
at 0% 100%,
|
||||
light-dark(rgba(255, 200, 221, 0.3), rgba(99, 102, 241, 0.15)) 0px,
|
||||
transparent 50%
|
||||
),
|
||||
linear-gradient(
|
||||
120deg,
|
||||
light-dark(#f0f4f8, #0f172a) 0%,
|
||||
light-dark(#e2e8f0, #1e293b) 100%
|
||||
)`,
|
||||
placeholder: "Help me test my English.",
|
||||
loadingText: [
|
||||
"A2UI Agent is working on your request...",
|
||||
"Analyzing your information...",
|
||||
"Working...",
|
||||
],
|
||||
serverUrl: "http://localhost:10002",
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
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";
|
||||
|
||||
/**
|
||||
* Configuration interface for the Universal App Shell.
|
||||
*/
|
||||
export interface AppConfig {
|
||||
/** Unique key for the app (e.g., 'restaurant', 'contacts') */
|
||||
key: string;
|
||||
/** Display title of the application */
|
||||
title: string;
|
||||
/** The background for the page */
|
||||
background?: string;
|
||||
/** Path to the hero image */
|
||||
heroImage?: string;
|
||||
/** Path to the hero image */
|
||||
heroImageDark?: string;
|
||||
/** Placeholder text for the input field */
|
||||
placeholder: string;
|
||||
/** Text to display while loading (optional). Can be a single string or an array of strings to rotate. */
|
||||
loadingText?: string | string[];
|
||||
/** Optional server URL for the agent (e.g., http://localhost:10003) */
|
||||
serverUrl?: string;
|
||||
/** Theme overrides (CSS Variables) */
|
||||
theme?: v0_8.Types.Theme;
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
246
examples/agent/a2ui_agent/samples/client/lit/shell/index.html
Normal file
246
examples/agent/a2ui_agent/samples/client/lit/shell/index.html
Normal file
@@ -0,0 +1,246 @@
|
||||
<!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>Loading...</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,dark_mode,delete,download,draw,edit,error,event,favorite,favorite_off,folder,help,home,info,light_mode,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"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Outfit:wght@100..900&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<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-dark(#f6f7f8, #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);
|
||||
|
||||
--background: radial-gradient(
|
||||
at 0% 0%,
|
||||
light-dark(rgba(161, 196, 253, 0.3), rgba(6, 182, 212, 0.15)) 0px,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
at 100% 0%,
|
||||
light-dark(rgba(255, 226, 226, 0.3), rgba(59, 130, 246, 0.15)) 0px,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
at 100% 100%,
|
||||
light-dark(rgba(162, 210, 255, 0.3), rgba(20, 184, 166, 0.15)) 0px,
|
||||
transparent 50%
|
||||
),
|
||||
radial-gradient(
|
||||
at 0% 100%,
|
||||
light-dark(rgba(255, 200, 221, 0.3), rgba(99, 102, 241, 0.15)) 0px,
|
||||
transparent 50%
|
||||
),
|
||||
linear-gradient(
|
||||
120deg,
|
||||
light-dark(#f0f4f8, #0f172a) 0%,
|
||||
light-dark(#e2e8f0, #1e293b) 100%
|
||||
);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
--font-family: "Outfit", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
--font-family-flex: "Outfit", "Helvetica Neue", Helvetica, Arial,
|
||||
sans-serif;
|
||||
--font-family-mono: monospace;
|
||||
|
||||
background: var(--background);
|
||||
font-family: var(--font-family);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100svw;
|
||||
height: 100svh;
|
||||
overflow: auto;
|
||||
|
||||
--color-scheme: light;
|
||||
color-scheme: var(--color-scheme);
|
||||
|
||||
&.light {
|
||||
--color-scheme: light;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
--color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html,
|
||||
body {
|
||||
--color-scheme: dark;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<a2ui-shell></a2ui-shell>
|
||||
<script type="module" src="./app.ts"></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,86 @@
|
||||
{
|
||||
"name": "@a2ui/shell",
|
||||
"private": true,
|
||||
"version": "0.8.1",
|
||||
"description": "A2UI Universal Shell",
|
||||
"main": "./dist/shell.js",
|
||||
"types": "./dist/shell.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/shell",
|
||||
"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",
|
||||
"@google/genai": "^1.22.0",
|
||||
"@lit-labs/signals": "^0.1.3",
|
||||
"@lit/context": "^1.1.4",
|
||||
"@types/node": "^24.7.1",
|
||||
"lit": "^3.3.1"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 249 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 296 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 278 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 298 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 190 KiB |
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
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 { theme } from "./default-theme.js";
|
||||
import { v0_8 } from "@a2ui/lit";
|
||||
|
||||
export function cloneDefaultTheme(): v0_8.Types.Theme {
|
||||
return structuredClone(theme);
|
||||
}
|
||||
@@ -0,0 +1,442 @@
|
||||
/*
|
||||
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,
|
||||
"typography-td-none": true,
|
||||
"color-c-p40": 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,
|
||||
"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,
|
||||
};
|
||||
|
||||
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,
|
||||
"color-c-n10": 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,
|
||||
"color-c-n10": 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,
|
||||
"color-c-n10": 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, {});
|
||||
const inputLight = v0_8.Styles.merge(input, {});
|
||||
const textareaLight = v0_8.Styles.merge(textarea, {});
|
||||
const buttonLight = v0_8.Styles.merge(button, {});
|
||||
const bodyLight = v0_8.Styles.merge(body, {});
|
||||
const pLight = v0_8.Styles.merge(p, {});
|
||||
const preLight = v0_8.Styles.merge(pre, {});
|
||||
const orderedListLight = v0_8.Styles.merge(orderedList, {});
|
||||
const unorderedListLight = v0_8.Styles.merge(unorderedList, {});
|
||||
const listItemLight = v0_8.Styles.merge(listItem, {});
|
||||
|
||||
export const theme: v0_8.Types.Theme = {
|
||||
additionalStyles: {
|
||||
Button: {
|
||||
"--n-35": "var(--n-100)",
|
||||
"--n-10": "var(--n-0)",
|
||||
background:
|
||||
"linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%)",
|
||||
boxShadow: "0 4px 15px rgba(102, 126, 234, 0.4)",
|
||||
padding: "12px 28px",
|
||||
textTransform: "uppercase",
|
||||
},
|
||||
Text: {
|
||||
h1: {
|
||||
color: "transparent",
|
||||
background:
|
||||
"linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%)",
|
||||
"-webkit-background-clip": "text",
|
||||
"background-clip": "text",
|
||||
"-webkit-text-fill-color": "transparent",
|
||||
},
|
||||
h2: {
|
||||
color: "transparent",
|
||||
background:
|
||||
"linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%)",
|
||||
"-webkit-background-clip": "text",
|
||||
"background-clip": "text",
|
||||
"-webkit-text-fill-color": "transparent",
|
||||
},
|
||||
h3: {
|
||||
color: "transparent",
|
||||
background:
|
||||
"linear-gradient(135deg, light-dark(#818cf8, #06b6d4) 0%, light-dark(#a78bfa, #3b82f6) 100%)",
|
||||
"-webkit-background-clip": "text",
|
||||
"background-clip": "text",
|
||||
"-webkit-text-fill-color": "transparent",
|
||||
},
|
||||
h4: {},
|
||||
h5: {},
|
||||
body: {},
|
||||
caption: {},
|
||||
},
|
||||
Card: {
|
||||
background:
|
||||
"radial-gradient(circle at top left, light-dark(transparent, rgba(6, 182, 212, 0.15)), transparent 40%), radial-gradient(circle at bottom right, light-dark(transparent, rgba(139, 92, 246, 0.15)), transparent 40%), linear-gradient(135deg, light-dark(rgba(255, 255, 255, 0.7), rgba(30, 41, 59, 0.7)), light-dark(rgba(255, 255, 255, 0.7), rgba(15, 23, 42, 0.8)))",
|
||||
},
|
||||
TextField: {
|
||||
"--p-0": "light-dark(var(--n-0), #1e293b)",
|
||||
},
|
||||
},
|
||||
components: {
|
||||
AudioPlayer: {},
|
||||
Button: {
|
||||
"layout-pt-2": true,
|
||||
"layout-pb-2": true,
|
||||
"layout-pl-3": true,
|
||||
"layout-pr-3": true,
|
||||
"border-br-12": true,
|
||||
"border-bw-0": true,
|
||||
"border-bs-s": true,
|
||||
"color-bgc-p30": true,
|
||||
"behavior-ho-70": true,
|
||||
"typography-w-400": true,
|
||||
},
|
||||
Card: { "border-br-9": true, "layout-p-4": true, "color-bgc-n100": 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: {
|
||||
"layout-g-2": true,
|
||||
},
|
||||
DateTimeInput: {
|
||||
container: {
|
||||
"typography-sz-bm": true,
|
||||
"layout-w-100": true,
|
||||
"layout-g-2": true,
|
||||
"layout-dsp-flexhor": true,
|
||||
"layout-al-c": true,
|
||||
"typography-ws-nw": true,
|
||||
},
|
||||
label: {
|
||||
"color-c-p30": true,
|
||||
"typography-sz-bm": true,
|
||||
},
|
||||
element: {
|
||||
"layout-pt-2": true,
|
||||
"layout-pb-2": true,
|
||||
"layout-pl-3": true,
|
||||
"layout-pr-3": true,
|
||||
"border-br-2": 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,
|
||||
},
|
||||
},
|
||||
Divider: {},
|
||||
Image: {
|
||||
all: {
|
||||
"border-br-5": true,
|
||||
"layout-el-cv": true,
|
||||
"layout-w-100": true,
|
||||
"layout-h-100": true,
|
||||
},
|
||||
avatar: { "is-avatar": true },
|
||||
header: {},
|
||||
icon: {},
|
||||
largeFeature: {},
|
||||
mediumFeature: {},
|
||||
smallFeature: {},
|
||||
},
|
||||
Icon: {},
|
||||
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,
|
||||
},
|
||||
Slider: {
|
||||
container: {},
|
||||
label: {},
|
||||
element: {},
|
||||
},
|
||||
Tabs: {
|
||||
container: {},
|
||||
controls: { all: {}, selected: {} },
|
||||
element: {},
|
||||
},
|
||||
Text: {
|
||||
all: {
|
||||
"layout-w-100": true,
|
||||
"layout-g-2": true,
|
||||
},
|
||||
h1: {
|
||||
"typography-f-sf": true,
|
||||
"typography-v-r": true,
|
||||
"typography-w-400": true,
|
||||
"layout-m-0": true,
|
||||
"layout-p-0": true,
|
||||
"typography-sz-hs": true,
|
||||
},
|
||||
h2: {
|
||||
"typography-f-sf": true,
|
||||
"typography-v-r": true,
|
||||
"typography-w-400": true,
|
||||
"layout-m-0": true,
|
||||
"layout-p-0": true,
|
||||
"typography-sz-tl": true,
|
||||
},
|
||||
h3: {
|
||||
"typography-f-sf": true,
|
||||
"typography-v-r": true,
|
||||
"typography-w-400": true,
|
||||
"layout-m-0": true,
|
||||
"layout-p-0": true,
|
||||
"typography-sz-tl": true,
|
||||
},
|
||||
h4: {
|
||||
"typography-f-sf": true,
|
||||
"typography-v-r": true,
|
||||
"typography-w-400": true,
|
||||
"layout-m-0": true,
|
||||
"layout-p-0": true,
|
||||
"typography-sz-bl": true,
|
||||
},
|
||||
h5: {
|
||||
"typography-f-sf": true,
|
||||
"typography-v-r": true,
|
||||
"typography-w-400": true,
|
||||
"layout-m-0": true,
|
||||
"layout-p-0": true,
|
||||
"typography-sz-bm": 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,
|
||||
"typography-ws-nw": true,
|
||||
},
|
||||
label: {
|
||||
"layout-flx-0": true,
|
||||
"color-c-p30": true,
|
||||
},
|
||||
element: {
|
||||
"typography-sz-bm": true,
|
||||
"layout-pt-2": true,
|
||||
"layout-pb-2": true,
|
||||
"layout-pl-3": true,
|
||||
"layout-pr-3": true,
|
||||
"border-br-2": 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: heading,
|
||||
h2: heading,
|
||||
h3: heading,
|
||||
h4: heading,
|
||||
h5: heading,
|
||||
iframe,
|
||||
input: inputLight,
|
||||
p: pLight,
|
||||
pre: preLight,
|
||||
textarea: textareaLight,
|
||||
video,
|
||||
},
|
||||
markdown: {
|
||||
p: [...Object.keys(pLight)],
|
||||
h1: [...Object.keys(heading)],
|
||||
h2: [...Object.keys(heading)],
|
||||
h3: [...Object.keys(heading)],
|
||||
h4: [...Object.keys(heading)],
|
||||
h5: [...Object.keys(heading)],
|
||||
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,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>`;
|
||||
}
|
||||
}
|
||||
17
examples/agent/a2ui_agent/samples/client/lit/shell/ui/ui.ts
Normal file
17
examples/agent/a2ui_agent/samples/client/lit/shell/ui/ui.ts
Normal file
@@ -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> = {
|
||||
shell: 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