backpacking-assistant / src /tools /widgets /search_cities_in_radius_widget.html
khlevon's picture
feat: add opeai chatgpt app widgets support
1ddfc0b
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
#root {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
min-height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.hidden {
display: none !important;
}
/* Loader styles */
.loader-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 24px;
}
.loader {
width: 40px;
height: 40px;
border: 3px solid #e5e7eb;
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.loader-text {
color: #6b7280;
font-size: 14px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Cities results container */
.cities-results {
width: 100%;
max-width: 420px;
animation: fadeIn 0.4s ease-out;
}
.results-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #e5e7eb;
}
.results-title {
font-size: 16px;
font-weight: 600;
color: #1f2937;
display: flex;
align-items: center;
gap: 8px;
}
.results-title-icon {
font-size: 20px;
}
.results-count {
font-size: 13px;
color: #6b7280;
background: #f3f4f6;
padding: 4px 12px;
border-radius: 16px;
}
.cities-list {
display: flex;
flex-direction: column;
gap: 10px;
max-height: 400px;
overflow-y: auto;
}
.city-item {
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border: 1px solid #e2e8f0;
border-radius: 12px;
padding: 14px 16px;
display: flex;
align-items: center;
gap: 14px;
transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
}
.city-item:hover {
transform: translateX(4px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border-color: #cbd5e1;
}
.city-item-icon {
width: 40px;
height: 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
flex-shrink: 0;
}
.city-item-content {
flex: 1;
min-width: 0;
}
.city-item-name {
font-size: 15px;
font-weight: 600;
color: #1f2937;
margin-bottom: 2px;
}
.city-item-country {
font-size: 13px;
color: #6b7280;
display: flex;
align-items: center;
gap: 4px;
}
.city-item-coords {
text-align: right;
flex-shrink: 0;
}
.city-item-coord {
font-size: 11px;
color: #9ca3af;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
}
.city-item-uuid {
margin-top: 8px;
padding-top: 8px;
border-top: 1px dashed #e2e8f0;
}
.city-item-uuid-label {
font-size: 10px;
color: #9ca3af;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.city-item-uuid-value {
font-size: 11px;
color: #64748b;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', monospace;
word-break: break-all;
}
/* Error card styles */
.error-card {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
border-radius: 16px;
padding: 24px;
color: white;
width: 100%;
max-width: 400px;
box-shadow: 0 10px 40px rgba(239, 68, 68, 0.3);
animation: fadeIn 0.4s ease-out;
text-align: center;
}
.error-icon {
width: 56px;
height: 56px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
margin: 0 auto 16px;
}
.error-title {
font-size: 18px;
font-weight: 600;
margin-bottom: 8px;
}
.error-message {
font-size: 14px;
opacity: 0.9;
line-height: 1.5;
}
/* Empty state */
.empty-card {
background: #f9fafb;
border: 2px dashed #d1d5db;
border-radius: 16px;
padding: 32px;
text-align: center;
width: 100%;
max-width: 400px;
animation: fadeIn 0.4s ease-out;
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
}
.empty-title {
font-size: 18px;
font-weight: 600;
color: #374151;
margin-bottom: 8px;
}
.empty-message {
font-size: 14px;
color: #6b7280;
}
</style>
<div id="root">
<div class="loader-container" id="loader">
<div class="loader"></div>
<span class="loader-text">Searching nearby cities...</span>
</div>
<div class="cities-results hidden" id="cities-results">
<div class="results-header">
<span class="results-title">
<span class="results-title-icon">📍</span>
Cities Found
</span>
<span class="results-count" id="results-count"></span>
</div>
<div class="cities-list" id="cities-list"></div>
</div>
<div class="empty-card hidden" id="empty-card">
<div class="empty-icon">🗺️</div>
<div class="empty-title">No Cities Found</div>
<div class="empty-message">No Flixbus cities found within the specified radius.</div>
</div>
<div class="error-card hidden" id="error-card">
<div class="error-icon">⚠️</div>
<div class="error-title">Search Failed</div>
<div class="error-message" id="error-message"></div>
</div>
</div>
<script>
function createCityItem(city) {
const item = document.createElement('div');
item.className = 'city-item';
item.innerHTML = `
<div class="city-item-icon">🏙️</div>
<div class="city-item-content">
<div class="city-item-name"></div>
<div class="city-item-country">
<span>📍</span>
<span class="country-text"></span>
</div>
<div class="city-item-uuid">
<div class="city-item-uuid-label">Flixbus UUID</div>
<div class="city-item-uuid-value"></div>
</div>
</div>
<div class="city-item-coords">
<div class="city-item-coord lat-coord"></div>
<div class="city-item-coord lng-coord"></div>
</div>
`;
item.querySelector('.city-item-name').textContent = city.name;
item.querySelector('.country-text').textContent = city.country;
item.querySelector('.city-item-uuid-value').textContent = city.id;
item.querySelector('.lat-coord').textContent = city.latitude.toFixed(4) + '° N';
item.querySelector('.lng-coord').textContent = city.longitude.toFixed(4) + '° E';
return item;
}
function render() {
const payload = window.openai?.toolOutput;
if (!payload) return;
const data = JSON.parse(payload.text);
document.getElementById('loader').classList.add('hidden');
if (data.error) {
document.getElementById('error-message').textContent = data.error;
document.getElementById('error-card').classList.remove('hidden');
return;
}
if (!data.cities || data.cities.length === 0) {
document.getElementById('empty-card').classList.remove('hidden');
return;
}
document.getElementById('results-count').textContent = `${data.cities.length} ${data.cities.length === 1 ? 'city' : 'cities'}`;
document.getElementById('cities-results').classList.remove('hidden');
const listEl = document.getElementById('cities-list');
data.cities.forEach(city => {
listEl.appendChild(createCityItem(city));
});
}
window.addEventListener("openai:set_globals", (event) => {
if (event.detail?.globals?.toolOutput) render();
}, { passive: true });
render();
</script>