Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,8 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => {

// Check if this is our map query trigger
if (toolOutput.type === "MAP_QUERY_TRIGGER" && name === "geospatialQueryTool") {
console.log('[getUIStateFromAIState] Matched geospatialQueryTool, rendering MapQueryHandler.');
console.log('[getUIStateFromAIState] toolOutput:', JSON.stringify(toolOutput, null, 2));
// The MapQueryHandler now expects the entire toolOutput object
return {
id, // message id
Expand Down
22 changes: 11 additions & 11 deletions app/search/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,17 @@ export default async function SearchPage({ params }: SearchPageProps) {
});

return (
<AI
initialAIState={{
chatId: chat.id,
messages: initialMessages, // Use the transformed messages from the database
// isSharePage: true, // This was in PR#533, but share functionality is removed.
// If needed for styling or other logic, it can be set.
}}
>
<MapDataProvider>
<MapDataProvider>
<AI
initialAIState={{
chatId: chat.id,
messages: initialMessages, // Use the transformed messages from the database
// isSharePage: true, // This was in PR#533, but share functionality is removed.
// If needed for styling or other logic, it can be set.
}}
>
<Chat id={id} />
</MapDataProvider>
</AI>
</AI>
</MapDataProvider>
);
Comment on lines +63 to 75
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Verify and align provider order across routes (MapDataProvider ↔ AI) to avoid inconsistent context lifecycles

Here, MapDataProvider wraps AI. In app/page.tsx (home), AI wraps MapDataProvider. Different provider hierarchies can change when mapData resets across page transitions (e.g., navigating between home and search), potentially leading to surprising state persistence/reset behavior. If this is intentional, ignore; otherwise, consider standardizing the order across routes.

Run this script to list occurrences and surrounding context so you can quickly verify composition order:


🏁 Script executed:

#!/bin/bash
# Show occurrences of AI, MapDataProvider, and Chat together to inspect hierarchy
rg -n -C3 -P '(?s)<AI\b|<MapDataProvider\b|<Chat\b' app/**/*.tsx

Length of output: 121


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "Searching repository for AI, MapDataProvider, Chat usages..."
rg -n -S -C3 -P '(?s)<(AI|MapDataProvider|Chat)\b' || true
echo
echo "Listing any page.tsx files found..."
find . -type f -name 'page.tsx' -print || true

Length of output: 2064


Align provider order (MapDataProvider ↔ AI) across routes

Verified: app/page.tsx uses AI > MapDataProvider, while app/search/[id]/page.tsx uses MapDataProvider > AI. This mismatch can change context lifecycles and cause surprising state resets across navigation. Standardize the order.

Files to update:

  • app/page.tsx — AI wraps MapDataProvider (current; lines ~12–16)
  • app/search/[id]/page.tsx — MapDataProvider wraps AI (current; lines ~63–74) — change to match chosen order

Suggested change (make app/search/[id]/page.tsx match app/page.tsx by moving AI outermost):

Before
< MapDataProvider>



After
< AI initialAIState={...}>



🤖 Prompt for AI Agents
In app/search/[id]/page.tsx around lines 63 to 75, the provider order is
MapDataProvider wrapping AI which differs from app/page.tsx and can cause
context lifecycle mismatches; change the nesting so AI is the outer provider and
MapDataProvider is inside it, preserving the existing initialAIState prop on AI
and passing Chat id as before (i.e., wrap MapDataProvider and Chat with AI as
the parent) so the component tree matches app/page.tsx.

}
74 changes: 35 additions & 39 deletions components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useUIState, useAIState } from 'ai/rsc'
import MobileIconsBar from './mobile-icons-bar'
import { useProfileToggle, ProfileToggleEnum } from "@/components/profile-toggle-context";
import SettingsView from "@/components/settings/settings-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

