diff --git a/samples/ai-powered-summaries-basic/README.md b/samples/ai-powered-summaries-basic/README.md new file mode 100644 index 00000000..1a4b6847 --- /dev/null +++ b/samples/ai-powered-summaries-basic/README.md @@ -0,0 +1,36 @@ +# Google Maps JavaScript Sample + +## ai-powered-summaries-basic + +The ai-powered-summaries-basic sample demonstrates how to retrieve AI-powered summaries. + +Follow these instructions to set up and run ai-powered-summaries-basic sample on your local computer. + +## Setup + +### Before starting run: + +`npm i` + +### Run an example on a local web server + +First `cd` to the folder for the sample to run, then: + +`npm start` + +### Build an individual example + +From `samples/`: + +`npm run build --workspace=ai-powered-summaries-basic/` + +### Build all of the examples. + +From `samples/`: + +`npm run build-all` + +## Feedback + +For feedback related to this sample, please open a new issue on +[GitHub](https://github.com/googlemaps-samples/js-api-samples/issues). diff --git a/samples/ai-powered-summaries-basic/index.html b/samples/ai-powered-summaries-basic/index.html new file mode 100644 index 00000000..6def208f --- /dev/null +++ b/samples/ai-powered-summaries-basic/index.html @@ -0,0 +1,26 @@ + + + + + + AI-powered Summaries Basic Sample + + + + + + + + + + + + diff --git a/samples/ai-powered-summaries-basic/index.ts b/samples/ai-powered-summaries-basic/index.ts new file mode 100644 index 00000000..58ab067d --- /dev/null +++ b/samples/ai-powered-summaries-basic/index.ts @@ -0,0 +1,110 @@ +/** + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// [START maps_ai_powered_summaries_basic] +const mapElement = document.querySelector('gmp-map') as google.maps.MapElement; +let innerMap; +let infoWindow; + +async function initMap() { + const { Map, InfoWindow } = (await google.maps.importLibrary( + 'maps' + )) as google.maps.MapsLibrary; + + innerMap = mapElement.innerMap; + innerMap.setOptions({ + mapTypeControl: false + }); + + infoWindow = new InfoWindow(); + getPlaceDetails(); +} + +async function getPlaceDetails() { + // Request needed libraries. + const [ {AdvancedMarkerElement}, { Place } ] = await Promise.all([ + google.maps.importLibrary('marker') as Promise, + google.maps.importLibrary('places') as Promise, + ]); + + // [START maps_ai_powered_summaries_basic_placeid] + // Use place ID to create a new Place instance. + const place = new Place({ + id: 'ChIJzzc-aWUM3IARPOQr9sA6vfY', // San Diego Botanic Garden + }); + // [END maps_ai_powered_summaries_basic_placeid] + + // Call fetchFields, passing the needed data fields. + // [START maps_ai_powered_summaries_basic_fetchfields] + await place.fetchFields({ + fields: [ + 'displayName', + 'formattedAddress', + 'location', + 'generativeSummary', + ], + }); + // [END maps_ai_powered_summaries_basic_fetchfields] + + // Add an Advanced Marker + const marker = new AdvancedMarkerElement({ + map: innerMap, + position: place.location, + title: place.displayName, + }); + + // Create a content container. + const content = document.createElement('div'); + // Populate the container with data. + const address = document.createElement('div'); + const summary = document.createElement('div'); + const lineBreak = document.createElement('br'); + const attribution = document.createElement('div'); + + // Retrieve the textual data (summary, disclosure, flag URI). + //@ts-ignore + let overviewText = place.generativeSummary.overview ?? 'No summary is available.'; + //@ts-ignore + let disclosureText = place.generativeSummary.disclosureText; + //@ts-ignore + let reportingUri = place.generativeSummary.flagContentURI; + + // Create HTML for reporting link. + const reportingLink = document.createElement('a'); + reportingLink.href = reportingUri; + reportingLink.target = '_blank'; + reportingLink.textContent = "Report a problem." + + // Add text to layout. + address.textContent = place.formattedAddress ?? ''; + summary.textContent = overviewText; + attribution.textContent = `${disclosureText} `; + attribution.appendChild(reportingLink); + + content.append(address, lineBreak, summary, lineBreak, attribution); + + innerMap.setCenter(place.location); + + // Handle marker click. + marker.addListener('gmp-click', () => { + showInfoWindow(marker, place, content); + }); + + // Display the info window at load time. + showInfoWindow(marker, place, content); +} + +function showInfoWindow(marker, place, content) { + // Display an info window. + infoWindow.setHeaderContent(place.displayName); + infoWindow.setContent(content); + infoWindow.open({ + anchor: marker, + }); +} + +initMap(); +// [END maps_ai_powered_summaries_basic] diff --git a/samples/ai-powered-summaries-basic/package.json b/samples/ai-powered-summaries-basic/package.json new file mode 100644 index 00000000..06aa6790 --- /dev/null +++ b/samples/ai-powered-summaries-basic/package.json @@ -0,0 +1,14 @@ +{ + "name": "@js-api-samples/ai-powered-summaries-basic", + "version": "1.0.0", + "scripts": { + "build": "tsc && bash ../jsfiddle.sh ai-powered-summaries-basic && bash ../app.sh ai-powered-summaries-basic && bash ../docs.sh ai-powered-summaries-basic && npm run build:vite --workspace=. && bash ../dist.sh ai-powered-summaries-basic", + "test": "tsc && npm run build:vite --workspace=.", + "start": "tsc && vite build --base './' && vite", + "build:vite": "vite build --base './'", + "preview": "vite preview" + }, + "dependencies": { + + } +} diff --git a/samples/ai-powered-summaries-basic/style.css b/samples/ai-powered-summaries-basic/style.css new file mode 100644 index 00000000..93167d23 --- /dev/null +++ b/samples/ai-powered-summaries-basic/style.css @@ -0,0 +1,25 @@ +/** + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* [START maps_ai_powered_summaries_basic] */ +/* + * Always set the map height explicitly to define the size of the div element + * that contains the map. + */ +#map { + height: 100%; +} + +/* + * Optional: Makes the sample page fill the window. + */ +html, +body { + height: 100%; + margin: 0; + padding: 0; +} + +/* [END maps_ai_powered_summaries_basic] */ diff --git a/samples/ai-powered-summaries-basic/tsconfig.json b/samples/ai-powered-summaries-basic/tsconfig.json new file mode 100644 index 00000000..366aabb0 --- /dev/null +++ b/samples/ai-powered-summaries-basic/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "esnext", + "strict": true, + "noImplicitAny": false, + "lib": [ + "es2015", + "esnext", + "es6", + "dom", + "dom.iterable" + ], + "moduleResolution": "Node", + "jsx": "preserve" + } +} diff --git a/samples/ai-powered-summaries/README.md b/samples/ai-powered-summaries/README.md new file mode 100644 index 00000000..709d9585 --- /dev/null +++ b/samples/ai-powered-summaries/README.md @@ -0,0 +1,36 @@ +# Google Maps JavaScript Sample + +## ai-powered-summaries + +The ai-powered-summaries sample demonstrates how to show AI-powered summaries a map. + +Follow these instructions to set up and run ai-powered-summaries sample on your local computer. + +## Setup + +### Before starting run: + +`npm i` + +### Run an example on a local web server + +First `cd` to the folder for the sample to run, then: + +`npm start` + +### Build an individual example + +From `samples/`: + +`npm run build --workspace=ai-powered-summaries/` + +### Build all of the examples. + +From `samples/`: + +`npm run build-all` + +## Feedback + +For feedback related to this sample, please open a new issue on +[GitHub](https://github.com/googlemaps-samples/js-api-samples/issues). diff --git a/samples/ai-powered-summaries/index.html b/samples/ai-powered-summaries/index.html new file mode 100644 index 00000000..02da9252 --- /dev/null +++ b/samples/ai-powered-summaries/index.html @@ -0,0 +1,52 @@ + + + + + + AI Place Summaries + + + + + + + + +
+

