From 8cf2ef148c58ee1899de01073f215c0871829d94 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 10:03:22 +0000 Subject: [PATCH 1/2] feat: restore and persist durable geospatial drawing context - Restored drawing and geospatial context to resolution carousel re-trigger in `components/resolution-carousel.tsx`. - Implemented durable drawing context in `app/actions.tsx` by capturing `drawing_context` messages in AI state and merging features across turns. - Seeded `MapDataProvider` from persisted state in `app/search/[id]/page.tsx` for hydrated chat reloads. - Resolved nested provider shadowing in `components/chat.tsx` to ensure consistent map state resolution. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- app/actions.tsx | 38 +++++++++++++++++++++++++++-- app/search/[id]/page.tsx | 24 +++++++++++++++--- components/chat.tsx | 10 ++++---- components/map/map-data-context.tsx | 4 +-- components/resolution-carousel.tsx | 10 ++++++++ 5 files changed, 73 insertions(+), 13 deletions(-) diff --git a/app/actions.tsx b/app/actions.tsx index 8b693603..19f96336 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -50,6 +50,38 @@ async function submit(formData?: FormData, skip?: boolean) { console.error('Failed to parse drawnFeatures:', e); } + // PERSISTENCE: Always append drawing_context for durable feature history + if (drawnFeatures.length > 0) { + aiState.update({ + ...aiState.get(), + messages: [ + ...aiState.get().messages, + { + id: nanoid(), + role: 'data', + content: JSON.stringify(drawnFeatures), + type: 'drawing_context' + } + ] + }); + } + + // PERSISTENCE: Build merged drawing set from all historical drawing_context messages + const mergedDrawnFeatures = [...drawnFeatures]; + const historicalDrawingContexts = aiState.get().messages.filter(m => m.type === 'drawing_context'); + historicalDrawingContexts.forEach(m => { + try { + const historicalFeatures = JSON.parse(m.content as string) as DrawnFeature[]; + historicalFeatures.forEach(hf => { + if (!mergedDrawnFeatures.some(f => f.id === hf.id)) { + mergedDrawnFeatures.push(hf); + } + }); + } catch (e) { + console.error('Failed to parse historical drawing context:', e); + } + }); + if (action === 'generate_report_context') { const messagesString = formData?.get('messages'); if (typeof messagesString !== 'string') { @@ -92,6 +124,7 @@ async function submit(formData?: FormData, skip?: boolean) { message.type !== 'followup' && message.type !== 'related' && message.type !== 'end' && + message.type !== 'drawing_context' && message.type !== 'resolution_search_result' ); @@ -115,7 +148,7 @@ async function submit(formData?: FormData, skip?: boolean) { async function processResolutionSearch() { try { - const streamResult = await resolutionSearch(messages, timezone, drawnFeatures, location); + const streamResult = await resolutionSearch(messages, timezone, mergedDrawnFeatures, location); let fullSummary = ''; for await (const partialObject of streamResult.partialObjectStream) { @@ -357,6 +390,7 @@ async function submit(formData?: FormData, skip?: boolean) { message.type !== 'followup' && message.type !== 'related' && message.type !== 'end' && + message.type !== 'drawing_context' && message.type !== 'resolution_search_result' ) .map((m: any) => { @@ -539,7 +573,7 @@ async function submit(formData?: FormData, skip?: boolean) { messages, mapProvider, useSpecificAPI, - drawnFeatures + mergedDrawnFeatures ) answer = fullResponse toolOutputs = toolResponses diff --git a/app/search/[id]/page.tsx b/app/search/[id]/page.tsx index 8db74186..ff8a2bd1 100644 --- a/app/search/[id]/page.tsx +++ b/app/search/[id]/page.tsx @@ -59,16 +59,32 @@ export default async function SearchPage({ params }: SearchPageProps) { }; }); + // PERSISTENCE: Seed map context from latest persisted drawing data + const latestDrawingContext = [...dbMessages] + .reverse() + .find(m => m.role === 'data'); + + let initialMapData = undefined; + if (latestDrawingContext) { + try { + const parsed = JSON.parse(latestDrawingContext.content as string); + initialMapData = { + drawnFeatures: parsed.drawnFeatures || [], + cameraState: parsed.cameraState, + }; + } catch (e) { + console.error('Failed to parse latest drawing context:', e); + } + } + return ( - + diff --git a/components/chat.tsx b/components/chat.tsx index d3560a77..3c05a4e2 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -17,7 +17,7 @@ import { useProfileToggle, ProfileToggleEnum } from "@/components/profile-toggle import { useUsageToggle } from "@/components/usage-toggle-context"; import SettingsView from "@/components/settings/settings-view"; import { UsageView } from "@/components/usage-view"; -import { MapDataProvider, useMapData } from './map/map-data-context'; // Add this and useMapData +import { useMapData } from './map/map-data-context'; // Add this and useMapData import { updateDrawingContext } from '@/lib/actions/chat'; // Import the server action import dynamic from 'next/dynamic' import { HeaderSearchButton } from './header-search-button' @@ -137,7 +137,7 @@ export function Chat({ id }: ChatProps) { // Mobile layout if (isMobile) { return ( - {/* Add Provider */} + <>
@@ -177,13 +177,13 @@ export function Chat({ id }: ChatProps) { )}
-
+ ); } // Desktop layout return ( - {/* Add Provider */} + <>
{/* This is the new div for scrolling */} @@ -223,6 +223,6 @@ export function Chat({ id }: ChatProps) { {activeView ? : isUsageOpen ? : }
-
+ ); } diff --git a/components/map/map-data-context.tsx b/components/map/map-data-context.tsx index 9b102547..ea5110cc 100644 --- a/components/map/map-data-context.tsx +++ b/components/map/map-data-context.tsx @@ -38,8 +38,8 @@ interface MapDataContextType { const MapDataContext = createContext(undefined); -export const MapDataProvider: React.FC<{ children: ReactNode }> = ({ children }) => { - const [mapData, setMapData] = useState({ drawnFeatures: [], markers: [] }); +export const MapDataProvider: React.FC<{ children: ReactNode; initialData?: MapData }> = ({ children, initialData }) => { + const [mapData, setMapData] = useState(initialData || { drawnFeatures: [], markers: [] }); return ( diff --git a/components/resolution-carousel.tsx b/components/resolution-carousel.tsx index 1b28cbf7..9aa51cd3 100644 --- a/components/resolution-carousel.tsx +++ b/components/resolution-carousel.tsx @@ -17,6 +17,7 @@ import { UserMessage } from './user-message' import { toast } from 'sonner' import { CompareSlider } from './compare-slider' import { compressImage } from '@/lib/utils/image-utils' +import { useMapData } from './map/map-data-context' interface ResolutionCarouselProps { mapboxImage?: string | null @@ -28,6 +29,7 @@ export function ResolutionCarousel({ mapboxImage, googleImage, initialImage }: R const actions = useActions() as any const [, setMessages] = useUIState() const [isAnalyzing, setIsAnalyzing] = React.useState(false) + const { mapData } = useMapData() const handleQCXAnalysis = async () => { if (!googleImage) return @@ -52,6 +54,14 @@ export function ResolutionCarousel({ mapboxImage, googleImage, initialImage }: R const formData = new FormData() formData.append('file', blob, 'google_analysis.png') formData.append('action', 'resolution_search') + formData.append('timezone', mapData.currentTimezone || 'UTC') + formData.append('drawnFeatures', JSON.stringify(mapData.drawnFeatures || [])) + + const center = mapData.cameraState?.center || mapData.targetPosition; + if (center) { + formData.append('latitude', center.lat.toString()) + formData.append('longitude', center.lng.toString()) + } const responseMessage = await actions.submit(formData) setMessages((currentMessages: any[]) => [...currentMessages, responseMessage as any]) From 9d42b543603270ee4adceeb4a7bd9fe3a25ab1f3 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 26 Jun 2026 11:16:00 +0000 Subject: [PATCH 2/2] feat: restore and persist durable geospatial drawing context and fix DB/Auth issues - Restored drawing and geospatial context to resolution carousel re-trigger in `components/resolution-carousel.tsx`. - Implemented durable drawing context in `app/actions.tsx` by capturing `drawing_context` messages in AI state and merging features across turns. - Seeded `MapDataProvider` from persisted state in `app/search/[id]/page.tsx` for hydrated chat reloads. - Resolved nested provider shadowing in `components/chat.tsx` to ensure consistent map state resolution. - Fixed `invalid input syntax for type uuid` by validating against 'anonymous' userId in `lib/actions/chat.ts` and `lib/actions/chat-db.ts`. - Fixed `TypeError: Cannot read properties of undefined (reading 'searchParams')` by correctly awaiting `cookies()` in `lib/auth/get-current-user.ts`. - Optimized agent imports in `app/actions.tsx` to avoid circular dependencies. - Ensured `sanitizedHistory` (with image placeholders) is saved to the database. Co-authored-by: ngoiyaeric <115367894+ngoiyaeric@users.noreply.github.com> --- app/actions.tsx | 19 ++++++++++++++----- app/search/[id]/page.tsx | 13 ++++++++----- lib/actions/chat-db.ts | 4 ++-- lib/actions/chat.ts | 8 ++++---- lib/auth/get-current-user.ts | 8 ++++---- 5 files changed, 32 insertions(+), 20 deletions(-) diff --git a/app/actions.tsx b/app/actions.tsx index 19f96336..90ebaae3 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -12,9 +12,14 @@ import type { FeatureCollection } from 'geojson' import { Spinner } from '@/components/ui/spinner' import { Section } from '@/components/section' import { FollowupPanel } from '@/components/followup-panel' -import { inquire, researcher, taskManager, querySuggestor, resolutionSearch, type DrawnFeature } from '@/lib/agents' +import { taskManager } from '@/lib/agents/task-manager' +import { inquire } from '@/lib/agents/inquire' +import { querySuggestor } from '@/lib/agents/query-suggestor' +import { researcher } from '@/lib/agents/researcher' +import { resolutionSearch, type DrawnFeature } from '@/lib/agents/resolution-search' import { writer } from '@/lib/agents/writer' import { saveChat, getSystemPrompt, generateReportContext } from '@/lib/actions/chat' + import { Chat, AIMessage } from '@/lib/types' import { UserMessage } from '@/components/user-message' import { BotMessage } from '@/components/message' @@ -66,7 +71,7 @@ async function submit(formData?: FormData, skip?: boolean) { }); } - // PERSISTENCE: Build merged drawing set from all historical drawing_context messages + // PERSISTENCE: build merged drawing set from all historical drawing_context messages const mergedDrawnFeatures = [...drawnFeatures]; const historicalDrawingContexts = aiState.get().messages.filter(m => m.type === 'drawing_context'); historicalDrawingContexts.forEach(m => { @@ -82,6 +87,11 @@ async function submit(formData?: FormData, skip?: boolean) { } }); + const { getCurrentUserIdOnServer } = await import( + "@/lib/auth/get-current-user" + ) + const userId = (await getCurrentUserIdOnServer()) || "anonymous"; + if (action === 'generate_report_context') { const messagesString = formData?.get('messages'); if (typeof messagesString !== 'string') { @@ -225,7 +235,7 @@ async function submit(formData?: FormData, skip?: boolean) { aiState.done({ ...aiState.get(), messages: [ - ...aiState.get().messages, + ...sanitizedHistory, { id: groupeId, role: 'assistant', @@ -522,7 +532,6 @@ async function submit(formData?: FormData, skip?: boolean) { } as CoreMessage) } - const userId = 'anonymous' const currentSystemPrompt = (await getSystemPrompt(userId)) || '' const mapProvider = formData?.get('mapProvider') as 'mapbox' | 'google' @@ -763,7 +772,7 @@ export const AI = createAI({ ] const { getCurrentUserIdOnServer } = await import( - '@/lib/auth/get-current-user' + "@/lib/auth/get-current-user" ) const actualUserId = await getCurrentUserIdOnServer() diff --git a/app/search/[id]/page.tsx b/app/search/[id]/page.tsx index ff8a2bd1..5276c5af 100644 --- a/app/search/[id]/page.tsx +++ b/app/search/[id]/page.tsx @@ -14,11 +14,14 @@ export interface SearchPageProps { } export async function generateMetadata({ params }: SearchPageProps) { - const { id } = await params; // Keep as is for now - // TODO: Metadata generation might need authenticated user if chats are private - // For now, assuming getChat can be called or it handles anon access for metadata appropriately - const userId = await getCurrentUserIdOnServer(); // Attempt to get user for metadata - const chat = await getChat(id, userId || 'anonymous'); // Pass userId or 'anonymous' if none + const { id } = await params; + const userId = await getCurrentUserIdOnServer(); + + if (!userId) { + return { title: 'Search' }; + } + + const chat = await getChat(id, userId); return { title: chat?.title?.toString().slice(0, 50) || 'Search', }; diff --git a/lib/actions/chat-db.ts b/lib/actions/chat-db.ts index e15ea4ad..c0af3fa1 100644 --- a/lib/actions/chat-db.ts +++ b/lib/actions/chat-db.ts @@ -20,7 +20,7 @@ export type NewMessage = typeof messages.$inferInsert; * @returns The chat object if found and accessible, otherwise null. */ export async function getChat(id: string, userId: string): Promise { - if (!userId) { + if (!userId || userId === 'anonymous') { console.warn('getChat called without userId'); // Potentially allow fetching public chats if userId is null for anonymous users const result = await db.select().from(chats).where(and(eq(chats.id, id), eq(chats.visibility, 'public'))).limit(1); @@ -51,7 +51,7 @@ export async function getChatsPage( limit: number = 20, offset: number = 0 ): Promise<{ chats: Chat[]; nextOffset: number | null }> { - if (!userId) { + if (!userId || userId === 'anonymous') { console.error('getChatsPage called without userId.'); return { chats: [], nextOffset: null }; } diff --git a/lib/actions/chat.ts b/lib/actions/chat.ts index c8263ebe..7c4b0d41 100644 --- a/lib/actions/chat.ts +++ b/lib/actions/chat.ts @@ -99,8 +99,8 @@ export async function getChats(userId?: string | null): Promise { } export async function getChat(id: string, userId: string): Promise { - if (!userId) { - console.warn('getChat called without userId.') + if (!userId || userId === 'anonymous') { + console.warn('getChat: Invalid or missing userId.'); return null; } try { @@ -185,8 +185,8 @@ export async function saveChat(chat: OldChatType, userId: string): Promise { - const cookie = (await cookieStore).get(name); // Use the correct get method + const cookie = cookieStore.get(name); // Use the correct get method return cookie?.value; // Return the value or undefined }, async set(name: string, value: string, options: CookieOptions): Promise { try { - const store = await cookieStore; + const store = cookieStore; store.set({ name, value, ...options }); // Set cookie with options } catch (error) { // console.warn(`[Auth] Failed to set cookie ${name}:`, error); @@ -75,7 +75,7 @@ export async function getSupabaseUserAndSessionOnServer(): Promise<{ }, async remove(name: string, options: CookieOptions): Promise { try { - const store = await cookieStore; + const store = cookieStore; store.set({ name, value: '', ...options, maxAge: 0 }); // Delete cookie by setting maxAge to 0 } catch (error) { // console.warn(`[Auth] Failed to delete cookie ${name}:`, error);