Skip to content
Merged
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
39 changes: 23 additions & 16 deletions components/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ 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';
import { updateDrawingContext } from '@/lib/actions/chat';
import { MapDataProvider, 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'

Expand All @@ -41,9 +41,6 @@ export function Chat({ id }: ChatProps) {
const [suggestions, setSuggestions] = useState<PartialRelated | null>(null)
const chatPanelRef = useRef<ChatPanelRef>(null);

// Ref to track the last message ID we refreshed the router for, to prevent infinite loops
const lastRefreshedMessageIdRef = useRef<string | null>(null);

const handleAttachment = () => {
chatPanelRef.current?.handleAttachmentClick();
};
Expand All @@ -57,11 +54,18 @@ export function Chat({ id }: ChatProps) {
}, [messages])

useEffect(() => {
// Check if device is mobile
const checkMobile = () => {
setIsMobile(window.innerWidth < 768)
}

// Initial check
checkMobile()

// Add event listener for window resize
window.addEventListener('resize', checkMobile)

// Cleanup
return () => window.removeEventListener('resize', checkMobile)
}, [])

Expand All @@ -72,16 +76,13 @@ export function Chat({ id }: ChatProps) {
}, [id, path, messages])

useEffect(() => {
// Check if there is a 'response' message in the history
const responseMessage = aiState.messages.findLast(m => m.type === 'response');

if (responseMessage && responseMessage.id !== lastRefreshedMessageIdRef.current) {
console.log('Chat.tsx: refreshing router for message:', responseMessage.id);
lastRefreshedMessageIdRef.current = responseMessage.id;
router.refresh();
if (aiState.messages[aiState.messages.length - 1]?.type === 'response') {
// Refresh the page to chat history updates
router.refresh()
}
}, [aiState.messages, router])
}, [aiState, router])

// Get mapData to access drawnFeatures
const { mapData } = useMapData();

useEffect(() => {
Expand All @@ -91,8 +92,10 @@ export function Chat({ id }: ChatProps) {
}
}, [isSubmitting])