type ChatProps = {
Expand Down Expand Up @@ -74,50 +74,46 @@ export function Chat({ id }: ChatProps) {
// Mobile layout
if (isMobile) {
return (
<MapDataProvider> {/* Add Provider */}
<div className="mobile-layout-container">
<div className="mobile-map-section">
{activeView ? <SettingsView /> : <Mapbox />}
</div>
<div className="mobile-icons-bar">
<MobileIconsBar />
</div>
<div className="mobile-chat-input-area">
<ChatPanel messages={messages} input={input} setInput={setInput} />
</div>
<div className="mobile-chat-messages-area">
{showEmptyScreen ? (
<EmptyScreen
submitMessage={message => {
setInput(message)
}}
/>
) : (
<ChatMessages messages={messages} />
)}
</div>
<div className="mobile-layout-container">
<div className="mobile-map-section">
{activeView ? <SettingsView /> : <Mapbox />}
</div>
<div className="mobile-icons-bar">
<MobileIconsBar />
</div>
<div className="mobile-chat-input-area">
<ChatPanel messages={messages} input={input} setInput={setInput} />
</div>
<div className="mobile-chat-messages-area">
{showEmptyScreen ? (
<EmptyScreen
submitMessage={message => {
setInput(message)
}}
/>
) : (
<ChatMessages messages={messages} />
)}
</div>
</MapDataProvider>
</div>
);
}

// Desktop layout
return (
<MapDataProvider> {/* Add Provider */}
<div className="flex justify-start items-start">
{/* This is the new div for scrolling */}
<div className="w-1/2 flex flex-col space-y-3 md:space-y-4 px-8 sm:px-12 pt-12 md:pt-14 pb-4 h-[calc(100vh-0.5in)] overflow-y-auto">
{/* TODO: Add EmptyScreen for desktop if needed */}
<ChatMessages messages={messages} />
<ChatPanel messages={messages} input={input} setInput={setInput} />
</div>
<div
className="w-1/2 p-4 fixed h-[calc(100vh-0.5in)] top-0 right-0 mt-[0.5in]"
style={{ zIndex: 10 }} // Added z-index
>
{activeView ? <SettingsView /> : <Mapbox />}
</div>
<div className="flex justify-start items-start">
{/* This is the new div for scrolling */}
<div className="w-1/2 flex flex-col space-y-3 md:space-y-4 px-8 sm:px-12 pt-12 md:pt-14 pb-4 h-[calc(100vh-0.5in)] overflow-y-auto">
{/* TODO: Add EmptyScreen for desktop if needed */}
<ChatMessages messages={messages} />
<ChatPanel messages={messages} input={input} setInput={setInput} />
</div>
<div
className="w-1/2 p-4 fixed h-[calc(100vh-0.5in)] top-0 right-0 mt-[0.5in]"
style={{ zIndex: 10 }} // Added z-index
>
{activeView ? <SettingsView /> : <Mapbox />}
</div>
</MapDataProvider>
</div>
);
}
41 changes: 17 additions & 24 deletions components/map/map-query-handler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,46 +31,39 @@ export const MapQueryHandler: React.FC<MapQueryHandlerProps> = ({ toolOutput })
const { setMapData } = useMapData();

useEffect(() => {
console.log('[MapQueryHandler] useEffect triggered. toolOutput:', JSON.stringify(toolOutput, null, 2));

if (toolOutput && toolOutput.mcp_response && toolOutput.mcp_response.location) {
const { latitude, longitude, place_name } = toolOutput.mcp_response.location;

if (typeof latitude === 'number' && typeof longitude === 'number') {
console.log(`MapQueryHandler: Received data from geospatialTool. Place: ${place_name}, Lat: ${latitude}, Lng: ${longitude}`);
const newMapData = {
targetPosition: [longitude, latitude] as [number, number],
mapFeature: {
place_name,
mapUrl: toolOutput.mcp_response?.mapUrl,
},
};
console.log('[MapQueryHandler] Calling setMapData with:', JSON.stringify(newMapData, null, 2));
setMapData(prevData => ({
...prevData,
// Ensure coordinates are in [lng, lat] format for MapboxGL
targetPosition: [longitude, latitude],
// Optionally store more info from mcp_response if needed by MapboxMap component later
mapFeature: {
place_name,
// Potentially add mapUrl or other details from toolOutput.mcp_response
mapUrl: toolOutput.mcp_response?.mapUrl
}
...newMapData,
}));
} else {
console.warn("MapQueryHandler: Invalid latitude/longitude in toolOutput.mcp_response:", toolOutput.mcp_response.location);
// Clear target position if data is invalid
console.warn('[MapQueryHandler] Invalid latitude/longitude in toolOutput.mcp_response:', toolOutput.mcp_response.location);
setMapData(prevData => ({
...prevData,
targetPosition: null,
mapFeature: null
mapFeature: null,
}));
}
} else {
// This case handles when toolOutput or its critical parts are missing.
// Depending on requirements, could fall back to originalUserInput and useMCPMapClient,
// or simply log that no valid data was provided from the tool.
// For this subtask, we primarily focus on using the new toolOutput.
if (toolOutput) { // It exists, but data is not as expected
console.warn("MapQueryHandler: toolOutput provided, but mcp_response or location data is missing.", toolOutput);
if (toolOutput) {
console.warn('[MapQueryHandler] toolOutput provided, but mcp_response or location data is missing.', toolOutput);
} else {
console.log('[MapQueryHandler] toolOutput is null or undefined.');
}
// If toolOutput is null/undefined, this component might not need to do anything,
// or it's an indication that it shouldn't have been rendered/triggered.
// For now, if no valid toolOutput, we clear map data or leave it as is.
// setMapData(prevData => ({ ...prevData, targetPosition: null, mapFeature: null }));
}
// The dependencies for this useEffect should be based on the props that trigger its logic.
// If originalUserInput and the old MCP client were still used as a fallback, they'd be dependencies.
}, [toolOutput, setMapData]);

// This component is a handler and does not render any visible UI itself.
Expand Down
24 changes: 9 additions & 15 deletions components/map/mapbox-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,17 +219,16 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number
})
})
setTimeout(() => {
if (mapType === MapToggleEnum.RealTimeMode) {
startRotation()
}
startRotation()
isUpdatingPositionRef.current = false
}, 500)
} catch (error) {
console.error('Error updating map position:', error)
} finally {
isUpdatingPositionRef.current = false
}
}
}, [mapType, startRotation, stopRotation])
}, [startRotation, stopRotation])

