diff --git a/client/src/pages/TeslaTripPlanner.tsx b/client/src/pages/TeslaTripPlanner.tsx index faa3d11..e920b90 100644 --- a/client/src/pages/TeslaTripPlanner.tsx +++ b/client/src/pages/TeslaTripPlanner.tsx @@ -118,13 +118,89 @@ const EMPTY_ITINERARY: Itinerary = { const STOP_TYPES: StopType[] = ['supercharger', 'destination-charger', 'hotel', 'attraction', 'restaurant', 'cafe', 'viewpoint', 'custom', 'origin', 'destination', 'tunnel']; -const VEHICLES = [ - { name: 'Model Y Long Range', trim: 'Long Range AWD', rangeKm: 514, efficiency: 165 }, - { name: 'Model 3 Highland LR', trim: 'Long Range', rangeKm: 549, efficiency: 155 }, - { name: 'Model S Long Range', trim: 'Long Range', rangeKm: 634, efficiency: 175 }, - { name: 'Model Y RWD (EU)', trim: 'Standard Range', rangeKm: 455, efficiency: 158 }, +interface VehicleTrim { + id: string; + name: string; // e.g. "Long Range AWD" + rangeKm: number; + kw: number; + sec0to60: number; + topKmh: number; + badge?: string; +} +interface VehicleModel { + id: string; + name: string; // e.g. "Model Y" + description: string; + trims: VehicleTrim[]; +} +interface Vehicle { + modelId: string; + trimId: string; + name: string; // model name + trim: string; // trim name + rangeKm: number; + kw: number; + sec0to60: number; + topKmh: number; + badge?: string; +} + +const TESLA_MODELS: VehicleModel[] = [ + { + id: 'model-s', name: 'Model S', description: 'Fastback sedan', + trims: [ + { id: 'lr', name: 'Long Range', rangeKm: 634, kw: 250, sec0to60: 3.1, topKmh: 240 }, + { id: 'plaid', name: 'Plaid', rangeKm: 600, kw: 250, sec0to60: 1.99, topKmh: 322, badge: 'Performance' }, + ], + }, + { + id: 'model-3', name: 'Model 3', description: 'Compact sedan', + trims: [ + { id: 'std', name: 'Standard Range', rangeKm: 438, kw: 175, sec0to60: 5.6, topKmh: 201 }, + { id: 'lr-rwd', name: 'Long Range RWD', rangeKm: 553, kw: 250, sec0to60: 4.9, topKmh: 201, badge: 'Best range' }, + { id: 'lr-awd', name: 'Long Range AWD', rangeKm: 528, kw: 250, sec0to60: 4.2, topKmh: 201 }, + { id: 'perf', name: 'Performance', rangeKm: 528, kw: 250, sec0to60: 2.9, topKmh: 261, badge: 'Performance' }, + ], + }, + { + id: 'model-y', name: 'Model Y', description: 'Crossover · best-seller', + trims: [ + { id: 'std', name: 'Standard Range', rangeKm: 460, kw: 175, sec0to60: 5.6, topKmh: 217 }, + { id: 'lr-rwd', name: 'Long Range RWD', rangeKm: 531, kw: 250, sec0to60: 5.9, topKmh: 217 }, + { id: 'lr-awd', name: 'Long Range AWD', rangeKm: 514, kw: 250, sec0to60: 4.8, topKmh: 217, badge: 'Most popular' }, + { id: 'perf', name: 'Performance', rangeKm: 488, kw: 250, sec0to60: 3.5, topKmh: 250, badge: 'Performance' }, + ], + }, + { + id: 'model-x', name: 'Model X', description: 'Three-row SUV · falcon doors', + trims: [ + { id: 'lr', name: 'Long Range', rangeKm: 543, kw: 250, sec0to60: 3.8, topKmh: 250 }, + { id: 'plaid', name: 'Plaid', rangeKm: 528, kw: 250, sec0to60: 2.5, topKmh: 262, badge: 'Performance' }, + ], + }, + { + id: 'cybertruck', name: 'Cybertruck', description: 'Angular pickup', + trims: [ + { id: 'rwd', name: 'Long Range RWD', rangeKm: 563, kw: 350, sec0to60: 6.5, topKmh: 180 }, + { id: 'awd', name: 'AWD', rangeKm: 547, kw: 350, sec0to60: 4.1, topKmh: 180 }, + { id: 'beast', name: 'Cyberbeast', rangeKm: 515, kw: 350, sec0to60: 2.6, topKmh: 209, badge: 'Performance' }, + ], + }, ]; +const DEFAULT_VEHICLE: Vehicle = (() => { + const m = TESLA_MODELS.find(x => x.id === 'model-y')!; + const t = m.trims.find(tr => tr.id === 'lr-awd')!; + return { modelId: m.id, trimId: t.id, name: m.name, trim: t.name, rangeKm: t.rangeKm, kw: t.kw, sec0to60: t.sec0to60, topKmh: t.topKmh, badge: t.badge }; +})(); + +function abbrevTrim(trim: string): string { + return trim + .replace('Long Range', 'LR') + .replace('Standard Range', 'Std') + .replace('Performance', 'Perf'); +} + const QUICK_PROMPTS = [ 'Plan a 2-day trip from London to Edinburgh in my Model Y', 'I want to drive from Amsterdam to Munich', @@ -1031,7 +1107,7 @@ function makePinIcon(color: string, active: boolean, hover: boolean): L.DivIcon function TopBar({ origin, destination, onOriginChange, onDestinationChange, onODCommit, chatInput, setChatInput, onChatSubmit, chips, onRemoveChip, - vehicle, onVehicleChange, grokStatus, onOpenGpx, + vehicle, onOpenVehiclePanel, grokStatus, onOpenGpx, }: { origin: string; destination: string; onOriginChange: (v: string) => void; @@ -1040,7 +1116,7 @@ function TopBar({ chatInput: string; setChatInput: (v: string) => void; onChatSubmit: () => void; chips: string[]; onRemoveChip: (i: number) => void; - vehicle: typeof VEHICLES[number]; onVehicleChange: (v: typeof VEHICLES[number]) => void; + vehicle: Vehicle; onOpenVehiclePanel: (rect: DOMRect) => void; grokStatus: { label?: string }; onOpenGpx: () => void; }) { @@ -1141,28 +1217,22 @@ function TopBar({ - {/* Vehicle chip */} -