diff --git a/client/src/pages/TeslaTripPlanner.tsx b/client/src/pages/TeslaTripPlanner.tsx index e920b90..64cb7ad 100644 --- a/client/src/pages/TeslaTripPlanner.tsx +++ b/client/src/pages/TeslaTripPlanner.tsx @@ -520,19 +520,24 @@ const VARIANT_ICON: Record void; switching: boolean; + cachedIds: string[]; + showCompare: boolean; + onToggleCompare: () => void; }) { if (variants.length === 0) return null; + const compareEligible = cachedIds.length >= 2; return (
+
{variants.map(v => { const isSel = v.id === selected; const tone = VARIANT_TONE[v.tone]; @@ -585,6 +590,23 @@ function VariantStrip({ ); })} +
+
); } @@ -1273,6 +1295,8 @@ export default function TeslaTripPlanner() { const [variants, setVariants] = useState([]); const [selectedVariant, setSelectedVariant] = useState('fast'); const [variantSwitching, setVariantSwitching] = useState(false); + const [variantCache, setVariantCache] = useState>({}); + const [showCompare, setShowCompare] = useState(false); const [draggingId, setDraggingId] = useState(null); const [modal, setModal] = useState< | { kind: 'customise'; stopId: string } @@ -1313,10 +1337,12 @@ export default function TeslaTripPlanner() { } if (cancelled) return; setLegs(fetched); + // Refresh cache with the up-to-date legs for the current variant + setVariantCache(prev => ({ ...prev, [selectedVariant]: { itinerary, legs: fetched } })); }; fetchRoutes(); return () => { cancelled = true; }; - }, [itinerary]); + }, [itinerary]); // eslint-disable-line react-hooks/exhaustive-deps const computedTotals = React.useMemo(() => { if (legs.length === 0) return null; @@ -1356,6 +1382,11 @@ export default function TeslaTripPlanner() { if (data.itinerary) { const clean = await normalizeAndSanitizeItinerary(data.itinerary); setItinerary(clean); + // Pre-cache for the variant we just rendered (legs will be filled by useEffect) + const variantJustRendered = typeof data.selectedVariant === 'string' + ? data.selectedVariant as RouteVariant['id'] + : opts.variant ?? selectedVariant; + setVariantCache(prev => ({ ...prev, [variantJustRendered]: { itinerary: clean, legs: [] } })); } if (Array.isArray(data.variants)) { setVariants(normalizeVariants(data.variants)); @@ -1476,6 +1507,19 @@ export default function TeslaTripPlanner() { const switchVariant = (variantId: RouteVariant['id']) => { if (variantId === selectedVariant || variantSwitching || thinking) return; + // Cache the current variant before switching + if (itinerary.days.length > 0) { + setVariantCache(prev => ({ ...prev, [selectedVariant]: { itinerary, legs } })); + } + // If target is already cached, swap instantly with no Grok call + const cached = variantCache[variantId]; + if (cached) { + setItinerary(cached.itinerary); + setLegs(cached.legs); + setSelectedVariant(variantId); + toast.success(`Switched to ${variantId} (cached)`); + return; + } const lastUserMsg = [...messages].reverse().find(m => m.role === 'user'); if (!lastUserMsg) { toast.info('Send a trip prompt first'); @@ -1551,6 +1595,9 @@ export default function TeslaTripPlanner() { selected={selectedVariant} onSelect={switchVariant} switching={variantSwitching} + cachedIds={Object.keys(variantCache)} + showCompare={showCompare} + onToggleCompare={() => setShowCompare(s => !s)} /> )} @@ -1598,6 +1645,19 @@ export default function TeslaTripPlanner() { ); })} + {showCompare && Object.entries(variantCache) + .filter(([id]) => id !== selectedVariant) + .map(([id, cached]) => { + const variant = variants.find(v => v.id === id); + const color = variant?.tone === 'green' ? '#4ade80' : variant?.tone === 'blue' ? '#60a5fa' : '#e31937'; + return cached.legs.map((leg, i) => ( + + )); + })} {legs.map((leg, i) => ( @@ -1606,7 +1666,31 @@ export default function TeslaTripPlanner() { ))} - {/* Map legend */} + {/* Map legend (variants when comparing, else stop types) */} + {showCompare ? ( +
+ {variants.filter(v => v.id === selectedVariant || variantCache[v.id]).map(v => { + const isSel = v.id === selectedVariant; + const c = v.tone === 'green' ? '#4ade80' : v.tone === 'blue' ? '#60a5fa' : '#e31937'; + return ( +
+
+
+ {v.label} +
+
+ ); + })} +
+ ) : (
Sleep
See
+ )} {/* Refinements overlay */} {chips.length > 0 && (