feat: Phase 2 — variant strip, while-here, charger swap block
Adds the three big "options" UX wins from Direction B:
1. Route variants (Fastest / Scenic / Cheapest)
- Grok prompt now returns a top-level variants[] summary with
drive/charge/cost/distance/pros for each variant, plus a
selectedVariant indicating which one the stops reflect.
- VariantStrip renders under the top bar with selected-state
styling, tone-coloured highlight (red/green/blue) on the most
relevant stat, and 3-5 pros pills.
- Clicking a variant fires /api/chat with selectedVariant=<id> so
Grok re-plans with that variant's bias. A "switching" state
disables the strip while the request is in flight.
- The chat route accepts selectedVariant ('fast'|'scenic'|'cheap')
and the GrokHeadlessClient threads it through both the local CLI
and xAI API paths.
2. While here (food / do / see / shop / rest)
- Every Supercharger, destination-charger and hotel stop now
returns a nearby[] array with category/icon/name/detail.
- Expanded stop card has tabs (All / Food / Do / See) — tabs
auto-hide when no items in that category. Two-column grid of
named places with walk-time + rating, e.g. "M&S Foodhall · 1 min
walk · 4.3★ · sandwiches".
3. Charger swap block
- Every charging stop now returns chargerOptions[] with the
current pick + 1-3 alternatives at the same location, each with
network (Tesla/Ionity/Allego/Fastned/BP Pulse), stalls, kW,
pricePerKwh, detourMin and an optional badge (Faster/Cheaper/
More stalls/Newer).
- ChargerSwapBlock shows the current charger as a red-tinted
header row that expands to reveal alternatives with stats and a
Use button per row.
Renamed the existing AlternativeStop UI label from "alternative(s)"
to "location alternative(s)" so it's clear when the user is swapping
the stop *location* vs swapping the *charger at the same location*.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+11
-2
@@ -12,6 +12,7 @@ const ChatRequestSchema = z.object({
|
||||
vehicle: z.object({ name: z.string(), rangeKm: z.number() }),
|
||||
itinerary: z.any().optional(),
|
||||
history: z.array(z.object({ role: z.enum(['user', 'assistant']), content: z.string() })).optional(),
|
||||
selectedVariant: z.enum(['fast', 'scenic', 'cheap']).optional(),
|
||||
});
|
||||
|
||||
router.post('/chat', async (req, res) => {
|
||||
@@ -27,7 +28,7 @@ router.post('/chat', async (req, res) => {
|
||||
return res.status(400).json({ error: 'Invalid request' });
|
||||
}
|
||||
|
||||
const { message, vehicle, itinerary, history = [] } = parsed.data;
|
||||
const { message, vehicle, itinerary, history = [], selectedVariant = 'fast' } = parsed.data;
|
||||
|
||||
log.info({
|
||||
requestId,
|
||||
@@ -35,13 +36,15 @@ router.post('/chat', async (req, res) => {
|
||||
vehicle: vehicle.name,
|
||||
historyLength: history.length,
|
||||
currentItineraryDays: itinerary?.days?.length || 0,
|
||||
selectedVariant,
|
||||
}, 'Parsed chat request');
|
||||
|
||||
// Call Grok (this will produce very detailed logs inside GrokHeadlessClient)
|
||||
const result = await grok.chat(
|
||||
[...history, { role: 'user' as const, content: message }],
|
||||
itinerary,
|
||||
vehicle
|
||||
vehicle,
|
||||
selectedVariant,
|
||||
);
|
||||
|
||||
const duration = Date.now() - start;
|
||||
@@ -50,6 +53,12 @@ router.post('/chat', async (req, res) => {
|
||||
if (result.updatedItinerary) {
|
||||
payload.itinerary = result.updatedItinerary;
|
||||
}
|
||||
if (result.variants && Array.isArray(result.variants)) {
|
||||
payload.variants = result.variants;
|
||||
}
|
||||
if (result.selectedVariant) {
|
||||
payload.selectedVariant = result.selectedVariant;
|
||||
}
|
||||
|
||||
log.info({
|
||||
requestId,
|
||||
|
||||
Reference in New Issue
Block a user