From 39c002d090d8f9828c25ac6c259e87ef77d5f17c Mon Sep 17 00:00:00 2001 From: Logan Date: Sun, 21 Jun 2026 14:08:33 -0400 Subject: [PATCH 01/18] Fix assistant --- drb-frontend/app/trips/[id]/page.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/drb-frontend/app/trips/[id]/page.tsx b/drb-frontend/app/trips/[id]/page.tsx index 975d1bc..0458436 100644 --- a/drb-frontend/app/trips/[id]/page.tsx +++ b/drb-frontend/app/trips/[id]/page.tsx @@ -10,6 +10,11 @@ import type { TripEvent, TripRecord, PlaceResult } from "@/lib/types"; // Helpers // --------------------------------------------------------------------------- +function uid(): string { + if (typeof crypto !== "undefined" && crypto.randomUUID) return uid(); + return Math.random().toString(36).slice(2) + Date.now().toString(36); +} + function toMin(t: string): number { const [h, m] = t.split(":").map(Number); return h * 60 + (m ?? 0); @@ -513,7 +518,7 @@ function AssistantPanel({ if (!text || loading) return; setInput(""); - const userMsg: ChatMessage = { id: crypto.randomUUID(), role: "user", content: text }; + const userMsg: ChatMessage = { id: uid(), role: "user", content: text }; setMessages((prev) => [...prev, userMsg]); setLoading(true); @@ -522,7 +527,7 @@ function AssistantPanel({ try { const res = await c2api.tripChat(trip.trip_id, text, history); const assistantMsg: ChatMessage = { - id: crypto.randomUUID(), + id: uid(), role: "assistant", content: res.reply, suggestions: (res.suggestions as unknown as SuggestionCard[]) ?? [], @@ -531,7 +536,7 @@ function AssistantPanel({ } catch { setMessages((prev) => [ ...prev, - { id: crypto.randomUUID(), role: "assistant", content: "Something went wrong. Try again." }, + { id: uid(), role: "assistant", content: "Something went wrong. Try again." }, ]); } finally { setLoading(false); From af4079d6482e910231670fbd9784cb8140383d68 Mon Sep 17 00:00:00 2001 From: Logan Date: Sun, 21 Jun 2026 14:15:09 -0400 Subject: [PATCH 02/18] fix build --- drb-frontend/app/trips/[id]/page.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/drb-frontend/app/trips/[id]/page.tsx b/drb-frontend/app/trips/[id]/page.tsx index 0458436..b291e4a 100644 --- a/drb-frontend/app/trips/[id]/page.tsx +++ b/drb-frontend/app/trips/[id]/page.tsx @@ -11,8 +11,7 @@ import type { TripEvent, TripRecord, PlaceResult } from "@/lib/types"; // --------------------------------------------------------------------------- function uid(): string { - if (typeof crypto !== "undefined" && crypto.randomUUID) return uid(); - return Math.random().toString(36).slice(2) + Date.now().toString(36); + try { return crypto.randomUUID(); } catch { return Math.random().toString(36).slice(2) + Date.now().toString(36); } } function toMin(t: string): number { From 522748f07a9081bc3e86d543818604cf4a00bdf0 Mon Sep 17 00:00:00 2001 From: Logan Date: Sun, 21 Jun 2026 14:31:26 -0400 Subject: [PATCH 03/18] debugging for trips assistant --- drb-c2-core/app/routers/trips.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/drb-c2-core/app/routers/trips.py b/drb-c2-core/app/routers/trips.py index 0b3e821..2e70597 100644 --- a/drb-c2-core/app/routers/trips.py +++ b/drb-c2-core/app/routers/trips.py @@ -84,6 +84,11 @@ async def _places_search(query: str, near: str) -> list[dict]: params={"query": f"{query} {near}".strip(), "key": settings.google_maps_api_key}, ) data = r.json() + status = data.get("status") + results = data.get("results", []) + logger.info(f"Places search '{query} {near}': status={status}, count={len(results)}") + if status not in ("OK", "ZERO_RESULTS"): + logger.warning(f"Places API error: {status} — {data.get('error_message', '')}") return [ { "name": p.get("name"), @@ -92,7 +97,7 @@ async def _places_search(query: str, near: str) -> list[dict]: "maps_link": f"https://www.google.com/maps/place/?q=place_id:{p.get('place_id')}", "rating": p.get("rating"), } - for p in data.get("results", [])[:5] + for p in results[:5] ] except Exception as e: logger.error(f"Places search in assistant failed: {e}") From 21268ab477afea78e8404f240b06f79754ff0195 Mon Sep 17 00:00:00 2001 From: Logan Date: Sun, 21 Jun 2026 14:35:12 -0400 Subject: [PATCH 04/18] fix: migrate Places and Routes to new GCP APIs Switch from legacy Places textsearch and Directions APIs (disabled on this project) to Places API (New) and Routes API (New). Both places.py and the assistant's _places_search helper updated. Also fixes uid() recursive self-call in trips page and adds Places API response logging. --- drb-c2-core/app/routers/places.py | 68 ++++++++++++++++++++----------- drb-c2-core/app/routers/trips.py | 34 ++++++++++------ 2 files changed, 66 insertions(+), 36 deletions(-) diff --git a/drb-c2-core/app/routers/places.py b/drb-c2-core/app/routers/places.py index 2ce71db..80797cf 100644 --- a/drb-c2-core/app/routers/places.py +++ b/drb-c2-core/app/routers/places.py @@ -5,8 +5,9 @@ from app.internal.logger import logger router = APIRouter(prefix="/places", tags=["places"]) -PLACES_SEARCH_URL = "https://maps.googleapis.com/maps/api/place/textsearch/json" -DIRECTIONS_URL = "https://maps.googleapis.com/maps/api/directions/json" +_PLACES_SEARCH_URL = "https://places.googleapis.com/v1/places:searchText" +_ROUTES_URL = "https://routes.googleapis.com/directions/v2:computeRoutes" +_PLACES_FIELDS = "places.id,places.displayName,places.formattedAddress,places.rating,places.googleMapsUri,places.location" @router.get("/search") @@ -17,9 +18,13 @@ async def search_places(query: str = Query(...), near: str = Query("")): full_query = f"{query} {near}".strip() try: async with httpx.AsyncClient(timeout=10) as client: - r = await client.get( - PLACES_SEARCH_URL, - params={"query": full_query, "key": settings.google_maps_api_key}, + r = await client.post( + _PLACES_SEARCH_URL, + json={"textQuery": full_query}, + headers={ + "X-Goog-Api-Key": settings.google_maps_api_key, + "X-Goog-FieldMask": _PLACES_FIELDS, + }, ) r.raise_for_status() data = r.json() @@ -29,15 +34,15 @@ async def search_places(query: str = Query(...), near: str = Query("")): return [ { - "name": p.get("name"), - "address": p.get("formatted_address"), - "place_id": p.get("place_id"), - "lat": p.get("geometry", {}).get("location", {}).get("lat"), - "lng": p.get("geometry", {}).get("location", {}).get("lng"), - "maps_link": f"https://www.google.com/maps/place/?q=place_id:{p.get('place_id')}", + "name": p.get("displayName", {}).get("text"), + "address": p.get("formattedAddress"), + "place_id": p.get("id"), + "lat": p.get("location", {}).get("latitude"), + "lng": p.get("location", {}).get("longitude"), + "maps_link": p.get("googleMapsUri"), "rating": p.get("rating"), } - for p in data.get("results", [])[:6] + for p in data.get("places", [])[:6] ] @@ -51,13 +56,16 @@ async def get_directions( try: async with httpx.AsyncClient(timeout=10) as client: - r = await client.get( - DIRECTIONS_URL, - params={ - "origin": origin, - "destination": destination, - "mode": "driving", - "key": settings.google_maps_api_key, + r = await client.post( + _ROUTES_URL, + json={ + "origin": {"address": origin}, + "destination": {"address": destination}, + "travelMode": "DRIVE", + }, + headers={ + "X-Goog-Api-Key": settings.google_maps_api_key, + "X-Goog-FieldMask": "routes.duration,routes.distanceMeters", }, ) r.raise_for_status() @@ -70,9 +78,23 @@ async def get_directions( if not routes: return {"duration_text": None, "duration_seconds": None, "distance_text": None} - leg = routes[0]["legs"][0] + route = routes[0] + duration_seconds = int(route.get("duration", "0s").rstrip("s") or 0) + distance_m = route.get("distanceMeters", 0) + + # Format human-readable strings + hours, rem = divmod(duration_seconds, 3600) + mins = rem // 60 + if hours: + duration_text = f"{hours} hr {mins} min" if mins else f"{hours} hr" + else: + duration_text = f"{mins} min" + + miles = distance_m / 1609.34 + distance_text = f"{miles:.1f} mi" + return { - "duration_text": leg["duration"]["text"], - "duration_seconds": leg["duration"]["value"], - "distance_text": leg["distance"]["text"], + "duration_text": duration_text, + "duration_seconds": duration_seconds, + "distance_text": distance_text, } diff --git a/drb-c2-core/app/routers/trips.py b/drb-c2-core/app/routers/trips.py index 2e70597..bd5b430 100644 --- a/drb-c2-core/app/routers/trips.py +++ b/drb-c2-core/app/routers/trips.py @@ -74,30 +74,38 @@ _TOOLS = [ ] +_PLACES_SEARCH_URL = "https://places.googleapis.com/v1/places:searchText" +_PLACES_FIELDS = "places.id,places.displayName,places.formattedAddress,places.rating,places.googleMapsUri" + + async def _places_search(query: str, near: str) -> list[dict]: if not settings.google_maps_api_key: return [] + full_query = f"{query} {near}".strip() try: async with httpx.AsyncClient(timeout=8) as client: - r = await client.get( - "https://maps.googleapis.com/maps/api/place/textsearch/json", - params={"query": f"{query} {near}".strip(), "key": settings.google_maps_api_key}, + r = await client.post( + _PLACES_SEARCH_URL, + json={"textQuery": full_query}, + headers={ + "X-Goog-Api-Key": settings.google_maps_api_key, + "X-Goog-FieldMask": _PLACES_FIELDS, + }, ) data = r.json() - status = data.get("status") - results = data.get("results", []) - logger.info(f"Places search '{query} {near}': status={status}, count={len(results)}") - if status not in ("OK", "ZERO_RESULTS"): - logger.warning(f"Places API error: {status} — {data.get('error_message', '')}") + places = data.get("places", []) + logger.info(f"Places search '{full_query}': count={len(places)}") + if not places and "error" in data: + logger.warning(f"Places API error: {data['error'].get('message', '')}") return [ { - "name": p.get("name"), - "address": p.get("formatted_address"), - "place_id": p.get("place_id"), - "maps_link": f"https://www.google.com/maps/place/?q=place_id:{p.get('place_id')}", + "name": p.get("displayName", {}).get("text"), + "address": p.get("formattedAddress"), + "place_id": p.get("id"), + "maps_link": p.get("googleMapsUri"), "rating": p.get("rating"), } - for p in results[:5] + for p in places[:5] ] except Exception as e: logger.error(f"Places search in assistant failed: {e}") From 21d15d0426c768e6cb14df07fd5dc81812dfe2a5 Mon Sep 17 00:00:00 2001 From: Logan Date: Sun, 21 Jun 2026 14:38:53 -0400 Subject: [PATCH 05/18] assistant markdown update --- drb-c2-core/app/routers/trips.py | 1 + drb-frontend/app/trips/[id]/page.tsx | 20 +++++++++++++++++++- drb-frontend/package.json | 3 ++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/drb-c2-core/app/routers/trips.py b/drb-c2-core/app/routers/trips.py index bd5b430..ef9d769 100644 --- a/drb-c2-core/app/routers/trips.py +++ b/drb-c2-core/app/routers/trips.py @@ -145,6 +145,7 @@ Current itinerary:{itinerary} Guidelines: - Be conversational and concise — don't over-explain. +- Format all responses using Markdown: use **bold** for place names and key details, bullet lists for options, and [links](url) for Maps links. - When the user mentions places, activities, or asks for suggestions, search for them with search_places before proposing. - Use propose_event for each concrete suggestion — one call per event. The user will approve or skip each one. - Be mindful of the existing schedule when assigning times. Avoid obvious conflicts. diff --git a/drb-frontend/app/trips/[id]/page.tsx b/drb-frontend/app/trips/[id]/page.tsx index b291e4a..bf1d514 100644 --- a/drb-frontend/app/trips/[id]/page.tsx +++ b/drb-frontend/app/trips/[id]/page.tsx @@ -1,6 +1,7 @@ "use client"; import { useCallback, useEffect, useRef, useState } from "react"; +import ReactMarkdown from "react-markdown"; import { useParams, useRouter } from "next/navigation"; import { useAuth } from "@/components/AuthProvider"; import { c2api } from "@/lib/c2api"; @@ -614,7 +615,24 @@ function AssistantPanel({ : "bg-gray-800 text-gray-200" }`} > - {msg.content} + {msg.role === "user" ? msg.content : ( +

