Skip to content
Open
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
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Do not commit .env with credentials; replace with .env.example and ignore .env

Committing environment files with secrets (even local defaults) is a security risk and makes accidental leaks likely. Also, dotenv-linter flags the quoted value. Recommended:

  • Remove .env from version control and add it to .gitignore.
  • Add a .env.example with placeholders for developers to copy locally.

Proposed changes:

Delete the committed secret (leaves the file empty if you don’t remove the file outright):

-DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"

Add a .env.example (outside this hunk) with placeholders:

# .env.example
# Copy to .env and fill real credentials locally
DATABASE_URL=postgresql://USER:PASSWORD@HOST:5432/DBNAME

And ignore .env (outside this hunk):

# .gitignore
.env

If, for some reason, this file must stay, at minimum remove quotes to satisfy dotenv-linter:

-DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
+DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres

I can open a follow-up PR to codify this workflow (add .env.example, update docs).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres"
🧰 Tools
🪛 dotenv-linter (3.3.0)

[warning] 1-1: [QuoteCharacter] The value has quote characters (', ")

(QuoteCharacter)

🤖 Prompt for AI Agents
In .env around lines 1 to 1, the repository contains a committed environment
file with hardcoded DB credentials and quoted value (security and dotenv-linter
issues); remove the sensitive .env from version control (git rm --cached if you
need to keep locally), add .env to .gitignore, create a .env.example with
placeholder values for DATABASE_URL for developers to copy, and if you must keep
a .env file in the repo temporarily remove the quotes around the value to
satisfy dotenv-linter (preferred: delete committed secret and replace with
.env.example).

9 changes: 8 additions & 1 deletion app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ export const AI = createAI<AIState, UIState>({
title,
messages: updatedMessages,
};
await saveChat(chat, actualUserId); // Pass actualUserId to saveChat
// await saveChat(chat, actualUserId); // Pass actualUserId to saveChat
},
});

Expand Down Expand Up @@ -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));
Expand Down
79 changes: 79 additions & 0 deletions components/map/mapbox-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<MapToggleEnum | null>(null)
const [aiState] = useAIState();

// Refs for long-press functionality
const longPressTimerRef = useRef<NodeJS.Timeout | null>(null);
Expand Down Expand Up @@ -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)
Expand Down
17 changes: 17 additions & 0 deletions lib/agents/tools/drawing.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { drawingSchema } from '@/lib/schema/drawing';
import { createStreamableUI } from 'ai/rsc';

export const drawingTool = ({
uiStream,
}: {
uiStream: ReturnType<typeof createStreamableUI>;
}) => ({
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,
};
},
});
Comment on lines +9 to +17
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Type the execute input and validate at runtime as a safety net

parameters uses drawingSchema for LLM tool calling, but adding an explicit runtime parse inside execute makes the tool robust against malformed inputs and keeps TypeScript happy.

Apply this diff:

-import { drawingSchema } from '@/lib/schema/drawing';
+import { drawingSchema } from '@/lib/schema/drawing';
+import type { z } from 'zod';

@@
-  execute: async ({ geojson }: { geojson: any }) => {
-    return {
-      type: 'DRAWING',
-      geojson,
-    };
-  },
+  execute: async (input: z.infer<typeof drawingSchema>) => {
+    // Safety net: validate again at runtime
+    const parsed = drawingSchema.parse(input);
+    return {
+      type: 'DRAWING' as const,
+      geojson: parsed.geojson,
+    };
+  },

If you prefer a softer failure mode, use safeParse and return an error to the UI stream instead of throwing.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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,
};
},
});
import { drawingSchema } from '@/lib/schema/drawing';
import type { z } from 'zod';
description: 'Draw GeoJSON features on the map. Use this tool to draw points, lines, and polygons.',
parameters: drawingSchema,
execute: async (input: z.infer<typeof drawingSchema>) => {
// Safety net: validate again at runtime
const parsed = drawingSchema.parse(input);
return {
type: 'DRAWING' as const,
geojson: parsed.geojson,
};
},
});
🤖 Prompt for AI Agents
In lib/agents/tools/drawing.tsx around lines 9 to 17, the execute handler
currently trusts the incoming { geojson } without runtime validation; update the
function signature to type the input explicitly (e.g., ({ geojson }: { geojson:
unknown }) or matching drawingSchema input type) and perform a runtime
validation using drawingSchema.parse or drawingSchema.safeParse before
proceeding; on successful parse return the existing DRAWING payload with the
validated geojson, and on failure either throw a clear error or (preferred
softer mode) return an error result that the UI can stream back to the user.

4 changes: 4 additions & 0 deletions lib/agents/tools/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof createStreamableUI>
Expand All @@ -25,6 +26,9 @@ export const getTools = ({ uiStream, fullResponse }: ToolProps) => {
geospatialQueryTool: geospatialTool({
uiStream
// mcp: mcp || null // Removed mcp argument
}),
drawing: drawingTool({
uiStream
})
}

Expand Down
71 changes: 71 additions & 0 deletions lib/schema/drawing.ts
Original file line number Diff line number Diff line change
@@ -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<typeof drawingSchema>;