Commit Graph

16 Commits

Author SHA1 Message Date
76749affb6 Make the airport→hotel drop-off a first-class, independently-editable leg
The drop-off (land → hotel, 'drop bags') was always by car, not L3-editable,
and the walk/taxi toggle was trip-global. It also read dayBase() rather than
the actual booked hotel, and the open route card went stale after enrichment
(the rail updated, the card didn't).

- transferIn is now a full leg: {mode, conf, geometry, hotelName} (both the
  apply_flight_anchors tool and the healTravelAnchors self-heal build it).
- enrichLegs routes the drop-off FIRST (so the stop-leg token bails can't
  starve it), idempotently, in its own mode; renderMap draws it from its
  routed geometry, walk=green / taxi=blue.
- The drop-off rail row is a normal clickable leg (not struct-leg): clicking
  it opens the same L3 editor as any stop-to-stop leg, with the walk/taxi
  toggle.
- openL3 generalises to openLegEditor(leg, from, to, ...) so any leg with
  explicit endpoints is editable; openArrivalEditor() edits the drop-off.
- The toggle is now per-leg (setLegMode) — it re-routes just the leg being
  edited and keeps the card, the itinerary rail, and the map in sync (the
  rail re-renders live, and enrichment refreshes an open card), killing the
  stale-state-after-toggle bug. rebuildLegs preserves each leg's mode.
- setTravelMode (no UI, kept as a global API) genuinely flips every on-ground
  leg + the drop-off.

Verified: cdp_leg 17/17 (drop-off routed on load, L3-editable, mode+colour+
pane update live, card==rail), struct 13/13, dates 7/7, Florence/Boston smoke
0 JS errors.
2026-09-09 19:33:48 -04:00
7bdb455074 Route card: put walk/taxi on the leg popup; make the drop-off ride hoverable
Two fixes from the review of the rail rework:

- The airport→hotel "drop bags" ride was a special case: it was a structural
  row with no map line and no hover. It now draws a real polyline (airport →
  hotel) in renderMap and the rail row highlights it on hover / flies to it on
  click, exactly like every other route.
- The walk/taxi toggle no longer sits as a permanent "Getting around" block at
  the top of the rail. It lives on the route card — the popup a leg click opens
  (#l3-mode). It shows only for on-ground legs (a flight / train+bus segment has
  a fixed mode); flipping it re-estimates every on-ground leg and re-binds the
  open editor to the new leg (l3.idx). The L3 waypoint-drag handler now routes
  at the leg's actual mode instead of hardcoding 'foot'.

Verified: CDP — drop-off line drawn + hover highlights then restores (weight
5→7→5); clicking a leg opens the card with the toggle; Taxi flips travelMode +
shortens legs, card stays open + rebound (idx preserved); back to foot restores;
top toggle gone; 0 JS exceptions. Structural suite 13/13 still green.
2026-09-09 17:56:58 -04:00
733843d096 Rail: real day structure (airport→hotel→day→hotel) + walk/taxi toggle
The left pane used to show one global hotel chip pinned to the top for the
whole trip. Now the itinerary reads like the day actually happens:

- Arrival day starts with an airport card (✈️ Landed — CTG, arr 10:20, the
  flight you booked) then a ground-transfer leg to the hotel ("drop bags"),
  then the activities with travel legs between them, then a ride back to the
  hotel and a per-night hotel card (🏨 … tonight, the right hotel + dates).
- Departure day ends with a ride to the airport and the flight. Fixed a bug
  where apply_flight_anchors kept the dataset's placeholder flight (e.g.
  AV 1352→BOG) instead of replacing it with the user's real ticket (CM 875→LGA).
- "Getting around" toggle (🚶 Walk / 🚗 Taxi) where the base-chip used to sit;
  every travel leg re-estimates instantly (26 km/h car vs 4.6 walk), per-trip
  and persisted.
