feat: faster variant switching, live Grok thoughts, accurate trip endpoints
- Bump z-index on vehicle selector and modals (z-50 sat below Leaflet panes) - Prefetch the other route variants in the background as soon as the first trip lands; switching now hits the cache and is near-instant - Surface Grok's streaming thoughts to the UI: glassy overlay on the empty map + sidebar callout, with skeleton shimmer until the first thought - Thread explicit origin/destination from the TopBar through to the prompt as a ground-truth block; harden rules so the first/last stops match the user's actual endpoints and cross-Channel trips include both sides
This commit is contained in:
@@ -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<GrokResponse> {
|
||||
async chat(messages: ChatMessage[], itinerary: any, vehicle: VehicleInput, selectedVariant: string = 'fast', opts: { origin?: string; destination?: string } = {}): Promise<GrokResponse> {
|
||||
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<StreamEvent> {
|
||||
async *chatStream(messages: ChatMessage[], itinerary: any, vehicle: VehicleInput, selectedVariant: string = 'fast', opts: { origin?: string; destination?: string } = {}): AsyncGenerator<StreamEvent> {
|
||||
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<GrokResponse> {
|
||||
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<GrokResponse> {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user