This sample lets you search for AI-powered summaries. Some suggested searches:
- "Hotel" for neighborhood summaries.
- "EV charging station" for EVCS amenity summaries.
- Any restaurant or business for place and review summaries.
Read the documentation.
TypeScript
// 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<void> { // 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. await place.fetchFields({ fields: [ 'displayName', 'formattedAddress', 'location', 'generativeSummary', 'neighborhoodSummary', 'reviewSummary', 'evChargeAmenitySummary', ], }); // 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();
JavaScript
// Define DOM elements. const mapElement = document.querySelector('gmp-map'); const placeAutocomplete = document.querySelector('gmp-place-autocomplete'); const summaryPanel = document.getElementById('summary-panel'); const placeName = document.getElementById('place-name'); const placeAddress = document.getElementById('place-address'); const tabContainer = document.getElementById('tab-container'); const summaryContent = document.getElementById('summary-content'); const aiDisclosure = document.getElementById('ai-disclosure'); const flagContentLink = document.getElementById('flag-content-link'); let innerMap; let marker; async function initMap() { // 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. await place.fetchFields({ fields: [ 'displayName', 'formattedAddress', 'location', 'generativeSummary', 'neighborhoodSummary', 'reviewSummary', 'evChargeAmenitySummary', ], }); // 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) { // 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, content, disclosure, flagUrl) => { 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, text) => { 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();
CSS
/* 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; } /* 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; }
HTML
<html>
<head>
<title>AI Place Summaries</title>
<link rel="stylesheet" type="text/css" href="./style.css" />
<script type="module" src="./index.js"></script>
<!-- prettier-ignore -->
<script>(g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})
({key: "AIzaSyA6myHzS10YXdcazAFalmXvDkrYCp5cLc8", v: "weekly"});</script>
</head>
<body>
<gmp-map center="37.805, -122.425" zoom="14" map-id="DEMO_MAP_ID">
<!-- Search Input Card -->
<div
class="place-autocomplete-card"
slot="control-inline-start-block-start">
<p>Search for a place with AI summaries:</p>
<gmp-place-autocomplete></gmp-place-autocomplete>
</div>
<!-- Summary text panel (initially hidden) -->
<div
id="summary-panel"
class="summary-card hidden"
slot="control-inline-end-block-start">
<div id="place-header">
<h2 id="place-name"></h2>
<p id="place-address"></p>
</div>
<!-- Tabs for toggling summary types -->
<div class="tab-container" id="tab-container"></div>
<!-- Content display area -->
<div id="summary-content" class="content-area"></div>
<!-- Legal/AI Disclosure -->
<div id="ai-disclosure" class="disclosure-footer"></div>
<!-- Flag content link -->
<a id="flag-content-link" class="flag-content-link"></a>
</div>
</gmp-map>
</body>
</html>Try Sample
Clone Sample
Git and Node.js are required to run this sample locally. Follow these instructions to install Node.js and NPM. The following commands clone, install dependencies and start the sample application.
git clone https://github.com/googlemaps-samples/js-api-samples.gitcd samples/ai-powered-summariesnpm inpm start