// Set up drawing tools
const setupDrawingTools = useCallback(() => {
Expand Down Expand Up @@ -506,23 +505,18 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number

// Effect to handle map updates from MapDataContext
useEffect(() => {
console.log('[Mapbox] useEffect for mapData triggered. mapData:', JSON.stringify(mapData, null, 2));
if (mapData.targetPosition && map.current) {
// console.log("Mapbox.tsx: Received new targetPosition from context:", mapData.targetPosition);
// targetPosition is LngLatLike, which can be [number, number]
// updateMapPosition expects (latitude, longitude)
const [lng, lat] = mapData.targetPosition as [number, number]; // Assuming LngLatLike is [lng, lat]
stopRotation();
const [lng, lat] = mapData.targetPosition as [number, number];
if (typeof lat === 'number' && typeof lng === 'number') {
console.log(`[Mapbox] Calling updateMapPosition with lat: ${lat}, lng: ${lng}`);
updateMapPosition(lat, lng);
} else {
// console.error("Mapbox.tsx: Invalid targetPosition format in mapData", mapData.targetPosition);
console.error("[Mapbox] Invalid targetPosition format in mapData", mapData.targetPosition);
}
}
// TODO: Handle mapData.mapFeature for drawing routes, polygons, etc. in a future step.
// For example:
// if (mapData.mapFeature && mapData.mapFeature.route_geometry && typeof drawRoute === 'function') {
// drawRoute(mapData.mapFeature.route_geometry); // Implement drawRoute function if needed
// }
}, [mapData.targetPosition, mapData.mapFeature, updateMapPosition]);
}, [mapData, updateMapPosition, stopRotation]);