{children}

, + ul: ({ children }) =>
    {children}
, + ol: ({ children }) =>
    {children}
, + li: ({ children }) =>
  • {children}
  • , + strong: ({ children }) => {children}, + a: ({ href, children }) => ( + + {children} + + ), + }} + > + {msg.content} +
    + )} {/* Suggestion cards */} diff --git a/drb-frontend/package.json b/drb-frontend/package.json index 24bdca5..546632c 100644 --- a/drb-frontend/package.json +++ b/drb-frontend/package.json @@ -14,7 +14,8 @@ "react-dom": "^18.3.0", "firebase": "^10.12.0", "leaflet": "^1.9.4", -"react-leaflet": "^4.2.1" +"react-leaflet": "^4.2.1", + "react-markdown": "^9.0.1" }, "devDependencies": { "typescript": "^5.4.0", From e7622c7e6df7329d7b0817d07bcc6941b2a9dcc7 Mon Sep 17 00:00:00 2001 From: Logan Date: Sun, 21 Jun 2026 14:47:17 -0400 Subject: [PATCH 06/18] chat box fixes --- drb-frontend/app/trips/[id]/page.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/drb-frontend/app/trips/[id]/page.tsx b/drb-frontend/app/trips/[id]/page.tsx index bf1d514..fc754f5 100644 --- a/drb-frontend/app/trips/[id]/page.tsx +++ b/drb-frontend/app/trips/[id]/page.tsx @@ -702,13 +702,14 @@ function AssistantPanel({ {/* Input */}
    - setInput(e.target.value)} + onChange={(e) => { setInput(e.target.value); e.target.style.height = "auto"; e.target.style.height = `${Math.min(e.target.scrollHeight, 120)}px`; }} onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && (e.preventDefault(), send())} placeholder="What do you want to do?" disabled={loading} - className="flex-1 bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-indigo-500 disabled:opacity-50" + rows={1} + className="flex-1 bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-indigo-500 disabled:opacity-50 resize-none overflow-hidden" />