Skip to main content

OTA Listings Example

Agent Grid Example

This example shows a simple online-travel-agency (OTA) layout with three static listings (hotel, restaurant, tour) and two Focus Chat configurations: a global "Travel Agent" scoped to the page header (H1) and per-listing assistants scoped to each card header (H2). All widget theming and agent card URLs are centralized under AGENTS (sand palette for the travel agent, blue for the hotel), while the listings themselves are defined once in LISTINGS and rendered dynamically. The "More details" section for each card is hydrated at runtime via a lightweight, CORS-friendly reader (r.jina.ai), which strips image tags/links and keeps only readable text before truncation. Triggers are wired with a single custom event, so adding or removing listings is as simple as updating the LISTINGS array.

<!DOCTYPE html>
<html>
<head>
<style>
:root{
--blue-600:#465EA4; --blue-500:#5E75BA; --blue-400:#7E91C8; --blue-200:#BFC8E4; --blue-060:#ECEEF7; --blue-020:#F9F9FC;
--sand-700:#885E2B; --sand-600:#B37B38; --sand-500:#C89250; --sand-400:#D3A873; --sand-200:#E9D4B9; --sand-060:#F9F3EC; --sand-020:#FEFDFB;
--muted:#6b7280; --gap:18px; --card-radius:12px;
}
*{ box-sizing:border-box; }
.container{ width:100%; max-width:1200px; margin:0 auto; padding:24px; }
.h1-row{ display:flex; align-items:center; justify-content:space-between; gap:16px; }
.h1-region h1, .h1-region h2.section-title{ padding:0; margin:0; }
.h1-region h1{ font-size:28px; color:#0f1724; }
.travel-agent-bar{ display:flex; align-items:center; gap:8px; }
.travel-agent-bar .tips{ color:#475569; font-size:14px; }
.ta-trigger{ text-decoration:none; line-height:0; display:inline-flex; align-items:center; justify-content:center; }

.focus-chat-icon svg{ display:block; }
.focus-chat-icon text{ fill:white; font-weight:700; }
.is-sand-agent .focus-chat-icon circle{ fill:var(--sand-400); transition:fill 140ms ease; }
.is-sand-agent:hover .focus-chat-icon circle{ fill:var(--sand-600); }
.is-blue-agent .focus-chat-icon circle{ fill:var(--blue-400); transition:fill 140ms ease; }
.is-blue-agent:hover .focus-chat-icon circle{ fill:var(--blue-600); }

.cards-grid{ display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:var(--gap); margin-top:24px; }
.card{
position:relative; background:var(--sand-020); border:1px solid #eef2f6; border-radius:var(--card-radius);
overflow:hidden; display:flex; flex-direction:column; min-height:520px; box-shadow:0 6px 16px rgba(12,20,32,0.04);
}
.card img.hero{ width:100%; height:200px; object-fit:cover; display:block; background:#f0f2f6; }
.card-body{ padding:16px; display:flex; flex-direction:column; gap:10px; }
h2.section-title{ display:flex; align-items:center; gap:12px; font-size:18px; color:#0f1724; font-weight:500; }
h2.section-title .focus-chat-trigger{ margin-left:auto; text-decoration:none; line-height:0; display:inline-flex; align-items:center; justify-content:center; }
.meta{ color:#292929; font-size:14px; line-height:1.2; }
.price{ margin-top:2px; font-weight:600; color:var(--sand-700); }
.desc{ margin-top:2px; font-size:14px; color:#6b7280; flex:1; }
details{ margin-top:8px; }
details > summary{ cursor:pointer; list-style:none; color:var(--sand-500); font-weight:600; font-size:14px; }
details > div{ font-size:14px; color:var(--sand-700); white-space:pre-wrap; }
@media (max-width:1000px){ .cards-grid{ grid-template-columns:repeat(2,minmax(0,1fr)); } }
@media (max-width:640px){ .cards-grid{ grid-template-columns:1fr; } }
</style>
</head>
<body>
<div class="container">
<div class="h1-region" aria-labelledby="main-heading">
<div class="h1-row">
<h1 id="main-heading">Explore our Amazing Destinations</h1>
<div class="travel-agent-bar">
<div class="tips">Ask across all listings. Try: <em>I'm hungry</em> or <em>What's nearby?</em></div>
<a id="fc-travel" href="#" class="ta-trigger is-sand-agent" title="Ask about everything" aria-label="Ask about everything">
<div class="focus-chat-icon" role="img" aria-hidden="false">
<svg viewBox="0 0 32 32" width="32" height="32" xmlns="http://www.w3.org/2000/svg" focusable="false">
<circle cx="16" cy="16" r="16"></circle>
<text x="11" y="22">?</text>
</svg>
</div>
</a>
</div>
</div>

<div class="cards-grid" id="cards"></div>
</div>
</div>

<script>

const AGENTS = {
travel: {
cardUrl: "https://75fa98e7-9278-47a0-8ce8-5b8523789930.agents.inhotel.io/.well-known/agent-card.json",
config: {
messageFontSize: "15px",
inputFontSize: "15px",
header_background_color: "var(--sand-500)",
header_icon_hover_background_color: "var(--sand-600)",
body_background_color: "var(--sand-060)",
assistant_message_background_color: "var(--sand-020)",
user_message_background_color: "var(--sand-200)",
user_message_color: "black",
send_button_background_color: "var(--sand-500)",
send_button_hover_background_color: "var(--sand-600)"
}
},
lacroixReservations: {
cardUrl: "https://38573332-48c8-4fbc-b3ff-0c23ce0e84e3.agents.inhotel.io/.well-known/agent-card.json",
config: {
messageFontSize: "15px",
inputFontSize: "15px",
header_background_color: "var(--blue-500)",
header_icon_hover_background_color: "var(--blue-600)",
body_background_color: "var(--blue-060)",
assistant_message_background_color: "var(--blue-020)",
user_message_background_color: "var(--blue-200)",
user_message_color: "black",
send_button_background_color: "var(--blue-500)",
send_button_hover_background_color: "var(--blue-600)"
}
}
};

const LISTINGS = [
{
id: "hotel",
title: "La Croix — Reservations",
hero: "https://restaurant-la-croix-dor.vaudhotels.com/data/Photos/OriginalPhoto/12601/1260143/1260143269/hotel-restaurant-la-croix-dor-ballaigues-photo-6.JPEG",
meta: "Historic stay in Jura with personal service.",
price: "Rooms from CHF 140 / night",
desc: "Book stays at La Croix with real-time guidance from our Reservations assistant.",
aboutUrl: "https://lacroixdor.ch/en/about-us/",
agentKey: "lacroixReservations",
iconClass: "is-blue-agent"
},
{
id: "restaurant",
title: "Nagomi Sushi (Genève)",
hero: "https://restaurant-nagomi.com/wp-content/uploads/2018/03/sushi_01.jpg",
meta: "Counter-focused sushi with seasonal produce.",
price: "Lunch & Dinner service",
desc: "Intimate counter with handcrafted nigiri. Ask for reservations, dietary notes, or nearby transport.",
aboutUrl: "https://restaurant-nagomi.com/",
agentKey: "travel",
iconClass: "is-sand-agent"
},
{
id: "tour",
title: "Soho & Chinatown Food Tour",
hero: "https://cdn.getyourguide.com/image/format=auto,fit=crop,gravity=auto,quality=60,width=620,height=400,dpr=2/tour_img/988c5b37df20dc632d4d75625acba28a8706a373eca543b1d96dff4aae25b3fe.jpg",
meta: "Two-hour tasting walk with local favorites.",
price: "From £32 per person",
desc: "Sample Mediterranean, Indian, and British bites. Confirm start points and dietary options.",
aboutUrl: "https://www.getyourguide.com/london-l57/london-s-best-bites-soho-chinatown-food-walking-tour-t943039/",
agentKey: "travel",
iconClass: "is-sand-agent"
}
];

const cardsEl = document.getElementById("cards");
cardsEl.innerHTML = LISTINGS.map(item => `
<article class="card" id="section-${item.id}">
<img class="hero" referrerpolicy="no-referrer" src="${item.hero}" alt="${item.title} hero"/>
<div class="card-body">
<h2 class="section-title">
${item.title}
<a id="fc-${item.id}" href="#" class="focus-chat-trigger ${item.iconClass}" title="Ask about ${item.title}" aria-label="Ask about ${item.title}">
<div class="focus-chat-icon" role="img" aria-hidden="false">
<svg viewBox="0 0 32 32" width="32" height="32" xmlns="http://www.w3.org/2000/svg" focusable="false">
<circle cx="16" cy="16" r="16"></circle>
<text x="11" y="22">?</text>
</svg>
</div>
</a>
</h2>
<div class="meta" style="line-height:1.1em;">${item.meta}</div>
<div class="price">${item.price}</div>
<p class="desc">${item.desc}</p>
<details>
<summary>More details</summary>
<div id="details-${item.id}">Loading details…</div>
</details>
</div>
</article>
`).join("");

const readerUrl = (url) => `https://r.jina.ai/${url.startsWith('http') ? url : 'https://' + url}`;

function scrubImagesKeepText(raw) {
// Drop lines that are images or links.
const cleaned = raw
.split('\n')
.filter(line => !/^\s*(?:<img\b|!\[.*\]\(.*\)|image:?|https?:\/\/\S+\.(?:png|jpe?g|gif|webp|svg)\b)/i.test(line))
.map(line =>
line
.replace(/!\[[^\]]*\]\([^)]+\)/g, '')
.replace(/<img\b[^>]*>/gi, '')
.replace(/\bhttps?:\/\/\S+\.(?:png|jpe?g|gif|webp|svg)\b/gi, '')
.replace(/\bhttps?:\/\/\S+\b/gi, '')
)
.join('\n')
.replace(/\n{3,}/g, '\n\n')
.trim();
return cleaned;
}

const first2kText = (txt) => scrubImagesKeepText(txt).slice(0, 5000);

async function hydrateDetails() {
await Promise.all(LISTINGS.map(async (item) => {
const target = document.getElementById(`details-${item.id}`);
try {
const res = await fetch(readerUrl(item.aboutUrl));
const text = await res.text();
target.textContent = first2kText(text);
} catch {
target.textContent = "More info is currently unavailable.";
}
}));
}

// ----- 5) Focus Chat widgets (use centralized agent configs) -----
function createFocusChat({ id, agentKey, extraConfig }) {
const agent = AGENTS[agentKey];
const el = document.createElement("focus-chat");
el.id = id;
el.setAttribute("agentCardUrl", agent.cardUrl);
el.setAttribute("config", JSON.stringify({ ...agent.config, ...extraConfig }));
document.body.appendChild(el);
return el;
}

// Global Travel Agent (sand theme), scoped to the H1 region
createFocusChat({
id: "fc-widget-travel",
agentKey: "travel",
extraConfig: {
title: "Travel Agent",
sourceContext: ".h1-region",
sourceTriggerSelector: "#fc-travel"
}
});

// One widget per listing, scoped to its <h2>, theme comes from agentKey
LISTINGS.forEach(item => {
createFocusChat({
id: `fc-widget-${item.id}`,
agentKey: item.agentKey,
extraConfig: {
title: item.title,
sourceContext: "h2",
sourceTriggerSelector: `#fc-${item.id}`
}
});
});

// ----- 6) Trigger wiring via custom event -----
document.querySelectorAll('.focus-chat-trigger, .ta-trigger').forEach(el=>{
el.addEventListener('click', function(e){
e.preventDefault();
const selector = '#' + this.id;
const evt = new CustomEvent('focus-chat-trigger', { detail:{ selector } });
window.dispatchEvent(evt);
});
});

// Start detail hydration
hydrateDetails();
</script>

<!-- Focus Chat script -->
<script src="https://embed.agents.inhotel.io/focus-chat/latest/focus-chat.min.js"></script>
</body>
</html>

Key Features

Multi-Agent Architecture

  • Global travel agent for general queries across all listings
  • Specialized agents for specific listings (e.g., hotel reservations)
  • Centralized agent configuration with consistent theming

Dynamic Content Integration

  • Real-time content scraping using r.jina.ai reader
  • Automatic text cleaning and truncation
  • Fallback handling for unavailable content

Scalable Configuration

  • Single LISTINGS array as source of truth
  • Centralized AGENTS configuration
  • Easy to add/remove listings and agents

Themed UI Components

  • Sand palette for travel agent
  • Blue palette for specialized services
  • Consistent hover states and transitions

Use Cases

Perfect for:

  • Travel booking websites with multiple service types
  • Marketplace platforms with different vendor categories
  • Multi-service businesses with specialized departments
  • Event platforms with various activity types
  • Restaurant aggregators with location-specific agents

Next Steps