Files
tesla-roadtrip/ui-preview/index.html
T
tony d516e93323 chore: initial checkpoint - Tesla Roadtrip planner
- Proactive Grok integration (xAI API + local CLI fallback)
- Real road routing via OSRM (no more bird's-eye lines)
- Heavy structured logging for fast iteration
- Strong sanitization + geocoding + ErrorBoundary (no black screens)
- Playwright E2E tests (API diagnostic + full UI flow)
- scripts/dev.sh for one-command startup
- Clean .env.example + documentation

This is a stable checkpoint before further prompt/UI refinement.
2026-05-15 19:24:35 +01:00

441 lines
21 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tesla Roadtrip • Grok Drive — UI Preview</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&amp;family=Space+Grotesk:wght@500;600&amp;display=swap');
:root {
--tesla-red: #E82127;
}
body {
font-family: 'Inter', system_ui, sans-serif;
}
.font-display {
font-family: 'Space Grotesk', 'Inter', system_ui, sans-serif;
font-weight: 600;
}
.tesla-red { color: #E82127; }
.bg-tesla-red { background-color: #E82127; }
.chat-bubble-user {
background: #E82127;
color: white;
border-bottom-right-radius: 4px;
}
.chat-bubble-assistant {
background: #1f242e;
border: 1px solid rgba(255,255,255,0.08);
border-bottom-left-radius: 4px;
}
.map-container {
background: #0a0f1a;
position: relative;
overflow: hidden;
}
.map-grid {
background-image:
linear-gradient(rgba(255,255,255,0.035) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.035) 1px, transparent 1px);
background-size: 42px 42px;
}
.tesla-marker {
width: 26px;
height: 26px;
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
border: 2px solid white;
z-index: 20;
}
.stop-row {
transition: all 0.1s ease;
}
.stop-row:hover {
background-color: #22283a;
}
.section-title {
font-size: 10px;
letter-spacing: 1.5px;
font-weight: 600;
}
.nav-text {
font-feature-settings: "tnum";
}
</style>
</head>
<body class="bg-[#0a0a0a] text-white">
<div class="flex h-screen overflow-hidden">
<!-- LEFT: CHAT -->
<div class="w-[380px] flex flex-col border-r border-white/10 bg-[#111111]">
<!-- Header -->
<div class="px-5 py-4 border-b border-white/10 flex items-center justify-between bg-black/60">
<div class="flex items-center gap-3">
<div class="w-9 h-9 rounded-full bg-[#E82127] flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.25" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</div>
<div>
<div class="font-semibold text-[17px] tracking-[-0.4px]">Grok Drive</div>
<div class="text-[10px] text-white/50 -mt-0.5">UK &amp; Europe • Headless Grok</div>
</div>
</div>
<div class="text-[10px] px-2.5 py-0.5 rounded bg-white/5 font-mono tracking-widest text-white/60">v0.1</div>
</div>
<!-- Vehicle Selector -->
<div class="px-4 py-3 border-b border-white/10 bg-black/30">
<div class="text-[10px] uppercase tracking-[1.5px] text-white/40 mb-1.5 px-1">YOUR VEHICLE</div>
<select class="w-full bg-[#1a1a1a] border border-white/10 rounded-xl px-4 py-[9px] text-sm focus:outline-none focus:border-[#E82127]">
<option>Model Y Long Range — 514 km</option>
<option>Model 3 Highland LR — 549 km</option>
<option>Model Y RWD (EU) — 455 km</option>
<option>Model S Long Range — 634 km</option>
</select>
</div>
<!-- Messages -->
<div class="flex-1 overflow-y-auto p-4 space-y-6 text-sm" id="chat-messages">
<!-- Assistant message -->
<div class="flex">
<div class="max-w-[85%] chat-bubble-assistant rounded-2xl px-4 py-3 leading-snug">
Hello! I'm Grok Drive, your Tesla trip planner for the UK and Europe. Where would you like to go? I know the Supercharger network across Britain, France, Germany, and beyond extremely well.
</div>
</div>
<!-- User message -->
<div class="flex justify-end">
<div class="max-w-[82%] chat-bubble-user rounded-2xl px-4 py-3 leading-snug">
Plan a 2-day trip from London to Edinburgh in my Model Y
</div>
</div>
<!-- Assistant reply -->
<div class="flex">
<div class="max-w-[85%] chat-bubble-assistant rounded-2xl px-4 py-3 leading-snug">
Excellent choice — London to Edinburgh is one of the most popular Tesla routes in the UK. I've routed you via the excellent M1/A1 corridor with fast V3/V4 stalls. Battery stays healthy throughout.
</div>
</div>
</div>
<!-- Quick Prompts -->
<div class="px-3 pb-2 flex flex-wrap gap-1.5 border-t border-white/10 pt-3 bg-black/40">
<div onclick="useQuickPrompt(this)" class="cursor-pointer text-[10px] px-3 py-1 bg-white/5 hover:bg-white/10 rounded-full border border-white/10 transition">Plan a 2-day trip from London to Edinburgh</div>
<div onclick="useQuickPrompt(this)" class="cursor-pointer text-[10px] px-3 py-1 bg-white/5 hover:bg-white/10 rounded-full border border-white/10 transition">Add a lunch stop in the Lake District</div>
<div onclick="useQuickPrompt(this)" class="cursor-pointer text-[10px] px-3 py-1 bg-white/5 hover:bg-white/10 rounded-full border border-white/10 transition">Find a hotel with destination charging</div>
</div>
<!-- Input -->
<div class="p-3 border-t border-white/10 bg-black/40">
<div class="flex items-center gap-2 bg-[#1a1a1a] border border-white/10 rounded-2xl px-3 focus-within:border-[#E82127]">
<input id="chat-input"
type="text"
placeholder="Where would you like to drive in the UK or Europe?"
class="flex-1 bg-transparent py-3 text-sm placeholder:text-white/40 outline-none"
onkeypress="if(event.key === 'Enter') sendMessage()">
<button onclick="sendMessage()"
class="p-2 rounded-xl bg-[#E82127] hover:bg-[#c01a20] active:scale-95 transition flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
</svg>
</button>
</div>
<div class="text-center text-[9px] text-white/30 mt-2 tracking-[1.5px]">POWERED BY HEADLESS GROK CLI</div>
</div>
</div>
<!-- RIGHT: MAP + ITINERARY -->
<div class="flex-1 flex flex-col min-w-0">
<!-- Stats Bar -->
<div class="h-14 border-b border-white/10 bg-black/40 px-6 flex items-center justify-between text-sm">
<div class="flex items-center gap-8 nav-text">
<div class="flex items-center gap-1.5">
<span class="text-[#E82127]"><svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314-11.314z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" /></svg></span>
<span class="font-semibold">665</span>
<span class="text-white/50">km</span>
</div>
<div class="flex items-center gap-1.5">
<span class="text-[#E82127]"><svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 01-9 9 9 9 0 01-9-9 9 9 0 019-9 9 9 0 019 9z" /></svg></span>
<span class="font-semibold">7.2</span>
<span class="text-white/50">hrs drive</span>
</div>
<div class="flex items-center gap-1.5">
<span class="text-[#E82127]"><svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg></span>
<span class="font-semibold">1.1</span>
<span class="text-white/50">hrs charging</span>
</div>
<div class="text-white/50">• 3 Superchargers • 1 hotel</div>
</div>
<div class="flex gap-2">
<button onclick="alert('GPX would be downloaded in the real app')"
class="flex items-center gap-1.5 px-3 py-1.5 text-xs border border-white/20 rounded-lg hover:bg-white/5 transition">
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 13v6m0 0l-3-3m3 3l3-3m-6-4v1m0 0l-3-3m3 3l3-3" /></svg>
GPX
</button>
<button onclick="alert('Share link copied in the real app')"
class="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-white/5 hover:bg-white/10 rounded-lg transition">
<svg xmlns="http://www.w3.org/2000/svg" class="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 01-2.25 2.25" /></svg>
Share
</button>
</div>
</div>
<!-- MAP -->
<div class="flex-1 p-3 bg-[#05070d]">
<div class="w-full h-full rounded-2xl overflow-hidden border border-white/10 relative" id="map-wrapper">
<!-- Real Leaflet will be initialized here -->
<div id="map" style="width: 100%; height: 100%; background: #0a0f1a;"></div>
</div>
</div>
<!-- Itinerary -->
<div class="h-[215px] border-t border-white/10 bg-[#111111] p-4 overflow-x-auto">
<div class="flex gap-6 min-w-max">
<!-- Day 1 -->
<div class="w-[310px]">
<div class="uppercase text-xs tracking-[2px] text-[#E82127] mb-2">DAY 1 • London → Edinburgh</div>
<div class="space-y-1.5">
<!-- Stop -->
<div class="flex justify-between items-center bg-[#1a1f2b] border border-white/5 rounded-2xl px-4 py-2.5 text-sm group">
<div class="flex items-center gap-3">
<div class="w-2.5 h-2.5 rounded-full bg-[#E82127] flex-shrink-0"></div>
<div>
<div class="font-medium">London Battersea Supercharger</div>
<div class="text-xs text-white/40">Arrive 28% • 22 min charge</div>
</div>
</div>
<button onclick="removeStop(this)" class="opacity-0 group-hover:opacity-100 text-white/30 hover:text-red-400"><svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.595 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.595-1.858L5 7m5 4v6m4-6v6m1-10V9a1 1 0 00-1-1h-4a1 1 0 00-1 1v1M9 7h6" /></svg></button>
</div>
<div class="flex justify-between items-center bg-[#1a1f2b] border border-white/5 rounded-2xl px-4 py-2.5 text-sm group">
<div class="flex items-center gap-3">
<div class="w-2.5 h-2.5 rounded-full bg-[#E82127] flex-shrink-0"></div>
<div>
<div class="font-medium">Birmingham Supercharger</div>
<div class="text-xs text-white/40">Arrive 19% • 28 min charge</div>
</div>
</div>
<button onclick="removeStop(this)" class="opacity-0 group-hover:opacity-100 text-white/30 hover:text-red-400"><svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.595 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.595-1.858L5 7m5 4v6m4-6v6m1-10V9a1 1 0 00-1-1h-4a1 1 0 00-1 1v1M9 7h6" /></svg></button>
</div>
<div class="flex justify-between items-center bg-[#1a1f2b] border border-white/5 rounded-2xl px-4 py-2.5 text-sm group">
<div class="flex items-center gap-3">
<div class="w-2.5 h-2.5 rounded-full bg-blue-500 flex-shrink-0"></div>
<div>
<div class="font-medium">The Balmoral Hotel, Edinburgh</div>
<div class="text-xs text-white/40">Destination charging • 4 nights</div>
</div>
</div>
<button onclick="removeStop(this)" class="opacity-0 group-hover:opacity-100 text-white/30 hover:text-red-400"><svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.595 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.595-1.858L5 7m5 4v6m4-6v6m1-10V9a1 1 0 00-1-1h-4a1 1 0 00-1 1v1M9 7h6" /></svg></button>
</div>
</div>
</div>
<!-- Day 2 -->
<div class="w-[310px]">
<div class="uppercase text-xs tracking-[2px] text-[#E82127] mb-2">DAY 2 • Edinburgh &amp; Return</div>
<div class="space-y-1.5">
<div class="flex justify-between items-center bg-[#1a1f2b] border border-white/5 rounded-2xl px-4 py-2.5 text-sm group">
<div class="flex items-center gap-3">
<div class="w-2.5 h-2.5 rounded-full bg-white/60 flex-shrink-0"></div>
<div>
<div class="font-medium">Edinburgh Castle</div>
</div>
</div>
<button onclick="removeStop(this)" class="opacity-0 group-hover:opacity-100 text-white/30 hover:text-red-400"><svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.595 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.595-1.858L5 7m5 4v6m4-6v6m1-10V9a1 1 0 00-1-1h-4a1 1 0 00-1 1v1M9 7h6" /></svg></button>
</div>
<div class="flex justify-between items-center bg-[#1a1f2b] border border-white/5 rounded-2xl px-4 py-2.5 text-sm group">
<div class="flex items-center gap-3">
<div class="w-2.5 h-2.5 rounded-full bg-[#E82127] flex-shrink-0"></div>
<div>
<div class="font-medium">Glasgow Supercharger</div>
<div class="text-xs text-white/40">Arrive 24% • 19 min charge</div>
</div>
</div>
<button onclick="removeStop(this)" class="opacity-0 group-hover:opacity-100 text-white/30 hover:text-red-400"><svg xmlns="http://www.w3.org/2000/svg" class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.595 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.595-1.858L5 7m5 4v6m4-6v6m1-10V9a1 1 0 00-1-1h-4a1 1 0 00-1 1v1M9 7h6" /></svg></button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
// Tailwind script
function initializeTailwind() {
document.documentElement.style.setProperty('--accent', '#E82127');
}
// Initialize Leaflet Map (Europe / UK view)
function initMap() {
const map = L.map('map', {
zoomControl: false,
attributionControl: false
}).setView([54.8, -2.8], 5.4);
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> &copy; <a href="https://carto.com/attributions">CARTO</a>', subdomains: 'abcd', maxZoom: 20
}).addTo(map);
// Stops for London → Edinburgh route
const stops = [
{ id: 1, name: "London Battersea", lat: 51.477, lng: -0.17, type: "supercharger" },
{ id: 2, name: "Birmingham", lat: 52.486, lng: -1.89, type: "supercharger" },
{ id: 3, name: "The Balmoral Hotel", lat: 55.953, lng: -3.188, type: "hotel" },
{ id: 4, name: "Edinburgh Castle", lat: 55.9486, lng: -3.2008, type: "attraction" }
];
// Add markers
stops.forEach(stop => {
const color = stop.type === 'supercharger' ? '#E82127' :
stop.type === 'hotel' ? '#3b82f6' : '#64748b';
const icon = L.divIcon({
className: 'custom-marker',
html: `<div style="background:${color}; width:22px; height:22px; border-radius:9999px; border:2px solid white; display:flex; align-items:center; justify-content:center; box-shadow:0 2px 6px rgba(0,0,0,0.3);">
<span style="color:white; font-size:10px; font-weight:700;">${stop.type === 'supercharger' ? '⚡' : '●'}</span>
</div>`,
iconSize: [22, 22],
iconAnchor: [11, 11]
});
L.marker([stop.lat, stop.lng], { icon })
.addTo(map)
.bindPopup(`<b>${stop.name}</b><br><span style="color:#888">${stop.type}</span>`);
});
// Draw route polyline
const routeCoords = stops.map(s => [s.lat, s.lng]);
L.polyline(routeCoords, {
color: '#E82127',
weight: 4,
opacity: 0.7,
lineJoin: 'round'
}).addTo(map);
}
function sendMessage() {
const input = document.getElementById('chat-input');
if (!input.value.trim()) return;
const messagesContainer = document.getElementById('chat-messages');
// Add user message
const userMsg = document.createElement('div');
userMsg.className = 'flex justify-end';
userMsg.innerHTML = `
<div class="max-w-[82%] chat-bubble-user rounded-2xl px-4 py-3 leading-snug">
${input.value}
</div>
`;
messagesContainer.appendChild(userMsg);
const userText = input.value;
input.value = '';
// Simulate thinking
setTimeout(() => {
const thinking = document.createElement('div');
thinking.className = 'flex items-center gap-2 pl-1 text-[#E82127]';
thinking.innerHTML = `
<div class="flex gap-1">
<div class="w-1 h-1 bg-current rounded-full animate-bounce"></div>
<div class="w-1 h-1 bg-current rounded-full animate-bounce" style="animation-delay:120ms"></div>
<div class="w-1 h-1 bg-current rounded-full animate-bounce" style="animation-delay:240ms"></div>
</div>
<span class="text-xs tracking-widest">GROK IS PLANNING...</span>
`;
messagesContainer.appendChild(thinking);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
setTimeout(() => {
thinking.remove();
const reply = document.createElement('div');
reply.className = 'flex';
reply.innerHTML = `
<div class="max-w-[85%] chat-bubble-assistant rounded-2xl px-4 py-3 leading-snug">
I've added a great lunch stop at Tebay Services in the Lake District. Many Tesla owners rate it as one of the best on this route.
</div>
`;
messagesContainer.appendChild(reply);
messagesContainer.scrollTop = messagesContainer.scrollHeight;
// Simulate itinerary update
setTimeout(() => {
alert("In the real app, this would dynamically add a new stop to the map and itinerary panel.");
}, 800);
}, 1400);
}, 900);
}
function useQuickPrompt(el) {
const text = el.textContent;
const input = document.getElementById('chat-input');
input.value = text;
// Trigger send
setTimeout(() => {
sendMessage();
}, 50);
}
function removeStop(btn) {
btn.closest('.group').style.transition = 'all 0.2s';
btn.closest('.group').style.opacity = '0';
setTimeout(() => {
btn.closest('.group').remove();
}, 180);
}
// Boot everything
function init() {
initializeTailwind();
initMap();
// Keyboard hint
const input = document.getElementById('chat-input');
input.addEventListener('focus', () => {
input.placeholder = "Try: 'Add a stop near the Lake District'";
});
}
window.onload = init;
</script>
</body>
</html>
EOF
echo "High-fidelity static UI preview created"