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: 1 addition & 1 deletion app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { MapQueryHandler } from '@/components/map/map-query-handler'

// Define the type for related queries
type RelatedQueries = {
items: { query: string }[]
items: string[]
}

async function submit(formData?: FormData, skip?: boolean) {
Expand Down
8 changes: 4 additions & 4 deletions components/search-related-optimized.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ export const SearchRelated: React.FC<SearchRelatedProps> = React.memo(({
// OPTIMIZATION: Memoize filtered and mapped items
const relatedItems = useMemo(() => {
return data?.items
?.filter(item => item?.query !== '')
.map((item, index) => (
?.filter(query => query !== '')
.map((query, index) => (
<div
key={`related-${index}`}
className="flex items-start w-full animate-in fade-in slide-in-from-bottom-2 duration-300"
Expand All @@ -60,9 +60,9 @@ export const SearchRelated: React.FC<SearchRelatedProps> = React.memo(({
<Button
variant="link"
className="flex-1 justify-start px-0 py-1 h-fit font-semibold text-accent-foreground/50 whitespace-normal text-left"
onClick={() => handleRelatedClick(item?.query || '')}
onClick={() => handleRelatedClick(query || '')}
>
{item?.query}
{query}
</Button>
</div>
)) || []
Expand Down
8 changes: 4 additions & 4 deletions components/search-related.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ export const SearchRelated: React.FC<SearchRelatedProps> = React.memo(({
// OPTIMIZATION: Memoize filtered and mapped items
const relatedItems = useMemo(() => {
return data?.items
?.filter(item => item?.query !== '')
.map((item, index) => (
?.filter(query => query !== '')

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Filter out falsy items, not just empty strings.

query => query !== '' lets undefined/null through, which can occur for not-yet-filled elements during partial streamObject streaming. That renders a blank button whose onClick submits an empty related_query. suggestions-dropdown.tsx already uses a truthy filter; align here for consistency.

🛡️ Proposed fix
-      ?.filter(query => query !== '')
+      ?.filter((query): query is string => Boolean(query))
📝 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
?.filter(query => query !== '')
?.filter((query): query is string => Boolean(query))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/search-related.tsx` at line 53, The filter currently in the
search-related rendering uses query => query !== '' which allows null/undefined
through; change the filter to remove all falsy values (e.g., use a truthy check
like Boolean(query) or query => !!query) so undefined/null are filtered out the
same way as suggestions-dropdown.tsx; update the filter applied to the array
being mapped in components/search-related.tsx (the expression using
?.filter(...)) to use the truthy check so no blank button is rendered and
onClick never submits an empty related_query.

.map((query, index) => (
<div
key={`related-${index}`}
className="flex items-start w-full animate-in fade-in slide-in-from-bottom-2 duration-300"
Expand All @@ -60,9 +60,9 @@ export const SearchRelated: React.FC<SearchRelatedProps> = React.memo(({
<Button
variant="link"
className="flex-1 justify-start px-0 py-1 h-fit font-semibold text-accent-foreground/50 whitespace-normal text-left"
onClick={() => handleRelatedClick(item?.query || '')}
onClick={() => handleRelatedClick(query || '')}
>
{item?.query}
{query}
</Button>
</div>
)) || []
Expand Down
12 changes: 6 additions & 6 deletions components/suggestions-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const SuggestionsDropdown: React.FC<SuggestionsDropdownProps> = ({
const dropdownRef = useRef<HTMLDivElement>(null)

const suggestionItems = useMemo(
() => suggestions?.items?.filter(item => item?.query) || [],
() => (suggestions?.items?.filter(query => query) as string[]) || [],
[suggestions]
)

Expand All @@ -47,7 +47,7 @@ export const SuggestionsDropdown: React.FC<SuggestionsDropdownProps> = ({
} else if (e.key === 'Enter') {
e.preventDefault()
if (selectedIndex >= 0 && suggestionItems[selectedIndex]) {
onSelect(suggestionItems[selectedIndex].query!)
onSelect(suggestionItems[selectedIndex])
}
} else if (e.key === 'Escape') {
onClose()
Expand Down Expand Up @@ -92,8 +92,8 @@ export const SuggestionsDropdown: React.FC<SuggestionsDropdownProps> = ({
>
<div className="p-2">
<p className="text-sm text-muted-foreground px-2 pb-1">Suggestions</p>
{suggestionItems.map((item, index) => {
if (!item?.query) return null
{suggestionItems.map((query, index) => {
if (!query) return null
return (
<Button
key={index}
Expand All @@ -102,11 +102,11 @@ export const SuggestionsDropdown: React.FC<SuggestionsDropdownProps> = ({
'w-full justify-start px-2 py-1 h-fit font-normal text-accent-foreground whitespace-normal text-left',
selectedIndex === index && 'bg-accent'
)}
onClick={() => onSelect(item.query!)}
onClick={() => onSelect(query)}
onMouseEnter={() => setSelectedIndex(index)}
>
<ArrowRight className="h-4 w-4 mr-2 flex-shrink-0" />
{item.query}
{query}
</Button>
)
})}
Expand Down
2 changes: 1 addition & 1 deletion lib/agents/query-suggestor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export async function querySuggestor(
// OPTIMIZATION: Use a more concise system prompt to reduce token usage
const result = await streamObject({
model: (await getModel()) as LanguageModel,
system: `Generate 3 follow-up queries that explore the subject matter deeper. Format as JSON with an "items" array containing objects with "query" fields. Keep queries concise and relevant.`,
system: `Generate 3 follow-up queries that explore the subject matter deeper. Format as JSON with an "items" array containing strings. Keep queries concise and relevant.`,
messages,
schema: relatedSchema,
temperature: 0.7, // Lower temperature for more consistent results
Expand Down
22 changes: 14 additions & 8 deletions lib/agents/tools/geospatial.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ Uses the Mapbox Search Box Text Search API endpoint to power searching for and g
,
parameters: geospatialQuerySchema,
execute: async (params: z.infer<typeof geospatialQuerySchema>) => {
const { queryType, includeMap = true } = params;
const { queryType, includeMap = true, latitude, longitude } = params;
console.log('[GeospatialTool] Execute called with:', params, 'and map provider:', mapProvider);
Comment on lines +244 to 245

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle reverse queries in the Gemini branch.

After flattening, reverse inputs only carry latitude/longitude, but this path still builds its prompt from location || query. In Google mode, a reverse request now becomes Find the location for: undefined and falls into the generic error path instead of reverse-geocoding. Build the Gemini input from the coordinate pair for queryType === 'reverse', or bypass this branch for that query type.

Suggested fix
-        const searchText = (params as any).location || (params as any).query;
+        const searchText =
+          queryType === 'reverse' && latitude !== undefined && longitude !== undefined
+            ? `${latitude},${longitude}`
+            : params.location || params.query;
+
+        if (!searchText) {
+          throw new Error(`'${queryType}' query is not supported by the Gemini grounding path`);
+        }
         const prompt = `Find the location for: ${searchText}`;

Also applies to: 262-263

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/agents/tools/geospatial.tsx` around lines 244 - 245, The Gemini branch
currently builds its prompt from "location || query" which breaks reverse
geocoding because flattened reverse inputs only have latitude/longitude; update
the code in the GeospatialTool execution path (look for the destructuring of
queryType, includeMap, latitude, longitude and the mapProvider usage) to detect
queryType === 'reverse' and construct the Gemini input from the coordinate pair
(e.g., `${latitude}, ${longitude}`) or skip the Gemini prompt branch for reverse
queries so the reverse-geocoding flow is used; apply the same change where the
prompt is built around lines referencing "location || query" (also at the
corresponding 262-263 area).


const uiFeedbackStream = createStreamableValue<string>();
Expand Down Expand Up @@ -275,19 +275,19 @@ Uses the Mapbox Search Box Text Search API endpoint to power searching for and g
// as I don't have a way to inspect it at the moment.
const place = (gsr as any).results[0].place;
if (place) {
const { latitude, longitude } = place.coordinates;
const { latitude: lat, longitude: lng } = place.coordinates;
const place_name = place.displayName;

const mcpData: McpResponse = {
location: {
latitude,
longitude,
latitude: lat,
longitude: lng,
place_name,
},
};

if (mapProvider === 'google') {
mcpData.mapUrl = getGoogleStaticMapUrl(latitude, longitude);
mcpData.mapUrl = getGoogleStaticMapUrl(lat, lng);
}

feedbackMessage = `Found location: ${place_name}`;
Expand Down Expand Up @@ -355,12 +355,18 @@ Uses the Mapbox Search Box Text Search API endpoint to power searching for and g
return { places: [params.origin, params.destination], includeMapPreview: includeMap, mode: params.mode || 'driving' };
}
case 'reverse': {
if (!params.coordinates) throw new Error("'reverse' query requires coordinates");
return { searchText: `${params.coordinates.latitude},${params.coordinates.longitude}`, includeMapPreview: includeMap, maxResults: params.maxResults || 5 };
if (latitude === undefined || longitude === undefined) throw new Error("'reverse' query requires latitude and longitude");
return { searchText: `${latitude},${longitude}`, includeMapPreview: includeMap, maxResults: params.maxResults || 5 };
}
case 'search': {
if (!params.query) throw new Error("'search' query requires query");
return { searchText: params.query, includeMapPreview: includeMap, maxResults: params.maxResults || 5, ...(params.coordinates && { proximity: `${params.coordinates.latitude},${params.coordinates.longitude}` }), ...(params.radius && { radius: params.radius }) };
return {
searchText: params.query,
includeMapPreview: includeMap,
maxResults: params.maxResults || 5,
...(latitude !== undefined && longitude !== undefined && { proximity: `${latitude},${longitude}` }),
...(params.radius && { radius: params.radius })
};
}
case 'geocode':
case 'map': {
Expand Down
22 changes: 22 additions & 0 deletions lib/schema/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# AI Schema Compatibility Guidelines

To ensure maximum compatibility with OpenAI-compatible endpoints (e.g., xAI, Grok, Gemini), all Zod schemas used for structured AI output must follow these rules.

## Core Rules

1. **Avoid `z.literal()`**: Some endpoints do not support the `const` constraint in JSON Schema generated by `z.literal()`. Use `z.string()` and provide allowed values in the field description.
2. **Avoid `z.any()`**: Use explicit union types or more specific schemas to ensure validation reliability.
3. **Prefer Flat Optional Fields**: Avoid deeply nested required objects or complex discriminated unions. Instead, use a flat object with optional fields.
4. **Use Field Descriptions**: Convey conditional requirements (e.g., "Field X is required if `queryType` is 'search'") via the `.describe()` method on fields.
5. **Runtime Reconstruction**: If the frontend or downstream tools require a nested format (like standard GeoJSON), implement a reconstruction step in the application logic (e.g., in server actions) to transform the flat AI output into the desired structure.

## Canonical Examples

### Flat Schema with Conditional Requirements
See `lib/schema/geospatial.tsx` for how `queryType` guides the use of other optional top-level fields.

### Avoiding Const and Nested GeoJSON
See `lib/schema/resolution-search.ts` for a flattened GeoJSON structure that avoids `z.literal()` and `z.any()`.

### Runtime Reconstruction
Comment on lines +15 to +21

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add missing blank lines after subheadings (MD022).

markdownlint flags missing blank lines below headings at Line 15, Line 18, and Line 21. This can cause lint failures in CI/doc checks.

Proposed fix
 ### Flat Schema with Conditional Requirements
+
 See `lib/schema/geospatial.tsx` for how `queryType` guides the use of other optional top-level fields.
 
 ### Avoiding Const and Nested GeoJSON
+
 See `lib/schema/resolution-search.ts` for a flattened GeoJSON structure that avoids `z.literal()` and `z.any()`.
 
 ### Runtime Reconstruction
+
 See `app/actions.tsx` (specifically the `processResolutionSearch` function) for how flattened features are reconstructed into a standard `FeatureCollection`.
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 15-15: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 18-18: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


[warning] 21-21: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/schema/README.md` around lines 15 - 21, Add a blank line immediately
after each subheading line shown in the diff—specifically after the "### Flat
Schema with Conditional Requirements", "### Avoiding Const and Nested GeoJSON",
and "### Runtime Reconstruction" headings in lib/schema/README.md—so that
markdownlint rule MD022 is satisfied; edit the file to insert a single empty
line following each of those heading lines.

See `app/actions.tsx` (specifically the `processResolutionSearch` function) for how flattened features are reconstructed into a standard `FeatureCollection`.
18 changes: 10 additions & 8 deletions lib/schema/geospatial.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ import { z } from 'zod';
// queryType are conveyed to the LLM via the queryType description; runtime
// behavior in the tool's execute() already tolerates missing fields per
// queryType, so loosening the schema introduces no new failure modes.
//
// Compatibility Note: Following the established flattening pattern,
// nested 'coordinates' object has been replaced with top-level 'latitude'
// and 'longitude' fields.

export const geospatialQuerySchema = z.object({
queryType: z.enum(['search', 'geocode', 'reverse', 'directions', 'distance', 'map'])
.describe(
"Type of geospatial query. Set the corresponding fields: " +
"'search' → query (optionally coordinates, radius, maxResults); " +
"'search' → query (optionally latitude/longitude, radius, maxResults); " +
"'geocode' → location (optionally maxResults); " +
"'reverse' → coordinates (optionally maxResults); " +
"'reverse' → latitude + longitude (optionally maxResults); " +
"'directions' → origin + destination (optionally mode); " +
"'distance' → origin + destination (optionally mode); " +
"'map' → location."
Expand All @@ -25,12 +29,10 @@ export const geospatialQuerySchema = z.object({
.min(1)
.optional()
.describe("Location to geocode or render as a map (used by 'geocode' and 'map')"),
coordinates: z.object({
latitude: z.number().min(-90).max(90),
longitude: z.number().min(-180).max(180)
})
.optional()
.describe("Coordinates (required for 'reverse', optional proximity hint for 'search')"),
latitude: z.number().min(-90).max(90).optional()
.describe("Latitude for reverse geocoding or map centering (used by 'reverse', optional for 'search')"),
longitude: z.number().min(-180).max(180).optional()
.describe("Longitude for reverse geocoding or map centering (used by 'reverse', optional for 'search')"),
Comment on lines +32 to +35

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.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep the field descriptions aligned with the executor.

These descriptions mention map centering, but lib/agents/tools/geospatial.tsx still rejects queryType === 'map' unless location is present. That mismatch makes the schema more likely to steer the model into coordinate-only map payloads that fail at runtime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/schema/geospatial.tsx` around lines 32 - 35, Update the
latitude/longitude field descriptions in lib/schema/geospatial.tsx to match the
executor behavior in lib/agents/tools/geospatial.tsx: explicitly state that
coordinates are optional in general but are required when queryType === 'map'
(or that 'map' queries require a full location object), so the schema text for
latitude and longitude reflects the executor's requirement and avoids implying
map-centering works with coordinates-only payloads.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Require latitude and longitude together.

Flattening made the two coordinate fields independently optional, so malformed payloads like { queryType: 'reverse', latitude: 40.7 } now pass schema validation and only fail later in lib/agents/tools/geospatial.tsx. Add a cross-field refinement so partial coordinates are rejected before tool execution.

Suggested fix
 export const geospatialQuerySchema = z.object({
   queryType: z.enum(['search', 'geocode', 'reverse', 'directions', 'distance', 'map'])
@@
   includeMap: z.boolean()
     .optional()
     .default(true)
     .describe("Whether to include a map preview/URL in the response"),
-});
+}).superRefine(({ latitude, longitude }, ctx) => {
+  if ((latitude === undefined) !== (longitude === undefined)) {
+    ctx.addIssue({
+      code: z.ZodIssueCode.custom,
+      path: [latitude === undefined ? 'latitude' : 'longitude'],
+      message: 'latitude and longitude must be provided together',
+    });
+  }
+});
📝 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
latitude: z.number().min(-90).max(90).optional()
.describe("Latitude for reverse geocoding or map centering (used by 'reverse', optional for 'search')"),
longitude: z.number().min(-180).max(180).optional()
.describe("Longitude for reverse geocoding or map centering (used by 'reverse', optional for 'search')"),
export const geospatialQuerySchema = z.object({
queryType: z.enum(['search', 'geocode', 'reverse', 'directions', 'distance', 'map'])
.describe("Type of geospatial query"),
latitude: z.number().min(-90).max(90).optional()
.describe("Latitude for reverse geocoding or map centering (used by 'reverse', optional for 'search')"),
longitude: z.number().min(-180).max(180).optional()
.describe("Longitude for reverse geocoding or map centering (used by 'reverse', optional for 'search')"),
includeMap: z.boolean()
.optional()
.default(true)
.describe("Whether to include a map preview/URL in the response"),
}).superRefine(({ latitude, longitude }, ctx) => {
if ((latitude === undefined) !== (longitude === undefined)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: [latitude === undefined ? 'latitude' : 'longitude'],
message: 'latitude and longitude must be provided together',
});
}
});
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/schema/geospatial.tsx` around lines 32 - 35, The latitude and longitude
fields are independently optional which allows partial coordinates to pass
validation; update the Zod schema that defines these fields (the object
containing latitude and longitude in lib/schema/geospatial.tsx) to add a
cross-field refinement (using .refine or .superRefine on the parent schema) that
enforces either both latitude and longitude are present or both are absent and
returns a clear validation error when one is provided without the other;
reference the latitude and longitude field names in the refinement to check
presence and reject partial coordinate payloads before tool execution.

origin: z.string()
.min(1)
.optional()
Expand Down
11 changes: 4 additions & 7 deletions lib/schema/related.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { DeepPartial } from 'ai'
import { z } from 'zod'

// Simplified schema to reduce nested complexity for better model compatibility.
// Items are now plain strings instead of objects with a 'query' property.
export const relatedSchema = z.object({
items: z
.array(
z.object({
query: z.string()
})
)
.length(3)
items: z.array(z.string()).length(3)
})

export type PartialRelated = DeepPartial<typeof relatedSchema>

export type Related = z.infer<typeof relatedSchema>