Get a Maps Demo Key: Try out select Maps JavaScript API and Places UI Kit features at no cost with a Maps Demo Key—no billing information required.
AI-powered summaries
Stay organized with collections
Save and categorize content based on your preferences.
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. constmapElement=document.querySelector('gmp-map')!; constplaceAutocomplete=document.querySelector('gmp-place-autocomplete')!; constsummaryPanel=document.getElementById('summary-panel')!; constplaceName=document.getElementById('place-name')!; constplaceAddress=document.getElementById('place-address')!; consttabContainer=document.getElementById('tab-container')!; constsummaryContent=document.getElementById('summary-content')!; constaiDisclosure=document.getElementById('ai-disclosure')!; constflagContentLink=document.getElementById( 'flag-content-link' )asHTMLAnchorElement; letinnerMap:google.maps.Map; letmarker:google.maps.marker.AdvancedMarkerElement; asyncfunctioninit():Promise<void>{ // Request needed libraries. const[{AdvancedMarkerElement}]=awaitPromise.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. innerMap.addListener('bounds_changed',()=>{ placeAutocomplete.locationRestriction=innerMap.getBounds()!; }); // Create the marker. marker=newAdvancedMarkerElement({ map:innerMap, }); // Handle selection of an autocomplete result. placeAutocomplete.addEventListener( 'gmp-select', async({placePrediction})=>{ constplace=placePrediction.toPlace(); // Fetch all summary fields. awaitplace.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); } ); } functionupdateSummaryPanel(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||''; letfirstTabActivated=false; /** * Safe Helper: Accepts either a text string or a DOM Node (like a div or DocumentFragment). */ constcreateTab=( label:string, content:string|Node, disclosure:string|null, flagUrl:string|null )=>{ constbtn=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(typeofcontent==='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) --- if(place.generativeSummary?.overview){ createTab( 'Overview', place.generativeSummary.overview, place.generativeSummary.disclosureText, place.generativeSummary.flagContentURI ); } // --- 2. Review Summary --- if(place.reviewSummary?.text){ createTab( 'Reviews', place.reviewSummary.text, place.reviewSummary.disclosureText, place.reviewSummary.flagContentURI ); } // --- 3. Neighborhood Summary --- if(place.neighborhoodSummary?.overview?.content){ createTab( 'Neighborhood', place.neighborhoodSummary.overview.content, place.neighborhoodSummary.disclosureText, place.neighborhoodSummary.flagContentURI ); } // --- 4. EV Amenity Summary (uses content blocks)) --- if(place.evChargeAmenitySummary){ constevSummary=place.evChargeAmenitySummary; constevContainer=document.createDocumentFragment(); // Helper to build a safe DOM section for EV categories. constcreateSection=(title:string,text:string)=>{ constwrapper=document.createElement('div'); wrapper.style.marginBottom='15px';// Or use a CSS class consttitleEl=document.createElement('strong'); titleEl.textContent=title; consttextEl=document.createElement('div'); textEl.textContent=text; wrapper.appendChild(titleEl); wrapper.appendChild(textEl); returnwrapper; }; // 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){ constmsg=document.createElement('em'); msg.textContent= 'No AI summaries are available for this specific location.'; summaryContent.replaceChildren(msg); aiDisclosure.textContent=''; } } voidinit();
JavaScript
// Define DOM elements. constmapElement=document.querySelector('gmp-map'); constplaceAutocomplete=document.querySelector('gmp-place-autocomplete'); constsummaryPanel=document.getElementById('summary-panel'); constplaceName=document.getElementById('place-name'); constplaceAddress=document.getElementById('place-address'); consttabContainer=document.getElementById('tab-container'); constsummaryContent=document.getElementById('summary-content'); constaiDisclosure=document.getElementById('ai-disclosure'); constflagContentLink=document.getElementById('flag-content-link'); letinnerMap; letmarker; asyncfunctioninit(){ // Request needed libraries. const[{AdvancedMarkerElement}]=awaitPromise.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. innerMap.addListener('bounds_changed',()=>{ placeAutocomplete.locationRestriction=innerMap.getBounds(); }); // Create the marker. marker=newAdvancedMarkerElement({ map:innerMap, }); // Handle selection of an autocomplete result. placeAutocomplete.addEventListener( 'gmp-select', async({placePrediction})=>{ constplace=placePrediction.toPlace(); // Fetch all summary fields. awaitplace.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); } ); } functionupdateSummaryPanel(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||''; letfirstTabActivated=false; /** * Safe Helper: Accepts either a text string or a DOM Node (like a div or DocumentFragment). */ constcreateTab=(label,content,disclosure,flagUrl)=>{ constbtn=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(typeofcontent==='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) --- if(place.generativeSummary?.overview){ createTab( 'Overview', place.generativeSummary.overview, place.generativeSummary.disclosureText, place.generativeSummary.flagContentURI ); } // --- 2. Review Summary --- if(place.reviewSummary?.text){ createTab( 'Reviews', place.reviewSummary.text, place.reviewSummary.disclosureText, place.reviewSummary.flagContentURI ); } // --- 3. Neighborhood Summary --- if(place.neighborhoodSummary?.overview?.content){ createTab( 'Neighborhood', place.neighborhoodSummary.overview.content, place.neighborhoodSummary.disclosureText, place.neighborhoodSummary.flagContentURI ); } // --- 4. EV Amenity Summary (uses content blocks)) --- if(place.evChargeAmenitySummary){ constevSummary=place.evChargeAmenitySummary; constevContainer=document.createDocumentFragment(); // Helper to build a safe DOM section for EV categories. constcreateSection=(title,text)=>{ constwrapper=document.createElement('div'); wrapper.style.marginBottom='15px';// Or use a CSS class consttitleEl=document.createElement('strong'); titleEl.textContent=title; consttextEl=document.createElement('div'); textEl.textContent=text; wrapper.appendChild(titleEl); wrapper.appendChild(textEl); returnwrapper; }; // 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){ constmsg=document.createElement('em'); msg.textContent= 'No AI summaries are available for this specific location.'; summaryContent.replaceChildren(msg); aiDisclosure.textContent=''; } } voidinit();
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)0px5px15px; margin:10px; padding:2px; font-family:Roboto,sans-serif; font-size:1rem; } gmp-place-autocomplete{ width:330px; } /* New: Summary Panel Styles */ .summary-card{ background-color:#fff; border-radius:5px; box-shadow:rgba(0,0,0,0.35)0px5px15px; 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:1pxsolid#ddd; } #place-headerh2{ margin:005px0; font-size:1.2rem; } #place-address{ margin:0; color:#555; font-size:0.9rem; } /* Tab Navigation */ .tab-container{ display:flex; border-bottom:1pxsolid#ddd; background-color:#fff; } .tab-button{ flex:1; background:none; border:none; padding:10px; cursor:pointer; font-weight:500; color:#555; border-bottom:3pxsolidtransparent; } .tab-button:hover{ background-color:#f1f1f1; } .tab-button.active{ font-weight:bold; border-bottom:3pxsolid#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:10px15px; border-top:1pxsolid#eee; font-style:italic; } .flag-content-link{ font-size:0.75rem; color:#666; padding:10px15px; border-top:1pxsolid#eee; }
HTML
<html>
<head>
<title>AI Place Summaries</title>
<link rel="stylesheet" type="text/css" href="./style.css" />
<script type="module" src="./index.js"></script>
<script>
// prettier-ignore
(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: "GOOGLE_MAPS_API_KEY"
});
</script>
</head>
<body>
<gmp-map center="37.80, -122.425" zoom="14" map-id="DEMO_MAP_ID">
<!-- Search Input Card -->
<div
class="place-autocomplete-card"
slot="control-inline-start-block-start">
<gmp-place-autocomplete
placeholder="Find places with AI summaries"></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>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.
gitclonehttps://github.com/googlemaps-samples/js-api-samples.gitcdsamples/ai-powered-summariesnpminpmstart