Place card
Restaurants, shops, museums — rating, price level, distance, and whether a place is open now, worked out from its hours.
On this page
<aparte-chat-viewport style="height: 35rem"></aparte-chat-viewport>
<script>
// In an app: import { setupPlaceCard } from '@aparte/plugin-cards/place';
const { setupPlaceCard } = window.apartePlugins.cards.place;
// A card shows only http(s) pictures: these are served by this site, which its image
// policy lets load. Without it they wait behind "Show images", as any picture would.
window.aparte.aparteGlobalConfig.setImagePolicy(['self']);
const picture = (name) => new URL('/demo-pictures/' + name + '.svg', location.href).href;
// Opening hours for every day but the ones listed.
const week = (open, close, closed = []) => ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
.filter((day) => !closed.includes(day)).map((day) => ({ day, open, close }));
setupPlaceCard({ tool: 'find_places', currencySymbol: '€' });
const viewport = document.querySelector('aparte-chat-viewport');
viewport.appendMessage({ id: 'u1', role: 'user', content: 'Where can we eat near the old town tonight?', timestamp: Date.now() });
viewport.appendMessage({ id: 'a1', role: 'assistant', timestamp: Date.now(), segments: [{
id: 's1', type: 'tool_call', status: 'resolved',
toolCall: { id: 'c-s1', name: 'find_places', input: { query: 'dinner', near: 'Vieux-Lille' } },
structuredResult: {
title: 'Dinner near the old town',
places: [
{ name: 'Le Comptoir des Halles', category: 'Bistro', image: picture('store-bistro'), rating: 4.6, reviewCount: 1284,
priceLevel: 2, distance: 350, address: '12 rue de la Halle, Lille', coordinates: { lat: 50.6405, lng: 3.0614 },
website: 'https://bistro.example.com', hours: week('12:00', '23:30', ['mon']), tags: ['Terrace', 'Natural wine'] },
{ name: 'Café Lumière', category: 'Café', image: picture('store-cafe'), rating: 4.4, reviewCount: 532, priceLevel: 1,
distance: 600, address: '3 place du Concert, Lille', hours: week('07:30', '19:00'), tags: ['Brunch', 'Wi-Fi'] },
{ name: 'Maison Brunel', category: 'Bakery', image: picture('store-bakery'), rating: 4.8, reviewCount: 2210, priceLevel: 1,
distance: 900, address: '41 rue Royale, Lille', hours: week('07:00', '20:00', ['sun']), tags: ['Sourdough'] },
],
},
}] });
</script>A place as a person decides on it: its photo and kind, its rating and price level, how far it is, and whether it is open — worked out from its hours at the moment the card is drawn, across midnight, on the place’s own clock when the data gives its offset. Open the frame at another time of day and the line changes.
Variants
Section titled “Variants”Compact
Section titled “Compact”<aparte-chat-bubble message-id="a1" data-role="assistant" name="Assistant"></aparte-chat-bubble>
<script>
// In an app: import { setupPlaceCard } from '@aparte/plugin-cards/place';
const { setupPlaceCard } = window.apartePlugins.cards.place;
// A card shows only http(s) pictures: these are served by this site, which its image
// policy lets load. Without it they wait behind "Show images", as any picture would.
window.aparte.aparteGlobalConfig.setImagePolicy(['self']);
const picture = (name) => new URL('/demo-pictures/' + name + '.svg', location.href).href;
// Opening hours for every day but the ones listed.
const week = (open, close, closed = []) => ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
.filter((day) => !closed.includes(day)).map((day) => ({ day, open, close }));
setupPlaceCard({ tool: 'find_places', currencySymbol: '€', variant: 'compact' });
document.querySelector('aparte-chat-bubble').setSegments([{
id: 's1', type: 'tool_call', status: 'resolved',
toolCall: { id: 'c-s1', name: 'find_places', input: { query: 'dinner', near: 'Vieux-Lille' } },
structuredResult: {
title: 'Dinner near the old town',
places: [
{ name: 'Le Comptoir des Halles', category: 'Bistro', image: picture('store-bistro'), rating: 4.6, reviewCount: 1284,
priceLevel: 2, distance: 350, address: '12 rue de la Halle, Lille', coordinates: { lat: 50.6405, lng: 3.0614 },
website: 'https://bistro.example.com', hours: week('12:00', '23:30', ['mon']), tags: ['Terrace', 'Natural wine'] },
{ name: 'Café Lumière', category: 'Café', image: picture('store-cafe'), rating: 4.4, reviewCount: 532, priceLevel: 1,
distance: 600, address: '3 place du Concert, Lille', hours: week('07:30', '19:00'), tags: ['Brunch', 'Wi-Fi'] },
{ name: 'Maison Brunel', category: 'Bakery', image: picture('store-bakery'), rating: 4.8, reviewCount: 2210, priceLevel: 1,
distance: 900, address: '41 rue Royale, Lille', hours: week('07:00', '20:00', ['sun']), tags: ['Sourdough'] },
],
},
}]);
</script>One row per place.
Detail
Section titled “Detail”<aparte-chat-bubble message-id="a1" data-role="assistant" name="Assistant"></aparte-chat-bubble>
<script>
// In an app: import { setupPlaceCard } from '@aparte/plugin-cards/place';
const { setupPlaceCard } = window.apartePlugins.cards.place;
// A card shows only http(s) pictures: these are served by this site, which its image
// policy lets load. Without it they wait behind "Show images", as any picture would.
window.aparte.aparteGlobalConfig.setImagePolicy(['self']);
const picture = (name) => new URL('/demo-pictures/' + name + '.svg', location.href).href;
// Opening hours for every day but the ones listed.
const week = (open, close, closed = []) => ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
.filter((day) => !closed.includes(day)).map((day) => ({ day, open, close }));
setupPlaceCard({ tool: 'get_place', currencySymbol: '€', variant: 'detail' });
document.querySelector('aparte-chat-bubble').setSegments([{
id: 's1', type: 'tool_call', status: 'resolved',
toolCall: { id: 'c-s1', name: 'get_place', input: { name: 'Le Comptoir des Halles' } },
structuredResult: { name: 'Le Comptoir des Halles', category: 'Bistro', image: picture('store-bistro'), rating: 4.6, reviewCount: 1284,
priceLevel: 2, distance: 350, address: '12 rue de la Halle, Lille', coordinates: { lat: 50.6405, lng: 3.0614 },
website: 'https://bistro.example.com', hours: week('12:00', '23:30', ['mon']), tags: ['Terrace', 'Natural wine'] },
}]);
</script>One place with the week’s hours, today first, and Directions and Website buttons when the data allows them (Call too, with a phone number).
While the call runs
Section titled “While the call runs”<aparte-chat-bubble message-id="a1" data-role="assistant" name="Assistant"></aparte-chat-bubble>
<script>
// In an app: import { setupPlaceCard } from '@aparte/plugin-cards/place';
const { setupPlaceCard } = window.apartePlugins.cards.place;
setupPlaceCard({ tool: 'get_place', currencySymbol: '€' });
// The call is still running: the card draws its skeleton, with what the input already says.
document.querySelector('aparte-chat-bubble').setSegments([{
id: 's1', type: 'tool_call', status: 'pending',
toolCall: { id: 'c-s1', name: 'get_place', input: { name: 'Le Comptoir des Halles' } },
}]);
</script>npm install @aparte/plugin-cards @aparte/coreimport { setupPlaceCard } from '@aparte/plugin-cards/place';
setupPlaceCard({ tool: 'find_places', currencySymbol: '€' });A price level is a tier, not an amount: currencySymbol is the sign it is drawn with ($ by
default). The reader takes both generations of Google Places and Yelp; the fields and the hours
helpers are in the guide.
A real map
Section titled “A real map”Without a photo, the card draws a map of its own and fetches nothing. Give it your map tiles and a place with coordinates sits on a real one, under its marker:
setupPlaceCard({
tool: 'find_places',
mapTiles: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
mapAttribution: '© OpenStreetMap contributors',
});- Your provider, your choice. Any server that cuts tiles the usual way: OpenStreetMap’s, with no key but a usage policy that rules out heavy traffic; MapTiler’s or Stadia’s, with your key in the address; your own. Each tile is a request from your visitor’s browser to that server, which sees the visitor and the place — which is why the card fetches none unless you ask. The tiles are your configuration, so the chat’s image policy does not apply to them; a place’s photo does follow it.
- The credit.
mapAttributionis shown in a corner of the map; most providers require one. - How close.
mapZoom(16 by default) is the tile server’s zoom: 16 is a street and its neighbours, 13 a district. - What stays. A photo still wins over the map. A place without coordinates keeps the drawn
map, and so does a card whose tile fails to load. The dark scheme keeps the tiles as they are;
--aparte-place-map-filtertakes a CSS filter to dim them (brightness(0.85)).
Related
Section titled “Related”- Order card — a table or a room, once booked.
- Weather card — the other card a trip asks for.