Skip to content

Commit 385c9f5

Browse files
committed
feat: add GoogleMap component with client-side API key decision logic
- Add google-map.ts component extending v0_8.UI.Root with center, zoom, pins properties - Register GoogleMap component and rizzchartsConfig in app.ts - Add rizzcharts.ts configuration for rizzcharts agent - Switch default catalog to RIZZCHARTS_CATALOG_URI in agent_executor.py - Add verify_converter.py for A2UI validation testing
1 parent dbc4187 commit 385c9f5

File tree

5 files changed

+315
-1
lines changed

5 files changed

+315
-1
lines changed

samples/agent/adk/rizzcharts/agent_executor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def __init__(self, base_url: str):
5858
STANDARD_CATALOG_ID: str(spec_root.joinpath("standard_catalog_definition.json")),
5959
RIZZCHARTS_CATALOG_URI: "rizzcharts_catalog_definition.json",
6060
},
61-
default_catalog_uri=STANDARD_CATALOG_ID
61+
default_catalog_uri=RIZZCHARTS_CATALOG_URI
6262
)
6363
agent = rizzchartsAgent.build_agent()
6464
runner = Runner(
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import json
2+
import jsonschema
3+
from pathlib import Path
4+
from component_catalog_builder import ComponentCatalogBuilder
5+
from a2ui.a2ui_extension import STANDARD_CATALOG_ID
6+
7+
def verify():
8+
print("Verifying A2UI Part Converter Logic...")
9+
10+
spec_root = Path(__file__).parent / "../../../../specification/0.8/json"
11+
schema_path = str(spec_root.joinpath("server_to_client.json"))
12+
catalog_path = str(spec_root.joinpath("standard_catalog_definition.json"))
13+
14+
print(f"Schema Path: {schema_path}")
15+
print(f"Catalog Path: {catalog_path}")
16+
17+
builder = ComponentCatalogBuilder(
18+
a2ui_schema_path=schema_path,
19+
uri_to_local_catalog_path={
20+
STANDARD_CATALOG_ID: catalog_path,
21+
},
22+
default_catalog_uri=STANDARD_CATALOG_ID
23+
)
24+
25+
# Load schema ( simulating default client without rizzcharts capabilities)
26+
a2ui_schema, uri = builder.load_a2ui_schema(client_ui_capabilities=None)
27+
print(f"Loaded Schema for Catalog: {uri}")
28+
29+
# Prepare array schema as tool does
30+
a2ui_schema_object = {"type": "array", "items": a2ui_schema}
31+
32+
# Test Chart JSON
33+
try:
34+
print("\n--- Testing chart.json ---")
35+
chart_str = Path("examples/standard_catalog/chart.json").read_text()
36+
chart_json = json.loads(chart_str)
37+
jsonschema.validate(instance=chart_json, schema=a2ui_schema_object)
38+
print("SUCCESS: chart.json passed validation.")
39+
except Exception as e:
40+
print(f"FAILURE: chart.json failed validation: {e}")
41+
42+
# Test Map JSON
43+
try:
44+
print("\n--- Testing map.json ---")
45+
map_str = Path("examples/standard_catalog/map.json").read_text()
46+
map_json = json.loads(map_str)
47+
jsonschema.validate(instance=map_json, schema=a2ui_schema_object)
48+
print("SUCCESS: map.json passed validation.")
49+
except Exception as e:
50+
print(f"FAILURE: map.json failed validation: {e}")
51+
52+
if __name__ == "__main__":
53+
verify()

samples/client/lit/shell/app.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,19 @@ import "./ui/ui.js";
4545
import { AppConfig } from "./configs/types.js";
4646
import { config as restaurantConfig } from "./configs/restaurant.js";
4747
import { config as contactsConfig } from "./configs/contacts.js";
48+
import { config as rizzchartsConfig } from "./configs/rizzcharts.js";
4849
import { styleMap } from "lit/directives/style-map.js";
4950

51+
// Custom components
52+
import { GoogleMap } from "./ui/google-map.js";
53+
54+
// Register custom components with A2UI
55+
v0_8.UI.componentRegistry.register("GoogleMap", GoogleMap);
56+
5057
const configs: Record<string, AppConfig> = {
5158
restaurant: restaurantConfig,
5259
contacts: contactsConfig,
60+
rizzcharts: rizzchartsConfig,
5361
};
5462

5563
@customElement("a2ui-shell")
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
Copyright 2025 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { AppConfig } from "./types.js";
18+
19+
export const config: AppConfig = {
20+
key: "rizzcharts",
21+
title: "Rizzcharts",
22+
heroImage: "/hero.png",
23+
heroImageDark: "/hero-dark.png",
24+
background: `radial-gradient(
25+
at 0% 0%,
26+
light-dark(rgba(161, 196, 253, 0.3), rgba(6, 182, 212, 0.15)) 0px,
27+
transparent 50%
28+
),
29+
radial-gradient(
30+
at 100% 0%,
31+
light-dark(rgba(255, 226, 226, 0.3), rgba(59, 130, 246, 0.15)) 0px,
32+
transparent 50%
33+
),
34+
radial-gradient(
35+
at 100% 100%,
36+
light-dark(rgba(162, 210, 255, 0.3), rgba(20, 184, 166, 0.15)) 0px,
37+
transparent 50%
38+
),
39+
radial-gradient(
40+
at 0% 100%,
41+
light-dark(rgba(255, 200, 221, 0.3), rgba(99, 102, 241, 0.15)) 0px,
42+
transparent 50%
43+
),
44+
linear-gradient(
45+
120deg,
46+
light-dark(#f0f4f8, #0f172a) 0%,
47+
light-dark(#e2e8f0, #1e293b) 100%
48+
)`,
49+
placeholder: "Show my sales data for Q4",
50+
loadingText: [
51+
"Crunching the numbers...",
52+
"Generating charts...",
53+
"Analyzing data...",
54+
"Almost there...",
55+
],
56+
serverUrl: "http://localhost:10005",
57+
};
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/*
2+
Copyright 2025 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { html, css, nothing } from "lit";
18+
import { customElement, property } from "lit/decorators.js";
19+
import { v0_8 } from "@a2ui/lit";
20+
21+
// Get API key from environment (Vite injects this at build time)
22+
const GOOGLE_API_KEY = (import.meta as any).env?.VITE_GOOGLE_API_KEY || "";
23+
24+
// Local placeholder image path (served from public directory)
25+
const LOCAL_PLACEHOLDER_IMAGE = "/map-placeholder.png";
26+
27+
interface LatLng {
28+
lat: number;
29+
lng: number;
30+
}
31+
32+
interface MapPin {
33+
lat: number;
34+
lng: number;
35+
name?: string;
36+
description?: string;
37+
background?: string;
38+
borderColor?: string;
39+
glyphColor?: string;
40+
}
41+
42+
interface PathValue {
43+
path: string;
44+
}
45+
46+
@customElement("a2ui-custom-googlemap")
47+
export class GoogleMap extends v0_8.UI.Root {
48+
@property({ attribute: false })
49+
accessor center: PathValue | LatLng | null = null;
50+
51+
@property({ attribute: false })
52+
accessor zoom: PathValue | number | null = null;
53+
54+
@property({ attribute: false })
55+
accessor pins: PathValue | MapPin[] | null = null;
56+
57+
static styles = [
58+
css`
59+
:host {
60+
display: block;
61+
flex: var(--weight);
62+
min-height: 200px;
63+
overflow: hidden;
64+
border-radius: 8px;
65+
}
66+
67+
.map-container {
68+
width: 100%;
69+
height: 100%;
70+
min-height: 300px;
71+
display: flex;
72+
align-items: center;
73+
justify-content: center;
74+
background: #e8e8e8;
75+
border-radius: 8px;
76+
overflow: hidden;
77+
}
78+
79+
img {
80+
width: 100%;
81+
height: 100%;
82+
object-fit: cover;
83+
}
84+
85+
.placeholder-container {
86+
display: flex;
87+
flex-direction: column;
88+
align-items: center;
89+
justify-content: center;
90+
gap: 16px;
91+
padding: 24px;
92+
text-align: center;
93+
}
94+
95+
.placeholder-icon {
96+
font-size: 48px;
97+
color: #5f6368;
98+
}
99+
100+
.placeholder-text {
101+
color: #5f6368;
102+
font-size: 14px;
103+
}
104+
105+
.pin-list {
106+
margin-top: 8px;
107+
font-size: 12px;
108+
color: #80868b;
109+
}
110+
`,
111+
];
112+
113+
/**
114+
* Resolves a value that might be a path binding or a literal value.
115+
*/
116+
private resolveValue<T>(value: PathValue | T | null): T | null {
117+
if (!value) return null;
118+
119+
if (typeof value === "object" && "path" in value && value.path) {
120+
if (!this.processor || !this.component) {
121+
return null;
122+
}
123+
return this.processor.getData(
124+
this.component,
125+
value.path,
126+
this.surfaceId ?? v0_8.Data.A2uiMessageProcessor.DEFAULT_SURFACE_ID
127+
) as T | null;
128+
}
129+
130+
return value as T;
131+
}
132+
133+
/**
134+
* Builds a Google Maps Static API URL from the resolved data.
135+
*/
136+
private buildGoogleMapsUrl(
137+
center: LatLng,
138+
zoom: number,
139+
pins: MapPin[]
140+
): string {
141+
const baseUrl = "https://maps.googleapis.com/maps/api/staticmap";
142+
const params = new URLSearchParams({
143+
center: `${center.lat},${center.lng}`,
144+
zoom: zoom.toString(),
145+
size: "600x400",
146+
scale: "2",
147+
key: GOOGLE_API_KEY,
148+
});
149+
150+
// Add markers for each pin
151+
pins.forEach((pin, index) => {
152+
const label = pin.name?.charAt(0).toUpperCase() || String(index + 1);
153+
const color = pin.background?.replace("#", "") || "red";
154+
params.append("markers", `color:0x${color}|label:${label}|${pin.lat},${pin.lng}`);
155+
});
156+
157+
return `${baseUrl}?${params.toString()}`;
158+
}
159+
160+
render() {
161+
const resolvedCenter = this.resolveValue<LatLng>(this.center);
162+
const resolvedZoom = this.resolveValue<number>(this.zoom) ?? 11;
163+
const resolvedPins = this.resolveValue<MapPin[]>(this.pins) ?? [];
164+
165+
// Decision logic: Use Google Maps if API key is available, otherwise use placeholder
166+
if (GOOGLE_API_KEY && resolvedCenter) {
167+
const mapUrl = this.buildGoogleMapsUrl(
168+
resolvedCenter,
169+
resolvedZoom,
170+
resolvedPins
171+
);
172+
return html`
173+
<div class="map-container">
174+
<img src=${mapUrl} alt="Map showing ${resolvedPins.length} locations" />
175+
</div>
176+
`;
177+
}
178+
179+
// Fallback: Show local placeholder image with pin information
180+
return html`
181+
<div class="map-container">
182+
<div class="placeholder-container">
183+
<img src=${LOCAL_PLACEHOLDER_IMAGE} alt="Map placeholder" />
184+
${resolvedPins.length > 0
185+
? html`
186+
<div class="pin-list">
187+
📍 ${resolvedPins.length} locations:
188+
${resolvedPins.map((pin) => pin.name).join(", ")}
189+
</div>
190+
`
191+
: nothing}
192+
</div>
193+
</div>
194+
`;
195+
}
196+
}

0 commit comments

Comments
 (0)