Search for a place with AI summaries:

+ +
+ + + +
+ + + diff --git a/samples/ai-powered-summaries/index.ts b/samples/ai-powered-summaries/index.ts new file mode 100644 index 00000000..4c44a671 --- /dev/null +++ b/samples/ai-powered-summaries/index.ts @@ -0,0 +1,257 @@ +/* + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// [START maps_ai_powered_summaries] +// Define DOM elements. +const mapElement = document.querySelector('gmp-map') as google.maps.MapElement; +const placeAutocomplete = document.querySelector( + 'gmp-place-autocomplete' +) as google.maps.places.PlaceAutocompleteElement; +const summaryPanel = document.getElementById('summary-panel') as HTMLDivElement; +const placeName = document.getElementById('place-name') as HTMLElement; +const placeAddress = document.getElementById('place-address') as HTMLElement; +const tabContainer = document.getElementById('tab-container') as HTMLDivElement; +const summaryContent = document.getElementById( + 'summary-content' +) as HTMLDivElement; +const aiDisclosure = document.getElementById('ai-disclosure') as HTMLDivElement; +const flagContentLink = document.getElementById('flag-content-link') as HTMLAnchorElement; + +let innerMap; +let marker: google.maps.marker.AdvancedMarkerElement; + +async function initMap(): Promise { + // Request needed libraries. + const [] = await Promise.all([ + google.maps.importLibrary('marker'), + google.maps.importLibrary('places'), + ]); + + innerMap = mapElement.innerMap; + innerMap.setOptions({ + mapTypeControl: false, + streetViewControl: false, + fullscreenControl: false, + }); + + // Bind autocomplete bounds to map bounds. + google.maps.event.addListener(innerMap, 'bounds_changed', async () => { + placeAutocomplete.locationRestriction = innerMap.getBounds(); + }); + + // Create the marker. + marker = new google.maps.marker.AdvancedMarkerElement({ + map: innerMap, + }); + + // Handle selection of an autocomplete result. + // prettier-ignore + // @ts-ignore + placeAutocomplete.addEventListener('gmp-select', async ({ placePrediction }) => { + const place = placePrediction.toPlace(); + + // Fetch all summary fields. + // [START maps_ai_powered_summaries_fetchfields] + await place.fetchFields({ + fields: [ + 'displayName', + 'formattedAddress', + 'location', + 'generativeSummary', + 'neighborhoodSummary', + 'reviewSummary', + 'evChargeAmenitySummary', + ], + }); + // [END maps_ai_powered_summaries_fetchfields] + + // Update the map viewport and position the marker. + if (place.viewport) { + innerMap.fitBounds(place.viewport); + } else { + innerMap.setCenter(place.location); + innerMap.setZoom(17); + } + marker.position = place.location; + + // Update the panel UI. + updateSummaryPanel(place); + } + ); +} + +function updateSummaryPanel(place: google.maps.places.Place) { + // Reset UI + summaryPanel.classList.remove('hidden'); + tabContainer.innerHTML = ''; // innerHTML is OK here since we're clearing known child elements. + summaryContent.textContent = ''; + aiDisclosure.textContent = ''; + + placeName.textContent = place.displayName || ''; + placeAddress.textContent = place.formattedAddress || ''; + + let firstTabActivated = false; + + /** + * Safe Helper: Accepts either a text string or a DOM Node (like a div or DocumentFragment). + */ + const createTab = ( + label: string, + content: string | Node, + disclosure: string, + flagUrl: string + ) => { + const btn = document.createElement('button'); + btn.className = 'tab-button'; + btn.textContent = label; + + btn.onclick = () => { + // Do nothing if the tab is already active. + if (btn.classList.contains('active')) { + return; + } + + // Manage the active class state. + document + .querySelectorAll('.tab-button') + .forEach((b) => b.classList.remove('active')); + btn.classList.add('active'); + + if (typeof content === 'string') { + summaryContent.textContent = content; + } else { + summaryContent.replaceChildren(content.cloneNode(true)); + } + + // Set the disclosure text. + aiDisclosure.textContent = disclosure || 'AI-generated content.'; + + // Add the content flag URI. + if (flagUrl) { + flagContentLink.href = flagUrl; + flagContentLink.textContent = "Report an issue" + } + }; + + tabContainer.appendChild(btn); + + // Auto-select the first available summary. + if (!firstTabActivated) { + btn.click(); + firstTabActivated = true; + } + }; + + // --- 1. Generative Summary (Place) --- + //@ts-ignore + if (place.generativeSummary?.overview) { + createTab( + 'Overview', + //@ts-ignore + place.generativeSummary.overview, + //@ts-ignore + place.generativeSummary.disclosureText, + //@ts-ignore + place.generativeSummary.flagContentURI + ); + } + + // --- 2. Review Summary --- + //@ts-ignore + if (place.reviewSummary?.text) { + createTab( + 'Reviews', + //@ts-ignore + place.reviewSummary.text, + //@ts-ignore + place.reviewSummary.disclosureText, + //@ts-ignore + place.reviewSummary.flagContentURI + ); + } + + // --- 3. Neighborhood Summary --- + //@ts-ignore + if (place.neighborhoodSummary?.overview?.content) { + createTab( + 'Neighborhood', + //@ts-ignore + place.neighborhoodSummary.overview.content, + //@ts-ignore + place.neighborhoodSummary.disclosureText, + //@ts-ignore + place.neighborhoodSummary.flagContentURI + ); + } + + // --- 4. EV Amenity Summary (uses content blocks)) --- + //@ts-ignore + if (place.evChargeAmenitySummary) { + //@ts-ignore + const evSummary = place.evChargeAmenitySummary; + const evContainer = document.createDocumentFragment(); + + // Helper to build a safe DOM section for EV categories. + const createSection = (title: string, text: string) => { + const wrapper = document.createElement('div'); + wrapper.style.marginBottom = '15px'; // Or use a CSS class + + const titleEl = document.createElement('strong'); + titleEl.textContent = title; + + const textEl = document.createElement('div'); + textEl.textContent = text; + + wrapper.appendChild(titleEl); + wrapper.appendChild(textEl); + return wrapper; + }; + + // Check and append each potential section + if (evSummary.overview?.content) { + evContainer.appendChild( + createSection('Overview', evSummary.overview.content) + ); + } + if (evSummary.coffee?.content) { + evContainer.appendChild( + createSection('Coffee', evSummary.coffee.content) + ); + } + if (evSummary.restaurant?.content) { + evContainer.appendChild( + createSection('Food', evSummary.restaurant.content) + ); + } + if (evSummary.store?.content) { + evContainer.appendChild( + createSection('Shopping', evSummary.store.content) + ); + } + + // Only add the tab if the container has children + if (evContainer.hasChildNodes()) { + createTab( + 'EV Amenities', + evContainer, // Passing a Node instead of string + evSummary.disclosureText, + evSummary.flagContentURI + ); + } + } + + // Safely handle the empty state. + if (!firstTabActivated) { + const msg = document.createElement('em'); + msg.textContent = + 'No AI summaries are available for this specific location.'; + summaryContent.replaceChildren(msg); + aiDisclosure.textContent = ''; + } +} + +initMap(); +// [END maps_ai_powered_summaries] diff --git a/samples/ai-powered-summaries/package.json b/samples/ai-powered-summaries/package.json new file mode 100644 index 00000000..e9df893e --- /dev/null +++ b/samples/ai-powered-summaries/package.json @@ -0,0 +1,14 @@ +{ + "name": "@js-api-samples/ai-powered-summaries", + "version": "1.0.0", + "scripts": { + "build": "tsc && bash ../jsfiddle.sh ai-powered-summaries && bash ../app.sh ai-powered-summaries && bash ../docs.sh ai-powered-summaries && npm run build:vite --workspace=. && bash ../dist.sh ai-powered-summaries", + "test": "tsc && npm run build:vite --workspace=.", + "start": "tsc && vite build --base './' && vite", + "build:vite": "vite build --base './'", + "preview": "vite preview" + }, + "dependencies": { + + } +} diff --git a/samples/ai-powered-summaries/style.css b/samples/ai-powered-summaries/style.css new file mode 100644 index 00000000..31092e63 --- /dev/null +++ b/samples/ai-powered-summaries/style.css @@ -0,0 +1,247 @@ +/** + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* [START maps_ai_powered_summaries] */ +/* Reuse existing map height */ +gmp-map { + height: 100%; +} + +html, +body { + height: 100%; + margin: 0; + padding: 0; +} + +/* Existing Autocomplete Card Style */ +.place-autocomplete-card { + background-color: #fff; + border-radius: 5px; + box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; + margin: 10px; + padding: 15px; + font-family: Roboto, sans-serif; + font-size: 1rem; +} + +gmp-place-autocomplete { + width: 300px; +} + +/* New: Summary Panel Styles */ +.summary-card { + background-color: #fff; + border-radius: 5px; + box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; + margin: 10px; + padding: 0; /* Padding handled by children */ + font-family: Roboto, sans-serif; + width: 350px; + max-height: 80vh; /* Prevent overflow on small screens */ + overflow-y: auto; + display: flex; + flex-direction: column; +} + +.hidden { + display: none; +} + +#place-header { + padding: 15px; + background-color: #f8f9fa; + border-bottom: 1px solid #ddd; +} + +#place-header h2 { + margin: 0 0 5px 0; + font-size: 1.2rem; +} + +#place-address { + margin: 0; + color: #555; + font-size: 0.9rem; +} + +/* Tab Navigation */ +.tab-container { + display: flex; + border-bottom: 1px solid #ddd; + background-color: #fff; +} + +.tab-button { + flex: 1; + background: none; + border: none; + padding: 10px; + cursor: pointer; + font-weight: 500; + color: #555; + border-bottom: 3px solid transparent; +} + +.tab-button:hover { + background-color: #f1f1f1; +} + +.tab-button.active { + font-weight: bold; + border-bottom: 3px solid #000000; +} + +.tab-button.active:hover { + background-color: #ffffff; + cursor: default; +} + +/* Content Area */ +.content-area { + padding: 15px; + line-height: 1.5; + font-size: 0.95rem; + color: #333; +} + +.disclosure-footer { + font-size: 0.75rem; + color: #666; + padding: 10px 15px; + border-top: 1px solid #eee; + font-style: italic; +} + +.flag-content-link { + font-size: 0.75rem; + color: #666; + padding: 10px 15px; + border-top: 1px solid #eee; +} +/* [END maps_ai_powered_summaries] */ +/** + * @license + * Copyright 2025 Google LLC. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +/* [START maps_ai_powered_summaries] */ +/* Reuse existing map height */ +gmp-map { + height: 100%; +} + +html, +body { + height: 100%; + margin: 0; + padding: 0; +} + +/* Existing Autocomplete Card Style */ +.place-autocomplete-card { + background-color: #fff; + border-radius: 5px; + box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; + margin: 10px; + padding: 15px; + font-family: Roboto, sans-serif; + font-size: 1rem; +} + +gmp-place-autocomplete { + width: 300px; +} + +/* New: Summary Panel Styles */ +.summary-card { + background-color: #fff; + border-radius: 5px; + box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px; + margin: 10px; + padding: 0; /* Padding handled by children */ + font-family: Roboto, sans-serif; + width: 350px; + max-height: 80vh; /* Prevent overflow on small screens */ + overflow-y: auto; + display: flex; + flex-direction: column; +} + +.hidden { + display: none; +} + +#place-header { + padding: 15px; + background-color: #f8f9fa; + border-bottom: 1px solid #ddd; +} + +#place-header h2 { + margin: 0 0 5px 0; + font-size: 1.2rem; +} + +#place-address { + margin: 0; + color: #555; + font-size: 0.9rem; +} + +/* Tab Navigation */ +.tab-container { + display: flex; + border-bottom: 1px solid #ddd; + background-color: #fff; +} + +.tab-button { + flex: 1; + background: none; + border: none; + padding: 10px; + cursor: pointer; + font-weight: 500; + color: #555; + border-bottom: 3px solid transparent; +} + +.tab-button:hover { + background-color: #f1f1f1; +} + +.tab-button.active { + font-weight: bold; + border-bottom: 3px solid #000000; +} + +.tab-button.active:hover { + background-color: #ffffff; + cursor: default; +} + +/* Content Area */ +.content-area { + padding: 15px; + line-height: 1.5; + font-size: 0.95rem; + color: #333; +} + +.disclosure-footer { + font-size: 0.75rem; + color: #666; + padding: 10px 15px; + border-top: 1px solid #eee; + font-style: italic; +} + +.flag-content-link { + font-size: 0.75rem; + color: #666; + padding: 10px 15px; +} +/* [END maps_ai_powered_summaries] */ diff --git a/samples/ai-powered-summaries/tsconfig.json b/samples/ai-powered-summaries/tsconfig.json new file mode 100644 index 00000000..366aabb0 --- /dev/null +++ b/samples/ai-powered-summaries/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "esnext", + "target": "esnext", + "strict": true, + "noImplicitAny": false, + "lib": [ + "es2015", + "esnext", + "es6", + "dom", + "dom.iterable" + ], + "moduleResolution": "Node", + "jsx": "preserve" + } +}