| <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-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 { |
| 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 { |
| 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-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> |
|
|