diff --git a/client/src/pages/TeslaTripPlanner.tsx b/client/src/pages/TeslaTripPlanner.tsx
index 16a1775..a12c1b5 100644
--- a/client/src/pages/TeslaTripPlanner.tsx
+++ b/client/src/pages/TeslaTripPlanner.tsx
@@ -857,15 +857,59 @@ function ExpStat({ label, value, tone = 'text' }: { label: string; value: string
);
}
+// ─── Night transition block (between days) ───────────────────────────────────
+function NightBlock({ lastStop, onOpenHotelOptions }: { lastStop: Stop; onOpenHotelOptions: () => void }) {
+ const isHotel = lastStop.type === 'hotel';
+ const label = isHotel ? lastStop.name : 'Sleep in car at services';
+ const detail = isHotel
+ ? `Overnight · ${lastStop.cuisine || 'destination charging'}`
+ : 'Safe overnight at last Supercharger · 24h facilities';
+ return (
+
@@ -977,13 +1027,14 @@ function makePinIcon(color: string, active: boolean, hover: boolean): L.DivIcon
// ─── Top bar ─────────────────────────────────────────────────────────────────
function TopBar({
- origin, destination, onOriginChange, onDestinationChange,
+ origin, destination, onOriginChange, onDestinationChange, onODCommit,
chatInput, setChatInput, onChatSubmit, chips, onRemoveChip,
vehicle, onVehicleChange, grokStatus,
}: {
origin: string; destination: string;
onOriginChange: (v: string) => void;
onDestinationChange: (v: string) => void;
+ onODCommit: () => void;
chatInput: string; setChatInput: (v: string) => void;
onChatSubmit: () => void;
chips: string[]; onRemoveChip: (i: number) => void;
@@ -1021,6 +1072,8 @@ function TopBar({
onOriginChange(e.target.value)}
+ onKeyDown={(e) => { if (e.key === 'Enter') { e.currentTarget.blur(); onODCommit(); } }}
+ onBlur={onODCommit}
placeholder="From"
className="bg-transparent border-none outline-none text-[13px] w-full"
style={{ color: 'var(--gd-text)' }}
@@ -1031,6 +1084,8 @@ function TopBar({
onDestinationChange(e.target.value)}
+ onKeyDown={(e) => { if (e.key === 'Enter') { e.currentTarget.blur(); onODCommit(); } }}
+ onBlur={onODCommit}
placeholder="To"
className="bg-transparent border-none outline-none text-[13px] w-full"
style={{ color: 'var(--gd-text)' }}
@@ -1139,9 +1194,11 @@ export default function TeslaTripPlanner() {
const [hoverStopId, setHoverStopId] = useState
(null);
const [origin, setOrigin] = useState('');
const [destination, setDestination] = useState('');
+ const lastODSent = React.useRef<{ from: string; to: string } | null>(null);
const [variants, setVariants] = useState([]);
const [selectedVariant, setSelectedVariant] = useState('fast');
const [variantSwitching, setVariantSwitching] = useState(false);
+ const [draggingId, setDraggingId] = useState(null);
React.useEffect(() => {
fetch('/api/grok/status').then(r => r.json()).then(setGrokStatus).catch(() => {});
@@ -1243,6 +1300,52 @@ export default function TeslaTripPlanner() {
}
};
+ const reorderStops = (dragId: string, targetId: string) => {
+ if (dragId === targetId) return;
+ const next = structuredClone(itinerary);
+ // Find the dragged stop across all days and remove it
+ let dragged: Stop | null = null;
+ for (const d of next.days) {
+ const idx = d.stops.findIndex(s => s.id === dragId);
+ if (idx !== -1) {
+ dragged = d.stops.splice(idx, 1)[0] as Stop;
+ break;
+ }
+ }
+ if (!dragged) return;
+ // Insert before the target stop
+ for (const d of next.days) {
+ const idx = d.stops.findIndex(s => s.id === targetId);
+ if (idx !== -1) {
+ dragged.day = d.day;
+ d.stops.splice(idx, 0, dragged);
+ break;
+ }
+ }
+ // Re-normalise order numbers within each day
+ for (const d of next.days) {
+ d.stops.forEach((s, i) => { s.order = i + 1; });
+ }
+ next.days = next.days.filter(d => d.stops.length > 0);
+ setItinerary(next);
+ toast.info('Stop reordered');
+ };
+
+ const handleODCommit = () => {
+ const from = origin.trim();
+ const to = destination.trim();
+ if (!from || !to) return;
+ if (lastODSent.current && lastODSent.current.from === from && lastODSent.current.to === to) return;
+ // First time: only fire if user has actually edited (we auto-populate from itinerary)
+ const auto = allStops.length > 0 && from === allStops[0].name && to === allStops[allStops.length - 1].name;
+ if (auto && !lastODSent.current) {
+ lastODSent.current = { from, to };
+ return;
+ }
+ lastODSent.current = { from, to };
+ sendMessage(`Replan the trip from ${from} to ${to}`);
+ };
+
const switchVariant = (variantId: RouteVariant['id']) => {
if (variantId === selectedVariant || variantSwitching || thinking) return;
const lastUserMsg = [...messages].reverse().find(m => m.role === 'user');
@@ -1304,6 +1407,7 @@ export default function TeslaTripPlanner() {
sendMessage(chatInput)}
chips={chips} onRemoveChip={(i) => setChips(chips.filter((_, idx) => idx !== i))}
@@ -1491,18 +1595,40 @@ export default function TeslaTripPlanner() {
{stops.map((stop, si) => {
const isLast = si === stops.length - 1;
const leg = legByFromId.get(stop.id);
+ const hasNextDay = di < itinerary.days.length - 1;
+ const showNightBlock = isLast && hasNextDay && (stop.type === 'hotel' || stop.type === 'supercharger');
return (
setActiveStopId(stop.id === activeStopId ? null : stop.id)}
onHover={(h) => setHoverStopId(h ? stop.id : null)}
onSwap={(alt) => swapStop(stop.id, alt)}
onRemove={() => removeStop(stop.id)}
+ onDragStart={(e) => {
+ e.dataTransfer.setData('text/plain', stop.id);
+ e.dataTransfer.effectAllowed = 'move';
+ setDraggingId(stop.id);
+ }}
+ onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }}
+ onDrop={(e) => {
+ e.preventDefault();
+ const dragId = e.dataTransfer.getData('text/plain');
+ if (dragId && dragId !== stop.id) reorderStops(dragId, stop.id);
+ setDraggingId(null);
+ }}
+ onDragEnd={() => setDraggingId(null)}
/>
{!isLast && }
+ {showNightBlock && (
+ setActiveStopId(stop.id)}
+ />
+ )}
);
})}