// Long-press handlers
const handleMouseDown = useCallback(() => {
Expand Down
37 changes: 23 additions & 14 deletions lib/agents/tools/geospatial.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,9 @@ export const geospatialTool = ({ uiStream }: { uiStream: ReturnType<typeof creat
- Geographic information lookup`,
parameters: geospatialQuerySchema,
execute: async (params: z.infer<typeof geospatialQuerySchema>) => {
console.log('[GeospatialTool] Execute called with:', JSON.stringify(params, null, 2));

const { queryType, includeMap = true } = params;
console.log('[GeospatialTool] Execute called with:', params);

const uiFeedbackStream = createStreamableValue<string>();
uiStream.append(<BotMessage content={uiFeedbackStream.value} />);
Expand All @@ -162,11 +163,14 @@ export const geospatialTool = ({ uiStream }: { uiStream: ReturnType<typeof creat

const mcpClient = await getConnectedMcpClient();
if (!mcpClient) {
feedbackMessage = 'Geospatial functionality is unavailable. Please check configuration.';
uiFeedbackStream.update(feedbackMessage);
const errorMsg = 'Geospatial functionality is unavailable. Please check configuration.';
console.error(`[GeospatialTool] ${errorMsg}`);
uiFeedbackStream.update(errorMsg);
uiFeedbackStream.done();
uiStream.update(<BotMessage content={uiFeedbackStream.value} />);
return { type: 'MAP_QUERY_TRIGGER', originalUserInput: JSON.stringify(params), timestamp: new Date().toISOString(), mcp_response: null, error: 'MCP client initialization failed' };
const result = { type: 'MAP_QUERY_TRIGGER', originalUserInput: JSON.stringify(params), timestamp: new Date().toISOString(), mcp_response: null, error: 'MCP client initialization failed' };
console.log('[GeospatialTool] Returning error:', JSON.stringify(result, null, 2));
return result;
}

let mcpData: McpResponse | null = null;
Expand All @@ -176,7 +180,6 @@ export const geospatialTool = ({ uiStream }: { uiStream: ReturnType<typeof creat
feedbackMessage = `Connected to mapping service. Processing ${queryType} query...`;
uiFeedbackStream.update(feedbackMessage);

// Pick appropriate tool
const toolName = await (async () => {
const { tools } = await mcpClient.listTools().catch(() => ({ tools: [] }));
const names = new Set(tools?.map((t: any) => t.name) || []);
Expand All @@ -191,7 +194,6 @@ export const geospatialTool = ({ uiStream }: { uiStream: ReturnType<typeof creat
}
})();

// Build arguments
const toolArgs = (() => {
switch (queryType) {
case 'directions':
Expand All @@ -203,9 +205,8 @@ export const geospatialTool = ({ uiStream }: { uiStream: ReturnType<typeof creat
}
})();

console.log('[GeospatialTool] Calling tool:', toolName, 'with args:', toolArgs);
console.log('[GeospatialTool] Calling tool:', toolName, 'with args:', JSON.stringify(toolArgs, null, 2));

// Retry logic
const MAX_RETRIES = 3;
let retryCount = 0;
let toolCallResult;
Expand All @@ -224,7 +225,8 @@ export const geospatialTool = ({ uiStream }: { uiStream: ReturnType<typeof creat
}
}

// Extract & parse content
console.log('[GeospatialTool] Raw MCP Response:', JSON.stringify(toolCallResult, null, 2));

const serviceResponse = toolCallResult as { content?: Array<{ text?: string | null } | { [k: string]: any }> };
const blocks = serviceResponse?.content || [];
const textBlocks = blocks.map(b => (typeof b.text === 'string' ? b.text : null)).filter((t): t is string => !!t && t.trim().length > 0);
Expand All @@ -235,10 +237,13 @@ export const geospatialTool = ({ uiStream }: { uiStream: ReturnType<typeof creat
const match = content.match(jsonRegex);
if (match) content = match[1].trim();

try { content = JSON.parse(content); }
catch { console.warn('[GeospatialTool] Content is not JSON, using as string:', content); }
try {
content = JSON.parse(content);
console.log('[GeospatialTool] Parsed MCP Response Content:', JSON.stringify(content, null, 2));
} catch {
console.warn('[GeospatialTool] Content is not JSON, using as string:', content);
}

// Process results
if (typeof content === 'object' && content !== null) {
const parsedData = content as any;
if (parsedData.results?.length > 0) {
Expand All @@ -249,7 +254,9 @@ export const geospatialTool = ({ uiStream }: { uiStream: ReturnType<typeof creat
} else {
throw new Error("Response missing required 'location' or 'results' field");
}
} else throw new Error('Unexpected response format from mapping service');
} else {
throw new Error('Unexpected response format from mapping service');
}

feedbackMessage = `Successfully processed ${queryType} query for: ${mcpData.location.place_name || JSON.stringify(params)}`;
uiFeedbackStream.update(feedbackMessage);
Expand All @@ -264,6 +271,8 @@ export const geospatialTool = ({ uiStream }: { uiStream: ReturnType<typeof creat
uiStream.update(<BotMessage content={uiFeedbackStream.value} />);
}

return { type: 'MAP_QUERY_TRIGGER', originalUserInput: JSON.stringify(params), queryType, timestamp: new Date().toISOString(), mcp_response: mcpData, error: toolError };
const result = { type: 'MAP_QUERY_TRIGGER', originalUserInput: JSON.stringify(params), queryType, timestamp: new Date().toISOString(), mcp_response: mcpData, error: toolError };
console.log('[GeospatialTool] Returning result:', JSON.stringify(result, null, 2));
return result;
},
});