- healTravelAnchors() rebuilds the arrival/departure airport cards + transfers
  on load for plans saved before day objects carried them.

Verified: CDP suites — 12/12 structural checks (incl. reload persistence),
7/7 date re-dating, 4/4 no-JS-error smoke on the other two trips.
2026-09-09 17:34:18 -04:00
54f049eecc Enforce the date-sync invariant in commit/restore, not just the flight tool
redateStaysAndPlaces() now runs in commit() (before the snapshot, so
history stores consistent pairs) and in restore(). Any present or future
edit that changes day dates keeps stays/places/booking strings in sync
for free; consistent plans are an idempotent no-op. apply_flight_anchors
no longer calls it directly — commit is the single enforcement point.
enterTrip's load-time self-heal stays for plans persisted by older code.
2026-09-09 16:43:47 -04:00
6c90a656bd Self-heal stale saved plans on load (days Nov / stays Sep)
Plans persisted by the old re-anchoring have November day dates but
September hotel stays, so covers() matched nothing and the base chip /
per-day hotel anchoring were wrong. enterTrip now runs the idempotent
redateStaysAndPlaces() after loading a saved plan and re-persists, so a
stale plan heals itself on the next load without a re-upload.
2026-09-09 16:34:21 -04:00
16c35b0bce Re-date the whole plan on flight upload: stays, tabs, chips, title
The re-anchoring only re-dated days[], leaving every other date-bearing
piece stale (day tabs read the original M.days, hotel stays/places kept
September checkIns so the base-chip and covers() logic went wrong, crumb
title and version pill never re-rendered). Now apply_flight_anchors also:
  - re-dates stays.checkIn/Out, places.dates and hotel booking strings
    from the day segments (redateStaysAndPlaces)
  - re-renders day tabs, base chip, crumb and version pill, and reads
    tab/trip-rail labels from the live days
  - persists immediately (force) — the one edit we must never lose
  - stayRange derives the month instead of hardcoding 'Sep'

Plus the .eml fix: mail metadata headers embed field names in their
values (X-Arc: ...content-type:from:subject:...), which the old
unanchored case-insensitive match grabbed — so single-part MIME bodies
were never decoded and raw headers + QP escapes leaked into the model's
context. Header fields are now parsed line-anchored, last occurrence
wins. The user's three real .emls now extract to clean text.

Prompt: when a document shows only one flight leg, the model is told to
search the other attached documents for the missing leg before asking.

E2E: drop the real AA e-mail -> model finds the return leg in the other
attached doc, applies the anchors, and title/tabs/chip/stays all show
7–15 Nov, surviving a hard reload.
2026-09-09 16:31:26 -04:00
bf72fdf32d mock: server-side per-trip store — originals, parsed docs, conversation
Context and documents now live on the server between turns (and across
reloads), per project, as requested:

  mock/store/<trip>/docs/<docId>-<file>        ORIGINAL uploaded bytes
  mock/store/<trip>/docs/<docId>-<file>.txt    extracted text (+ header)
  mock/store/<trip>/chat.jsonl                 full LLM conversation

Endpoints: GET /trip-store/:trip (manifest incl. doc texts),
GET .../doc/:docId, GET .../chat (last 100 turns), POST .../doc,
POST .../chat. /parse-doc's extraction factored into shared finishParse()
so both paths use identical logic. The store dir is gitignored (PII).

Client: enterTrip rehydrates tripDocs + chatLog from the server
(loadTripStore, race-guarded, one-time migration of localStorage docs
from older builds), re-renders past turns in the chat panel, and
askLLM persists each turn fire-and-forget. localStorage now carries
only the plan state.

Verified end-to-end: drop 133 KB email → direct re-anchor, original +
parsed files on disk, both turns in chat.jsonl → full page reload →
doc chip, document and conversation restored from the server → question
asked without re-upload answered correctly (AK-77Q2ZD, 14C/22A).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-09 15:31:06 -04:00
91e93ec265 mock: apply flight anchors directly + give the model conversation memory
The real-email case exposed two gaps:

