diff --git a/.env b/.env new file mode 100644 index 00000000..04edbb80 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" diff --git a/app/actions.tsx b/app/actions.tsx index f3b3c44f..43fa556d 100644 --- a/app/actions.tsx +++ b/app/actions.tsx @@ -336,7 +336,7 @@ export const AI = createAI({ title, messages: updatedMessages, }; - await saveChat(chat, actualUserId); // Pass actualUserId to saveChat + // await saveChat(chat, actualUserId); // Pass actualUserId to saveChat }, }); @@ -430,6 +430,13 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => { }; } + if (toolOutput.type === 'DRAWING') { + return { + id, + component: null, // Replace with a new DrawingHandler component if needed + }; + } + // Existing tool handling const searchResults = createStreamableValue(); searchResults.done(JSON.stringify(toolOutput)); diff --git a/components/map/mapbox-map.tsx b/components/map/mapbox-map.tsx index 3f2d5f2b..a3d4c41b 100644 --- a/components/map/mapbox-map.tsx +++ b/components/map/mapbox-map.tsx @@ -11,6 +11,8 @@ import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css' import { useMapToggle, MapToggleEnum } from '../map-toggle-context' import { useMapData } from './map-data-context'; // Add this import import { useMapLoading } from '../map-loading-context'; // Import useMapLoading +import { useAIState } from 'ai/rsc'; +import { AIState } from '@/app/actions'; mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_ACCESS_TOKEN as string; @@ -32,6 +34,7 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number const { mapData, setMapData } = useMapData(); // Consume the new context, get setMapData const { setIsMapLoaded } = useMapLoading(); // Get setIsMapLoaded from context const previousMapTypeRef = useRef(null) + const [aiState] = useAIState(); // Refs for long-press functionality const longPressTimerRef = useRef(null); @@ -524,6 +527,82 @@ export const Mapbox: React.FC<{ position?: { latitude: number; longitude: number // } }, [mapData.targetPosition, mapData.mapFeature, updateMapPosition]); + useEffect(() => { + if (!map.current) return; + + const drawFeatures = () => { + if (!map.current || !map.current.isStyleLoaded()) { + // Style not loaded yet, wait for it + map.current?.once('styledata', drawFeatures); + return; + } + + const drawingMessages = (aiState as AIState).messages.filter( + msg => msg.name === 'drawing' && msg.role === 'tool' + ); + + drawingMessages.forEach(message => { + try { + const { geojson } = JSON.parse(message.content); + if (!geojson || !geojson.features) return; + + const sourceId = `geojson-source-${message.id}`; + const source = map.current?.getSource(sourceId); + + if (source) { + // Source exists, update data + (source as mapboxgl.GeoJSONSource).setData(geojson); + } else { + // Source does not exist, add new source + map.current?.addSource(sourceId, { + type: 'geojson', + data: geojson, + }); + } + + // Layer definitions + const layers = [ + { + id: `points-layer-${message.id}`, + type: 'circle', + filter: ['==', '$type', 'Point'], + paint: { 'circle-radius': 6, 'circle-color': '#B42222' }, + }, + { + id: `lines-layer-${message.id}`, + type: 'line', + filter: ['==', '$type', 'LineString'], + paint: { 'line-color': '#B42222', 'line-width': 4 }, + layout: { 'line-join': 'round', 'line-cap': 'round' }, + }, + { + id: `polygons-layer-${message.id}`, + type: 'fill', + filter: ['==', '$type', 'Polygon'], + paint: { 'fill-color': '#B42222', 'fill-opacity': 0.5 }, + }, + ]; + + layers.forEach(layer => { + if (!map.current?.getLayer(layer.id)) { + map.current?.addLayer({ + ...layer, + source: sourceId, + } as mapboxgl.AnyLayer); + } + }); + + } catch (error) { + console.error('Error parsing or adding GeoJSON data:', error); + } + }); + }; + + if (aiState && (aiState as AIState).messages) { + drawFeatures(); + } + }, [aiState]); + // Long-press handlers const handleMouseDown = useCallback(() => { // Only activate long press if not in real-time mode (as that mode has its own interactions) diff --git a/lib/agents/tools/drawing.tsx b/lib/agents/tools/drawing.tsx new file mode 100644 index 00000000..65c196bd --- /dev/null +++ b/lib/agents/tools/drawing.tsx @@ -0,0 +1,17 @@ +import { drawingSchema } from '@/lib/schema/drawing'; +import { createStreamableUI } from 'ai/rsc'; + +export const drawingTool = ({ + uiStream, +}: { + uiStream: ReturnType; +}) => ({ + description: 'Draw GeoJSON features on the map. Use this tool to draw points, lines, and polygons.', + parameters: drawingSchema, + execute: async ({ geojson }: { geojson: any }) => { + return { + type: 'DRAWING', + geojson, + }; + }, +}); diff --git a/lib/agents/tools/index.tsx b/lib/agents/tools/index.tsx index 4c08f373..ff689068 100644 --- a/lib/agents/tools/index.tsx +++ b/lib/agents/tools/index.tsx @@ -3,6 +3,7 @@ import { retrieveTool } from './retrieve' import { searchTool } from './search' import { videoSearchTool } from './video-search' import { geospatialTool } from './geospatial' // Removed useGeospatialToolMcp import +import { drawingTool } from './drawing' export interface ToolProps { uiStream: ReturnType @@ -25,6 +26,9 @@ export const getTools = ({ uiStream, fullResponse }: ToolProps) => { geospatialQueryTool: geospatialTool({ uiStream // mcp: mcp || null // Removed mcp argument + }), + drawing: drawingTool({ + uiStream }) } diff --git a/lib/schema/drawing.ts b/lib/schema/drawing.ts new file mode 100644 index 00000000..c4335038 --- /dev/null +++ b/lib/schema/drawing.ts @@ -0,0 +1,71 @@ +import { z } from 'zod'; + +// GeoJSON Position (longitude, latitude) +const PositionSchema = z.tuple([z.number(), z.number()]); + +// Geometry Schemas +const PointSchema = z.object({ + type: z.literal('Point'), + coordinates: PositionSchema, +}); + +const MultiPointSchema = z.object({ + type: z.literal('MultiPoint'), + coordinates: z.array(PositionSchema), +}); + +const LineStringSchema = z.object({ + type: z.literal('LineString'), + coordinates: z.array(PositionSchema).min(2, { message: "LineString must have at least two positions." }), +}); + +const MultiLineStringSchema = z.object({ + type: z.literal('MultiLineString'), + coordinates: z.array(z.array(PositionSchema).min(2)), +}); + +// A LinearRing is a closed LineString with four or more positions. +const LinearRingSchema = z.array(PositionSchema).min(4, { message: "LinearRing must have at least four positions." }) + .refine(positions => { + const first = positions[0]; + const last = positions[positions.length - 1]; + return first[0] === last[0] && first[1] === last[1]; + }, { message: "The first and last positions of a LinearRing must be identical." }); + +const PolygonSchema = z.object({ + type: z.literal('Polygon'), + coordinates: z.array(LinearRingSchema), +}); + +const MultiPolygonSchema = z.object({ + type: z.literal('MultiPolygon'), + coordinates: z.array(z.array(LinearRingSchema)), +}); + +const GeometrySchema = z.union([ + PointSchema, + MultiPointSchema, + LineStringSchema, + MultiLineStringSchema, + PolygonSchema, + MultiPolygonSchema, +]); + +// Feature and FeatureCollection Schemas +const FeatureSchema = z.object({ + type: z.literal('Feature'), + geometry: GeometrySchema, + properties: z.record(z.string(), z.any()).nullable(), +}); + +const FeatureCollectionSchema = z.object({ + type: z.literal('FeatureCollection'), + features: z.array(FeatureSchema), +}); + +// The main schema for the drawing tool +export const drawingSchema = z.object({ + geojson: FeatureCollectionSchema.describe("A valid GeoJSON FeatureCollection object to be drawn on the map."), +}); + +export type Drawing = z.infer;