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:
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user