1. Confirmation friction: when the booked dates didn't match the plan,
   the model asked to confirm instead of anchoring. The guideline now
   says to apply directly (the user's ticket is the truth, and the edit
   is undo-able) and report the date shift — verified: a dropped booking
   now re-anchors in one round, and a redundant "yes" afterwards is
   handled gracefully ("already done").

2. Amnesia on the follow-up: the model has no conversation memory, so a
   confirming "yes" round had to re-derive the dates from the document
   in context — and that copy was clipped at 20k chars. Now the recent
   turns ride in the model's context (chatLog, ~12k char budget, upload
   messages excluded since docSection carries the document), and the doc
   is no longer clipped in-context at all (60k server cap ≪ 131k-token
   window). A new guideline tells the model to apply a confirmed change
   from its own earlier message rather than claiming it can't see info
   it already quoted.

Extraction hardening: broader HTML detection (20k-char scan + tag
density), and a brute-force base64-block fallback in decodeMime for
mail clients with quirky MIME structure. The server now logs an
extraction preview and saves the exact model-facing text to
/tmp/parse-doc-last.txt for debugging real files.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-09 14:48:59 -04:00
f03e7b5dbc mock: robust booking-doc extraction + documents persist across turns
An 118 KB HTML flight confirmation exposed two problems:

1. Extraction — the old <[^>]+> tag-strip broke on a ">" inside a quoted
   attribute (inline SVG data-URIs are common in mail markup) and the
   text was silently cut at 20 000 chars, so the model saw a mangled,
   truncated document ("it was truncated"). /parse-doc now uses a
   quote-aware state-machine extractor (skips comments / MSO conditional
   comments / style / script / head blocks; block tags → newlines; table
   cells → " | "; single-pass entity decode), decodes .eml / MIME mail
   (base64 + quoted-printable, html part preferred, one level of nested
   multipart), collapses 10×+ repeated boilerplate lines, and caps at
   60 000 chars with an explicit `truncated` flag (server logs it too).
   File limit raised to 16 MB; Outlook .msg gets a clear "forward as
   HTML" error. A 130 KB bloated fixture now extracts to 901 clean chars
   with all flight details intact.