// useEffect to call the server action when drawnFeatures changes
useEffect(() => {
if (id && mapData.drawnFeatures && mapData.cameraState) {
console.log('Chat.tsx: drawnFeatures changed, calling updateDrawingContext', mapData.drawnFeatures);
updateDrawingContext(id, {
drawnFeatures: mapData.drawnFeatures,
cameraState: mapData.cameraState,
Expand All @@ -109,6 +112,7 @@ export function Chat({ id }: ChatProps) {
onSelect={query => {
setInput(query)
setSuggestions(null)
// Use a small timeout to ensure state update before submission
setIsSubmitting(true)
}}
onClose={() => setSuggestions(null)}
Expand All @@ -118,9 +122,10 @@ export function Chat({ id }: ChatProps) {
);
};

// Mobile layout
if (isMobile) {
return (
<MapDataProvider>
<MapDataProvider> {/* Add Provider */}
<HeaderSearchButton />
<div className="mobile-layout-container">
<div className="mobile-map-section">
Expand Down Expand Up @@ -164,10 +169,12 @@ export function Chat({ id }: ChatProps) {
);
}

// Desktop layout
return (
<MapDataProvider>
<MapDataProvider> {/* Add Provider */}
<HeaderSearchButton />
<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-16 md:pt-20 pb-4 h-[calc(100vh-0.5in)] overflow-y-auto">
{isCalendarOpen ? (
<CalendarNotepad chatId={id} />
Expand Down Expand Up @@ -199,7 +206,7 @@ export function Chat({ id }: ChatProps) {
</div>
<div
className="w-1/2 p-4 fixed h-[calc(100vh-0.5in)] top-0 right-0 mt-[0.5in]"
style={{ zIndex: 10 }}
style={{ zIndex: 10 }} // Added z-index
>
{activeView ? <SettingsView /> : isUsageOpen ? <UsageView /> : <MapProvider />}
</div>
Expand Down
38 changes: 7 additions & 31 deletions components/header-search-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useActions, useUIState } from 'ai/rsc'
import { AI } from '@/app/actions'
import { nanoid } from 'nanoid'
import { UserMessage } from './user-message'
import { toast } from 'sonner'
import { toast } from 'react-toastify'
import { useSettingsStore } from '@/lib/store/settings'
import { useMapData } from './map/map-data-context'
Comment on lines 10 to 14
Copy link

Choose a reason for hiding this comment

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

Switching from sonner to react-toastify is a breaking change unless the app is already configured with ToastContainer and CSS imports. In this diff there’s no corresponding setup, so runtime toasts may silently fail or look unstyled.

Suggestion

Either revert to the previously configured toast library (sonner) or ensure react-toastify is globally configured (e.g., <ToastContainer /> in the root layout and the CSS import once). If you keep react-toastify, add/verify the app-wide setup in the same PR to avoid a partial migration.

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.


Expand All @@ -22,46 +22,24 @@ export function HeaderSearchButton() {
const { map } = useMap()
const { mapProvider } = useSettingsStore()
const { mapData } = useMapData()
// Cast the actions to our defined interface to avoid build errors
const actions = useActions<typeof AI>() as unknown as HeaderActions
const [, setMessages] = useUIState<typeof AI>()
const [isAnalyzing, setIsAnalyzing] = useState(false)

// Use state for portals to trigger re-renders when they are found
const [desktopPortal, setDesktopPortal] = useState<HTMLElement | null>(null)
const [mobilePortal, setMobilePortal] = useState<HTMLElement | null>(null)

useEffect(() => {
// Function to find and set portals
const findPortals = () => {
setDesktopPortal(document.getElementById('header-search-portal'))
setMobilePortal(document.getElementById('mobile-header-search-portal'))
}

// Initial check
findPortals()

// Use a MutationObserver to detect when portals are added to the DOM
const observer = new MutationObserver(() => {
findPortals()
})

observer.observe(document.body, {
childList: true,
subtree: true
})

return () => observer.disconnect()
// Portals can only be used on the client-side after the DOM has mounted
setDesktopPortal(document.getElementById('header-search-portal'))
setMobilePortal(document.getElementById('mobile-header-search-portal'))
}, [])

const handleResolutionSearch = async () => {
if (mapProvider === 'mapbox' && !map) {
toast.error('Map is not available yet. Please wait for it to load.')
return
}
if (mapProvider === 'google' && !mapData.cameraState) {
toast.error('Google Maps state is not available. Try moving the map first.')
return
}
if (!actions) {
toast.error('Search actions are not available.')
return
Expand Down Expand Up @@ -124,14 +102,12 @@ export function HeaderSearchButton() {
}
}

const isMapAvailable = mapProvider === 'mapbox' ? !!map : !!mapData.cameraState

const desktopButton = (
<Button
variant="ghost"
size="icon"
onClick={handleResolutionSearch}
disabled={isAnalyzing || !isMapAvailable || !actions}
disabled={isAnalyzing || !map || !actions}
title="Analyze current map view"
>
{isAnalyzing ? (
Expand All @@ -143,7 +119,7 @@ export function HeaderSearchButton() {
)

const mobileButton = (
<Button variant="ghost" size="sm" onClick={handleResolutionSearch} disabled={isAnalyzing || !isMapAvailable || !actions}>
<Button variant="ghost" size="sm" onClick={handleResolutionSearch} disabled={isAnalyzing || !map || !actions}>
<Search className="h-4 w-4 mr-2" />
Search
</Button>
Expand Down
60 changes: 34 additions & 26 deletions components/map/map-query-handler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,26 @@
import { useEffect } from 'react';
import { useMapData } from './map-data-context';
import { useMapToggle, MapToggleEnum } from '../map-toggle-context';
import { ToolOutput } from '@/lib/types/tools';

// Define the expected structure of the mcp_response from geospatialTool
interface McpResponseData {
location: {
latitude?: number;
longitude?: number;
place_name?: string;
address?: string;
};
mapUrl?: string;
}

interface ToolOutput {
type: string;
originalUserInput?: string;
timestamp: string;
mcp_response?: McpResponseData | null;
features?: any[];
error?: string | null;
}
Comment on lines +7 to +25
Copy link

Choose a reason for hiding this comment

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

ToolOutput is redefined locally and includes features?: any[], while the shared lib/types/tools.ts was deleted. This creates type drift risk across the codebase and makes it easy for tool producers/consumers to diverge silently.

Suggestion

Reintroduce a single shared ToolOutput type (e.g., lib/types/tools.ts) and import it in both tool producers and UI consumers. Avoid any[] for features—use geojson.Feature[] (or a narrower type used across drawing/map components).

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.


interface MapQueryHandlerProps {
toolOutput?: ToolOutput | null;
Expand All @@ -14,12 +33,7 @@ export const MapQueryHandler: React.FC<MapQueryHandlerProps> = ({ toolOutput })
const { setMapType } = useMapToggle();

useEffect(() => {
if (!toolOutput) {
if (process.env.NODE_ENV === 'development') {
console.warn('MapQueryHandler: missing toolOutput');
}
return;
}
if (!toolOutput) return;

if (toolOutput.type === 'DRAWING_TRIGGER' && toolOutput.features) {
console.log('MapQueryHandler: Received drawing data.', toolOutput.features);
Expand All @@ -28,25 +42,19 @@ export const MapQueryHandler: React.FC<MapQueryHandlerProps> = ({ toolOutput })
...prevData,
pendingFeatures: toolOutput.features
}));
} else if (toolOutput.type === 'MAP_QUERY_TRIGGER') {
if (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}`);
setMapData(prevData => ({
...prevData,
targetPosition: { lat: latitude, lng: longitude },
mapFeature: {
place_name,
mapUrl: toolOutput.mcp_response?.mapUrl
}
}));
} else {
console.warn('MapQueryHandler: invalid MAP_QUERY_TRIGGER payload', { toolOutput, mcp_response: toolOutput.mcp_response });
}
} else {
console.warn('MapQueryHandler: invalid MAP_QUERY_TRIGGER payload', { toolOutput, mcp_response: toolOutput?.mcp_response });
} else if (toolOutput.type === 'MAP_QUERY_TRIGGER' && 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}`);
setMapData(prevData => ({
...prevData,
targetPosition: { lat: latitude, lng: longitude },
mapFeature: {
place_name,
mapUrl: toolOutput.mcp_response?.mapUrl
}
}));
}
}
}, [toolOutput, setMapData, setMapType]);
Expand Down
7 changes: 3 additions & 4 deletions components/mobile-icons-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,9 @@ export const MobileIconsBar: React.FC<MobileIconsBarProps> = ({ onAttachmentClic
<Button variant="ghost" size="icon" onClick={toggleCalendar} title="Open Calendar" data-testid="mobile-calendar-button">
<CalendarDays className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
</Button>

{/* Portal target for resolution search button on mobile */}
<div id="mobile-header-search-portal" />

<Button variant="ghost" size="icon" data-testid="mobile-search-button">
<Search className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
</Button>
<a href="https://buy.stripe.com/14A3cv7K72TR3go14Nasg02" target="_blank" rel="noopener noreferrer">
<Button variant="ghost" size="icon">
<TentTree className="h-[1.2rem] w-[1.2rem] transition-all rotate-0 scale-100" />
Expand Down
4 changes: 2 additions & 2 deletions components/resolution-image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function ResolutionImage({ src, className, alt = 'Map Imagery' }: Resolut
<Dialog>
<DialogTrigger asChild>
<motion.div
className="w-fit cursor-pointer relative overflow-hidden rounded-lg border border-border/40 bg-background/30 backdrop-blur-md"
className="w-fit cursor-pointer relative glassmorphic overflow-hidden rounded-lg border bg-muted"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
Expand All @@ -42,7 +42,7 @@ export function ResolutionImage({ src, className, alt = 'Map Imagery' }: Resolut
</Card>
</motion.div>
</DialogTrigger>
<DialogContent className="sm:max-w-5xl max-h-[90vh] p-1 border-none bg-background/80 backdrop-blur-md">
<DialogContent className="sm:max-w-5xl max-h-[90vh] p-1 glassmorphic border-none">
<DialogHeader className="sr-only">
<DialogTitle>{alt}</DialogTitle>
</DialogHeader>
Expand Down
5 changes: 2 additions & 3 deletions lib/agents/researcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ Use these user-drawn areas/lines as primary areas of interest for your analysis
• Drawing shapes (circles, polygons, lines) on the map
• Highlighting areas or marking specific routes/boundaries
• Responding to requests like "Draw a 1km circle around...", "Highlight this area", etc.
• **Priority**: If a query involves both drawing and geospatial lookup (e.g., "Draw a circle around Eiffel Tower"), use this tool. It will geocode the location internally.

**Behavior when using \`drawingQueryTool\`:**
- Geocode the location internally if a place name is provided.
Expand Down Expand Up @@ -81,8 +80,8 @@ Use these user-drawn areas/lines as primary areas of interest for your analysis

#### **Summary of Decision Flow**
1. User gave explicit URLs? → \`retrieve\`
2. Draw shapes, highlight areas, or circle locations? → \`drawingQueryTool\` (mandatory)
3. Location/distance/direction/maps? → \`geospatialQueryTool\` (mandatory)
2. Location/distance/direction/maps? → \`geospatialQueryTool\` (mandatory)
3. Draw shapes, highlight areas, or circle locations? → \`drawingQueryTool\` (mandatory)
4. Everything else needing external data? → \`search\`
5. Otherwise → answer from knowledge

Expand Down
Loading