e.stopPropagation()}
@@ -2819,10 +2962,11 @@ function VehicleSelectorPanel({
return (
<>
-
+
{
@@ -28,7 +30,7 @@ router.post('/chat', async (req, res) => {
return res.status(400).json({ error: 'Invalid request' });
}
- const { message, vehicle, itinerary, history = [], selectedVariant = 'fast' } = parsed.data;
+ const { message, vehicle, itinerary, history = [], selectedVariant = 'fast', origin, destination } = parsed.data;
log.info({
requestId,
@@ -37,6 +39,8 @@ router.post('/chat', async (req, res) => {
historyLength: history.length,
currentItineraryDays: itinerary?.days?.length || 0,
selectedVariant,
+ origin,
+ destination,
}, 'Parsed chat request');
// Call Grok (this will produce very detailed logs inside GrokHeadlessClient)
@@ -45,6 +49,7 @@ router.post('/chat', async (req, res) => {
itinerary,
vehicle,
selectedVariant,
+ { origin, destination },
);
const duration = Date.now() - start;
@@ -86,7 +91,7 @@ router.post('/chat/stream', async (req, res) => {
if (!parsed.success) {
return res.status(400).json({ error: 'Invalid request', issues: parsed.error.format() });
}
- const { message, vehicle, itinerary, history = [], selectedVariant = 'fast' } = parsed.data;
+ const { message, vehicle, itinerary, history = [], selectedVariant = 'fast', origin, destination } = parsed.data;
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache, no-transform');
@@ -119,6 +124,7 @@ router.post('/chat/stream', async (req, res) => {
itinerary,
vehicle,
selectedVariant,
+ { origin, destination },
);
for await (const ev of stream) {
if (cancelled) break;
diff --git a/server/services/llm/GrokHeadlessClient.ts b/server/services/llm/GrokHeadlessClient.ts
index f3b4803..3cb682d 100644
--- a/server/services/llm/GrokHeadlessClient.ts
+++ b/server/services/llm/GrokHeadlessClient.ts
@@ -160,19 +160,23 @@ export class GrokHeadlessClient {
}
}
- private buildPrompt(messages: ChatMessage[], itinerary: any, vehicle: VehicleInput, selectedVariant: string = 'fast') {
+ private buildPrompt(messages: ChatMessage[], itinerary: any, vehicle: VehicleInput, selectedVariant: string = 'fast', opts: { origin?: string; destination?: string } = {}) {
const variantBrief = {
fast: 'Fastest — minimise drive time. Pick the most direct route via motorways. Sleep in the car or at a budget hotel with destination charging. Optimise for arriving sooner, not for sightseeing.',
scenic: 'Scenic — pick the prettiest practical route even if it adds time. Favour scenic A-roads, viewpoints, charming towns, regional food. Stay at a hotel (not car-sleep). Add an extra hour or two for memorable stops.',
cheap: 'Cheapest — minimise cost. Avoid toll roads where possible, prefer off-peak charging, pick budget overnight options (car sleep or basic hotels), and choose cheaper chargers when available. Drive time can be a bit longer to save €.',
}[selectedVariant] || 'Fastest — minimise drive time.';
+ const odBlock = (opts.origin && opts.destination)
+ ? `\nTRIP ENDPOINTS (these are the ground truth — your itinerary MUST start exactly here and end exactly here):\n Origin: ${opts.origin}\n Destination: ${opts.destination}\n`
+ : '';
+
return `You are Grok Drive, an expert Tesla road trip planner for the UK and Europe. You build practical, enjoyable itineraries — not just a list of charging stops. Treat every break as a chance to eat, rest, sightsee, or sleep.
Selected route variant: ${selectedVariant.toUpperCase()}
${variantBrief}
-Current vehicle: ${vehicleName(vehicle)}
+Current vehicle: ${vehicleName(vehicle)}${odBlock}
Current itinerary: ${JSON.stringify(itinerary || {}, null, 2)}
Respond with **only** a single valid JSON object in exactly this format. No text before or after. No markdown.
@@ -294,6 +298,10 @@ Respond with **only** a single valid JSON object in exactly this format. No text
Strict route planning rules:
- Plan stops in the actual order the driver will encounter them on the road.
+- THE FIRST STOP MUST BE THE EXACT ORIGIN THE USER GAVE. THE LAST STOP MUST BE THE EXACT DESTINATION THE USER GAVE. Never start the trip in a different city, country, port or service station. If the user said "from MK78PJ" or "from Milton Keynes", the very first stop must be at that postcode/town with type "custom" (or "supercharger"/"hotel" if it genuinely is one), real lat/lng, and a name like "Start · Milton Keynes (MK7 8PJ)".
+- UK postcodes (e.g. MK7 8PJ, SW1A 1AA, EH1 1YZ) are valid starting points. Geocode them precisely — the first letters are the postal area (MK = Milton Keynes, SW = London SW, EH = Edinburgh). Do not skip the UK leg of the trip just because chargers are sparse there.
+- For UK → mainland Europe trips: include the UK departure point (e.g. Folkestone Eurotunnel, Dover ferry, or Hull ferry) and the corresponding mainland arrival point (Calais/Coquelles, Dunkirk, Rotterdam, etc.) as explicit stops. Note the crossing in the description and add a sensible duration. Do NOT begin the itinerary in Calais — that erases the UK side of the journey.
+- For mainland Europe → UK trips: same but in reverse.
- Choose Superchargers that are realistically reachable given the vehicle range.
- Space charging stops sensibly (every 150-250km depending on route and battery).
- Calculate realistic estArrivalBattery based on distance driven since last charge.
@@ -355,21 +363,21 @@ ${messages.map(m => `${m.role.toUpperCase()}: ${m.content}`).join('\n')}
Respond with ONLY the JSON object.`;
}
- async chat(messages: ChatMessage[], itinerary: any, vehicle: VehicleInput, selectedVariant: string = 'fast'): Promise {
+ async chat(messages: ChatMessage[], itinerary: any, vehicle: VehicleInput, selectedVariant: string = 'fast', opts: { origin?: string; destination?: string } = {}): Promise {
const requestId = crypto.randomUUID().slice(0, 8);
log.info({ requestId, vehicle: vehicleName(vehicle), messageCount: messages.length, selectedVariant }, '=== NEW CHAT REQUEST ===');
const activeProvider = await this.getActiveProvider(requestId);
if (activeProvider === 'xai') {
- return this.callXaiApi(messages, itinerary, vehicle, requestId, selectedVariant);
+ return this.callXaiApi(messages, itinerary, vehicle, requestId, selectedVariant, opts);
}
if (activeProvider === 'fallback') {
return this.dumbFallback(messages, requestId);
}
// LOCAL PERSONAL GROK CLI
- const prompt = this.buildPrompt(messages, itinerary, vehicle, selectedVariant);
+ const prompt = this.buildPrompt(messages, itinerary, vehicle, selectedVariant, opts);
const tmp = await mkdtemp(join(tmpdir(), 'grok-eu-'));
const disallowed = env.nodeEnv === 'development'
@@ -420,7 +428,7 @@ Respond with ONLY the JSON object.`;
} catch (err) {
log.error({ requestId, err: String(err) }, 'Local authenticated Grok CLI failed — falling back to xAI API');
if (env.xaiApiKey) {
- return this.callXaiApi(messages, itinerary, vehicle, requestId, selectedVariant);
+ return this.callXaiApi(messages, itinerary, vehicle, requestId, selectedVariant, opts);
}
return this.dumbFallback(messages, requestId);
} finally {
@@ -432,7 +440,7 @@ Respond with ONLY the JSON object.`;
* Streaming chat — yields incremental partial itineraries as Grok produces output.
* Falls back to non-streaming if local CLI is unavailable.
*/
- async *chatStream(messages: ChatMessage[], itinerary: any, vehicle: VehicleInput, selectedVariant: string = 'fast'): AsyncGenerator {
+ async *chatStream(messages: ChatMessage[], itinerary: any, vehicle: VehicleInput, selectedVariant: string = 'fast', opts: { origin?: string; destination?: string } = {}): AsyncGenerator {
const requestId = crypto.randomUUID().slice(0, 8);
log.info({ requestId, vehicle: vehicleName(vehicle), selectedVariant }, '=== NEW STREAMING CHAT REQUEST ===');
@@ -442,7 +450,7 @@ Respond with ONLY the JSON object.`;
// No real streaming for xAI/fallback yet — just do the regular call and emit a single done event
yield { type: 'thinking', message: 'Asking Grok…' };
const result = activeProvider === 'xai'
- ? await this.callXaiApi(messages, itinerary, vehicle, requestId, selectedVariant)
+ ? await this.callXaiApi(messages, itinerary, vehicle, requestId, selectedVariant, opts)
: await this.dumbFallback(messages, requestId);
yield {
type: 'done',
@@ -454,7 +462,7 @@ Respond with ONLY the JSON object.`;
return;
}
- const prompt = this.buildPrompt(messages, itinerary, vehicle, selectedVariant);
+ const prompt = this.buildPrompt(messages, itinerary, vehicle, selectedVariant, opts);
const tmp = await mkdtemp(join(tmpdir(), 'grok-eu-stream-'));
const disallowed = env.nodeEnv === 'development'
? 'search_replace,write_file,Agent,run_terminal_cmd'
@@ -488,6 +496,9 @@ Respond with ONLY the JSON object.`;
let lastParseLen = 0;
let lastEmittedStops = 0;
let lastEmittedDays = 0;
+ let thoughtBuffer = '';
+ let lastEmittedThought = '';
+ let lastEmittedThoughtLen = 0;
let closed = false;
let closeCode: number | null = null;
let waker: (() => void) | null = null;
@@ -574,7 +585,21 @@ Respond with ONLY the JSON object.`;
}
}
} else if (ev.type === 'thought' && typeof ev.data === 'string') {
- // Optional: surface short snippets of Grok's thinking
+ thoughtBuffer += ev.data;
+ // Emit on newline boundaries or when we've buffered enough; show the last
+ // line trimmed and capped so the UI gets a steady stream of short snippets.
+ const lastNewline = thoughtBuffer.lastIndexOf('\n');
+ const shouldEmit = lastNewline >= 0 || thoughtBuffer.length - lastEmittedThoughtLen > 200;
+ if (shouldEmit) {
+ const tail = lastNewline >= 0 ? thoughtBuffer.slice(lastNewline + 1) : thoughtBuffer;
+ const snippet = (tail || thoughtBuffer).trim().replace(/\s+/g, ' ').slice(-220);
+ if (snippet && snippet !== lastEmittedThought) {
+ lastEmittedThought = snippet;
+ lastEmittedThoughtLen = thoughtBuffer.length;
+ yield { type: 'thinking', message: snippet };
+ }
+ if (lastNewline >= 0) thoughtBuffer = thoughtBuffer.slice(lastNewline + 1);
+ }
} else if (ev.type === 'error') {
log.error({ requestId, msg: ev.message }, 'grok streaming error event');
yield { type: 'error', error: ev.message || 'Grok stream error' };
@@ -604,8 +629,8 @@ Respond with ONLY the JSON object.`;
}
}
- private async callXaiApi(messages: ChatMessage[], itinerary: any, vehicle: VehicleInput, requestId: string, selectedVariant: string = 'fast'): Promise {
- const prompt = this.buildPrompt(messages, itinerary, vehicle, selectedVariant);
+ private async callXaiApi(messages: ChatMessage[], itinerary: any, vehicle: VehicleInput, requestId: string, selectedVariant: string = 'fast', opts: { origin?: string; destination?: string } = {}): Promise {
+ const prompt = this.buildPrompt(messages, itinerary, vehicle, selectedVariant, opts);
log.info({ requestId, promptLength: prompt.length, model: 'grok-4.3' }, 'Calling xAI API (grok-4.3 + JSON mode)');
try {