2. Memory — uploaded documents now stay attached to the trip: persisted
   in tripStore with the plan, shown as chips in the chat, and included
   in the model's context on EVERY turn, so follow-up questions
   ("what's my confirmation code?") are answered without a re-upload and
   documents survive reloads. 📎 now works for any reference doc (flight
   bookings still trigger the re-anchor flow; the model asks to confirm
   when the booked dates don't match the plan).

Also: askLLM refuses to start a second agent loop while one is in
flight — a question typed during a long tool loop used to fork a
parallel loop that corrupted the first.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-09 14:16:36 -04:00
88dc0c5323 mock: drop a real flight booking → LLM re-anchors the trip
The user drags a flight ticket (PDF/text/ics/html) onto the chat panel,
or uses the new 📎 button. The doc is text-extracted server-side
(pdftotext for PDFs; images rejected with a clear "no OCR" message),
then the LLM reads it, extracts the confirmed arrival + departure
(date, local time, airport, city) and calls a new deterministic tool,
apply_flight_anchors, which reconfigures the trip:

- re-dates the day window across the booked [arrival, departure] span
  (contiguous when the day counts match, evenly spread when they don't)
- records the flights as hard-anchor bookings, replacing placeholders
- shortens the arrival day (start after landing + a real airport→hotel
  taxi time) and the departure day (ends exactly at the booked time)
- trims stops that no longer fit and updates the trip title's range

Undo/redo and reload persistence now version the trip's bookings + title
alongside days/stays, so a re-configuration reverts and reloads cleanly.
Server gains POST /parse-doc. Verified E2E (LLM extraction + reconfigure),
idempotency, drag-and-drop, and undo; non-flight agent unaffected.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-09 13:20:47 -04:00
c7b5ecb8da mock: give the chat assistant a tool-calling agent loop
The LLM now orchestrates the plan instead of only answering from a
static snapshot. It reads the live plan, calls deterministic client-side
tools, and proposes small fix-ops; the tools validate, compute and
commit, so every edit is undo-able (LLM proposes, tools decide).

This llama.cpp build ignores the native OpenAI `tools` field, so tool
calls use a text protocol: the model emits <tool>{"name","args"}</tool>,
the app executes it and feeds the JSON result back as the next turn; the
loop ends when the model stops calling (capped at 10 turns).

Tools: itinerary_summary, search_places, place_facts, route_between
(real OSRM walk, straight-line fallback), review_plan (day overrun /
meal-hour / duplicate checks), and fix-ops add_stop, remove_stop,
move_stop, set_duration, set_start. Places and stops resolve by id or
name; dayIds are surfaced in the plan snapshot ([D1 · date]) so the model
doesn't guess. A subtle ⚙ activity line shows each tool call in the chat.

Verified end-to-end in headless Chrome: factual routing (route_between),
search+swap (search_places→add_stop, incumbent demoted to backup), and
diagnose+fix (review_plan→move_stop→set_start→re-review), with clean
undo-able commits each time.
2026-09-09 11:48:00 -04:00
af63297297 mock: wire the local LLM backend (llama.cpp Qwen3.8-27B-UD3-Q5-dual)
server.js: /llm-status probe + /llm-chat proxy to the local llama.cpp
OpenAI-compatible server (192.168.3.7:1234, model ctx 131072, 2 parallel
sessions). Model name, base URL and token budget pinned server-side
(LLM_BASE/LLM_MODEL/LLM_MAX_TOKENS env to override); non-streaming, since
the hidden thinking text shares max_tokens with the visible answer.

app.js: checkLLM() alongside checkRouter(); free-form chat now goes to the
model with a compact live plan snapshot (bookings, stays, per-day stops
with times) as system context. Deterministic UI intents (discovery
drawer, time nudges, swaps) stay local — the LLM proposes, the UI
commits. Canned-reply fallback when the server is down or the model is
not loaded; status badge now reads e.g. 'online · router · llm'.
2026-09-09 10:19:39 -04:00
33ed5d9fe6 multi-trip mock + Colombia trip with a real walking router
The mock can now work several trips at once: a trip picker in the
topbar, per-trip state in localStorage (edits + version history survive
switching), and a per-trip router key so each trip routes on its own
OSRM extract.

Colombia (Cartagena -> Santa Marta, 8 days) becomes the default working
trip, inside a new walking-profile OSRM instance:
- scripts/crop_pbf.py: streaming 3-pass PBF cropper (no PBF library;
  the country file is too big for osmium extract -s complete_ways on
  this box). Framing validated against osmium test fixtures and
  osrm-extract. Drops relations (foot profile ignores restrictions).
- scripts/setup-osrm-colombia.sh: download -> crop (~4.6 MB) ->
  extract/partition/customize with the official foot.lua -> serve
  :5003. Verified: Getsemaní -> Castillo San Felipe 7.98 km / 96 min.
- server.js: ROUTERS map, ?router=colombia|northeast on /route,
  /table, /router-status (per-router probe points).
- app.js: legs touching a transit stop are fixed (flight/taxi/bus via
  M.multimodal), never re-routed; distance-0 router answers (both
  points on one node) read as adjacent stops, not out-of-coverage.
- multi-hotel fix: day-scope map fit uses the stays covering THAT day,
  so a two-city trip no longer fits the whole country per day.

Also: pre-cache-tiles.sh (Cartagena+Santa Marta z14-16, 2560 tiles),
tile cache write fix (Node rejects flags:'x'; buffer-then-write),
favicon, renderHotelMks load-order guard, tests updated for the new
default trip (all four puppeteer suites green, zero console errors).
2026-09-09 09:44:29 -04:00
30f3dd9cbf mock: wire to the real local OSRM router + L0 trip view, version history, offline cues
- server.js: proxy /route, /table, /router-status to the local OSRM on :5000
- app.js: routeClient (polyline-5 decode, ordered route fetch); legs start as
  straight-line estimates (conf 1) and are upgraded in the background to real
  road geometry + duration (conf 3); out-of-coverage trips (Florence) fall
  back gracefully with a 'estimate — outside router coverage' source
- L3 leg editor: dragging a waypoint now genuinely re-routes through the
  router (was silently no-op — #app covered the map with pointer-events:auto,
  so no map drag ever landed; #app is now pointer-events:none, children opt in)
- data.js: second demo city (Boston 14–16 Sep, inside the router's NE-US
  extract) via ?city=boston; nudges + multimodal legs are now dataset-driven
- L0: Day/Trip scope toggle — per-day summary cards (pacing bar, stops,
  end time, price, base) with click-to-jump; map fits the whole trip
- version history: commit() snapshots days+stays on every mutation; topbar
  vN pill with undo/redo + dropdown timeline showing a diff label per version;
  Ctrl+Z / Ctrl+Shift+Z / Ctrl+Y; restore() re-enriches legs (snapshots may
  predate route enrichment)
- offline: staged 'prepare for offline' card in the trip rail (itinerary →
  POI details → vector tiles) that flips the topbar badge to '📦 offline ready'
- chat/scripts/nudges: Florence-specific strings generalized (stay name/dates,
  vibes, pacing summary) so both demo cities read naturally
2026-09-09 00:47:50 -04:00
146c7e168c mock: redesign around the planning workflow
- one unified canvas: discovery is a drawer over the map (chips: lunch /
  dinner / afternoon / hotel), the plan rail stays visible — no more
  modal focus-mode / dimmed rail
- smoother discovery: 'you might like' suggestion block in the rail
  (unfilled slots / more-like-this-day), region placeholders open the
  drawer, chat still filters an open drawer
- frictionless replacement: every planned stop card expands to inline
  alternatives with one-click swap (incumbent demotes to backup, new
  stop takes its sequence position); candidate cards are draggable and
  drop onto a same-slot stop (swap) or the plan (hold as idea)
- multi-scale comparison: persistent bottom dock lines up 2-4
  candidates/stops side by side (walk from anchor, walk to hotel,
  duration, price, tags) at both hotel and stop scale, with choose /
  idea actions
- gentler load: states read as in plan / idea / backup, issues panel
  becomes 'nudges' (one-tap fixes, 'this day looks good' when clear),
  budget phrasing ('comfortably paced' / 'running a bit full')
- new day strip: horizontal L1 overview of the whole day (stops, legs,
  buffer, return-to-hotel) with hour axis; click a block to fly
- mobile: drawer becomes a bottom sheet, dock sits above the tab bar
2026-09-08 22:54:33 -04:00
efde2cc71b maps project: design, survey, mock app, and route-aware planning backend
- DESIGN.md: full design incl. driving-trip requirements (R1-R4),
  stays model, focus mode, mobile, provenance rules
- SURVEY.md: open-source landscape
- mock/: interaction mock (Florence itinerary, focus mode, stays,
  region stops, mobile layout)
- router/: Go module (stdlib-only) with Router interface
  (Valhalla + OSRM backends), stop_cost, optimize_stops, corridor,
  routectl CLI, bench (5 real NE-corridor tasks, 26 checks passing),
  integration tests, and setup-osrm.sh for the self-hosted router
- osm/: NH+MA+CT+NY PBFs (gitignored) + setup artifacts
2026-09-06 00:05:17 -04:00