查看完整範例原始碼
搜尋地點,即可查看 AI 輔助的摘要。建議搜尋:
- 「飯店」:用於社區摘要。
- 「電動車充電站」:電動車充電站設施摘要。
- 任何餐廳或商家,用於地點和評論摘要。
TypeScript
// 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' ) as HTMLAnchorElement; let innerMap: google.maps.Map; let marker: google.maps.marker.AdvancedMarkerElement; async function init(): Promise<void> { // Request needed libraries. const [{ AdvancedMarkerElement }] = 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. innerMap.addListener('bounds_changed', () => { placeAutocomplete.locationRestriction = innerMap.getBounds()!; }); // Create the marker. marker = new AdvancedMarkerElement({ map: innerMap, }); // Handle selection of an autocomplete result. 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 | null, flagUrl: string | null ) => { 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) --- 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) { 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 = ''; } } void init();
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 init() { // Request needed libraries. const [{ AdvancedMarkerElement }] = 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. innerMap.addListener('bounds_changed', () => { placeAutocomplete.locationRestriction = innerMap.getBounds(); }); // Create the marker. marker = new AdvancedMarkerElement({ map: innerMap, }); // Handle selection of an autocomplete result. 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) --- 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) { 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 = ''; } } void init();
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: 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) 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; }
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>AI 輔助摘要是地點或區域的總覽,提供特定地點、地點周邊區域和地點相關評論的實用深入分析。AI 生成摘要分為三種:
-
地點摘要:特定地點 ID 的簡短 100 字元總覽,將多種不同類型的資料彙整成地點的概略摘要。
-
評論摘要:根據使用者評論生成的地點摘要。
-
區域摘要:系統生成的摘要,說明地點周邊區域,並提供附近搜尋點等額外背景資訊。區域摘要可分為兩種類型:
-
鄰近地區摘要:
premise、street_address類型地點,以及「住宅」和「住宿」類別中所有類型地點的附近搜尋點概略總覽。 -
電動車充電站設施摘要:
electric_vehicle_charging_station類型地點附近搜尋點的概略總覽。
-
擷取 AI 生成的摘要
如要擷取並顯示 AI 生成的摘要,請按照下列步驟操作:
-
const { Place } = await google.maps.importLibrary("places");
取得
Place例項。下列程式碼片段說明如何從地點 ID 建立Place執行個體:const place = new Place("ChIJaYaXFTqq3oARNy537Kb_W_c");
呼叫
place.fetchFields()時,請指定要使用的摘要類型欄位。在下列程式碼片段中,系統會要求所有摘要欄位:await place.fetchFields({ fields: [ 'generativeSummary', 'neighborhoodSummary', 'reviewSummary', 'evChargeAmenitySummary' // Include other fields as needed. ] });
分別存取
generativeSummary、neighborhoodSummary、reviewSummary和evChargeAmenitySummary屬性,擷取摘要資料。下列程式碼片段顯示如何從generativeSummary擷取總覽。const summaryText = place.generativeSummary.overview;
並非所有地點都有 AI 輔助摘要,因此請務必先確認是否有必要資料,再向使用者顯示。下列程式碼片段使用 if 陳述式檢查 generativeSummary:
if (place.generativeSummary) { overviewText = place.generativeSummary.overview; } else { overviewText = 'No summary is available.'; }
或者,您也可以使用 nullish 運算子,簡潔地檢查摘要是否存在:
const overviewText = place.generativeSummary.overview ?? 'No summary is available.';
顯示必要的作者資訊
應用程式中顯示的所有 AI 輔助摘要,都必須依據 Google 的政策和標準,附上適當的出處。詳情請參閱「Maps JavaScript API 政策和出處」。
地點摘要
地點摘要是特定地點 ID 的簡短總覽,長度為 100 個字元,可提供地點的概略資訊。地點摘要可能會醒目顯示某個地點提供的熱門餐點、服務或商品:
-
「Forum Shops 餐廳,在輕鬆的空間中供應大份量的傳統義式料理。」
-
「時尚沙龍,提供剪髮、染髮和吹整服務。」
-
「大型商店,有許多供應商提供各種復古裝飾、家具和服飾。」
地點摘要適用於「支援類型」中顯示的地點類型,涵蓋文化、娛樂和休閒、餐飲、購物、服務和運動等類別。
地點摘要支援下列語言和地區的搜尋點:
| 語言 | 區域 |
|---|---|
| 英文 |
印度 美國 |
要求生成地點摘要
如要要求生成式地點摘要,請在呼叫 fetchFields() 時加入 generativeSummary 欄位:
await place.fetchFields({ fields: [ 'generativeSummary', // Include other fields as needed. ], });
使用 generativeSummary 屬性擷取地點摘要。下列程式碼片段會從 generativeSummary 擷取總覽和揭露事項文字:
if (place.generativeSummary) { console.log("Place Overview:", place.generativeSummary.overview); console.log("Disclosure:", place.generativeSummary.disclosureText); }
評論摘要
評論摘要是根據使用者評論生成的摘要。評論摘要會整合使用者評論的重要元素 (例如地點屬性和評論者情緒),提供高層次的深入分析,協助使用者做出明智決定。
舉例來說,舊金山渡輪大廈的評論摘要包含美食、購物、景觀和氣氛等資訊:
「遊客表示,這個歷史地標提供多樣化的商店、餐廳和農夫市集,許多人對海灣和城市景觀讚譽有加。此外,他們也強調這裡充滿活力的氛圍、方便搭乘渡輪前往其他目的地,以及享受當地商家的機會。
評論摘要功能支援下列語言和地區的搜尋點:
| 語言 | 區域 |
|---|---|
| 英文 | 阿根廷、玻利維亞、巴西、智利、哥倫比亞、哥斯大黎加、多明尼加共和國、厄瓜多、瓜地馬拉、印度、日本、墨西哥、巴拉圭、秘魯、英國、美國、烏拉圭、委內瑞拉 |
| 日文 | 日本 |
| 葡萄牙文 | 巴西 |
| 西班牙文 | 阿根廷、玻利維亞、智利、哥倫比亞、哥斯大黎加、多明尼加共和國、厄瓜多、瓜地馬拉、墨西哥、巴拉圭、秘魯、美國、烏拉圭、委內瑞拉 |
要求生成評論摘要
如要要求評論摘要,請在呼叫 fetchFields() 時加入 reviewSummary 欄位:
await place.fetchFields({ fields: [ 'reviewSummary', // Include other fields as needed. ], });
使用 reviewSummary 屬性擷取評論摘要。如要擷取評論摘要,請存取 reviewSummary.text 屬性。下列程式碼片段會從 reviewSummary 擷取文字。
if (place.reviewSummary) { console.log("Place Review Summary:", place.reviewSummary.text); }
區域摘要
系統會生成地點周邊區域的摘要。區域摘要會提供地點的額外背景資訊,包括附近景點,方便使用者更明智地決定目的地和行程。舉例來說,造訪新城市時,您可以查看飯店的鄰近地區摘要,進一步瞭解周遭環境:
-
「這個充滿活力的舊金山地區融合了北灘和中國城,位於金融區西北方,擁有文學地標、獨特的文化景點和多元化的餐飲。知名景點包括著名的城市之光書店、迷人的纜車博物館,以及熱鬧的中國城街道。
如果您考慮為電動車充電,可以查看電動車充電站的生成摘要,進一步瞭解周遭區域:
-
「這個區域提供多種餐飲選擇,步行 9 分鐘內即可抵達,包括星巴克、Sushi Jin 和 Safeway。」
除了區域說明外,回覆也包含說明中提及地點的 Place 執行個體清單;請對這些 Place 執行個體呼叫 fetchFields(),要求各個地點的詳細資料。
AI 輔助的區域摘要分為兩種類型:
-
周邊摘要:
premise、street_address類型地點,以及「住宅」和「住宿」類別中所有類型地點的周邊搜尋點概略總覽。 -
電動車充電站設施摘要:概略總覽
electric_vehicle_charging_station類型地點附近的搜尋點。
區域摘要功能支援下列語言和地區的搜尋點:
| 語言 | 區域 |
|---|---|
| 英文 | 美國 |
要求生成鄰近地區摘要
您可以要求提供 premise、street_address 類型地點的周邊摘要,以及「住宅」和「住宿」類別中所有類型的地點。如要要求鄰近地區摘要,請在呼叫 fetchFields() 時加入 neighborhoodSummary 欄位:
await place.fetchFields({ fields: [ 'neighborhoodSummary', // Include other fields as needed. ], });
使用 neighborhoodSummary 屬性擷取鄰近區域摘要。如要擷取鄰近區域摘要,請存取 neighborhoodSummary.content 屬性來取得文字。
下列程式碼片段會擷取 neighborhoodSummary 的內容:
if (place.neighborhoodSummary) { console.log("Place Neighborhood Summary:", place.neighborhoodSummary.overview.content); }
要求提供電動車充電站設施摘要
您可以要求類型為 electric_vehicle_charging_station 的地點,提供電動車充電站設施摘要。EVCS 設施摘要提供四種摘要類型:overview、coffee、restaurant 和 store。因此,資料結構是物件陣列,每個物件都包含摘要。如要要求電動車充電站設施摘要,請在呼叫 fetchFields() 時加入 evChargeAmenitySummary 欄位:
await place.fetchFields({ fields: [ 'evChargeAmenitySummary', // Include other fields as needed. ], });
使用 evChargeAmenitySummary 屬性,擷取電動車充電站設施摘要。如要從摘要中擷取文字,請存取 evChargeAmenitySummary.overview、evChargeAmenitySummary.coffee、evChargeAmenitySummary.restaurant 和 evChargeAmenitySummary.store 屬性的 content 屬性。
下列程式碼片段會擷取 evChargeAmenitySummary 的內容:
// overview, coffee, restaurant, store. if (place.evChargeAmenitySummary) { console.log("Place EVCS Amenity Summary:", place.evChargeAmenitySummary.overview.content); console.log("Coffee:", place.evChargeAmenitySummary.coffee.content); console.log("Restaurants:", place.evChargeAmenitySummary.restaurant.content); console.log("Stores:", place.evChargeAmenitySummary.store.content); }