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
5 changes: 5 additions & 0 deletions src/tools/__snapshots__/tool-naming-convention.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ exports[`Tool Naming Convention should maintain consistent tool list (snapshot t
"description": "Generate a comparison URL for comparing two Mapbox styles side-by-side",
"toolName": "style_comparison_tool",
},
{
"className": "StyleHelperTool",
"description": "Interactive helper for creating custom Mapbox styles with specific features and colors",
"toolName": "style_helper_tool",
},
{
"className": "TilequeryTool",
"description": "Query vector and raster data from Mapbox tilesets at geographic coordinates",
Expand Down
25 changes: 25 additions & 0 deletions src/tools/style-helper-tool/StyleHelperTool.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { z } from 'zod';

export const StyleHelperToolSchema = z.object({
step: z
.enum(['start', 'features', 'colors', 'generate'])
Copy link
Member

Choose a reason for hiding this comment

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

Is there an introduction about how and when to use those steps? Can LLM understand this?

.optional()
.describe('Current step in the wizard'),
name: z.string().optional().describe('Name for the style'),
// Feature toggles
show_pois: z.boolean().optional().describe('Show POI labels'),
show_road_labels: z.boolean().optional().describe('Show road labels'),
show_place_labels: z.boolean().optional().describe('Show city/town labels'),
show_transit: z.boolean().optional().describe('Show transit features'),
show_buildings: z.boolean().optional().describe('Show buildings'),
show_parks: z.boolean().optional().describe('Show parks and green spaces'),
Comment on lines +10 to +15
Copy link
Member

Choose a reason for hiding this comment

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

We seems trying to list the layers, but can we exhaust all the layers?
For example: in streets v8 tileset we have airport_label , landuse_overlay, natural_label etc. are this listed here? Maybe it's better to "teach" LLM how to do the general configure and provide the resources(i.e. all the layers from streets v8), so that agent can do the job by themself, not relay on we list the possible features layers here.

// Colors
road_color: z.string().optional().describe('Road color (hex)'),
water_color: z.string().optional().describe('Water color (hex)'),
building_color: z.string().optional().describe('Building color (hex)'),
land_color: z.string().optional().describe('Land/background color (hex)'),
park_color: z.string().optional().describe('Park color (hex)'),
label_color: z.string().optional().describe('Label text color (hex)')
Comment on lines +17 to +22
Copy link
Member

Choose a reason for hiding this comment

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

The same here, the source layer could be a lot, how could we involved all related layers?
For example:

  • In the water_color we only control the water layer, but how about the waterway layer?
    The
  • In the land_color we control all the features from landuse layer, but how about landuse_overlay layer? And if we listed the park_color, how about airport_color, grass_color, sand_color etc.?

});

export type StyleHelperToolInput = z.infer<typeof StyleHelperToolSchema>;
348 changes: 348 additions & 0 deletions src/tools/style-helper-tool/StyleHelperTool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,348 @@
import { BaseTool } from '../BaseTool.js';
import {
StyleHelperToolSchema,
type StyleHelperToolInput
} from './StyleHelperTool.schema.js';

export class StyleHelperTool extends BaseTool<typeof StyleHelperToolSchema> {
name = 'style_helper_tool';
description =
'Interactive helper for creating custom Mapbox styles with specific features and colors';

constructor() {
super({ inputSchema: StyleHelperToolSchema });
}

protected async execute(input: StyleHelperToolInput) {
const step = input.step || 'start';

switch (step) {
case 'start':
return this.handleStart();
case 'features':
return this.handleFeatures(input);
case 'colors':
return this.handleColors(input);
case 'generate':
return this.handleGenerate(input);
default:
return this.handleStart();
}
}

private handleStart() {
return {
content: [
{
type: 'text' as const,
text: `**Mapbox Style Helper - Initialized**

**Current step: 1 of 4**

**Waiting for:** Style name

---
**Status: REQUIRES USER INPUT FOR NAME**`
}
],
isError: false
};
}

private handleFeatures(input: StyleHelperToolInput) {
if (!input.name) {
return this.handleStart();
}

return {
content: [
{
type: 'text' as const,
text: `**Style:** ${input.name}

**Current step: 2 of 4**

**Waiting for:** Feature toggles

**Available options:**
• show_place_labels (true/false)
• show_road_labels (true/false)
• show_pois (true/false)
• show_buildings (true/false)
• show_parks (true/false)
• show_transit (true/false)

---
**Status: REQUIRES USER FEATURE SELECTION**`
}
],
isError: false
};
}

private handleColors(input: StyleHelperToolInput) {
if (!input.name) {
return this.handleStart();
}

const features = this.getFeatureSummary(input);

return {
content: [
{
type: 'text' as const,
text: `**Style:** ${input.name}
**Features:** ${features}

**Current step: 3 of 4**

**Waiting for:** Color values

**Required:**
• road_color (hex)
• water_color (hex)
• land_color (hex)
• label_color (hex)

**Optional:**
• building_color (hex)
• park_color (hex)

---
**Status: REQUIRES USER COLOR SELECTION**`
}
],
isError: false
};
}

private handleGenerate(input: StyleHelperToolInput) {
if (
!input.name ||
!input.road_color ||
!input.water_color ||
!input.land_color ||
!input.label_color
) {
return {
content: [
{
type: 'text' as const,
text: 'Missing required colors. Please complete all color steps.'
}
],
isError: true
};
}

const style = this.generateStyle(input);

return {
content: [
{
type: 'text' as const,
text: `**COMPLETED: Style Generated**

**Name:** ${input.name}

**Final Configuration:**
• POIs: ${input.show_pois ? 'shown' : 'hidden'}
• Road Labels: ${input.show_road_labels ? 'shown' : 'hidden'}
• Place Labels: ${input.show_place_labels ? 'shown' : 'hidden'}
• Transit: ${input.show_transit ? 'shown' : 'hidden'}
• Buildings: ${input.show_buildings ? 'shown' : 'hidden'}
• Parks: ${input.show_parks ? 'shown' : 'hidden'}

**Colors:**
• Roads: ${input.road_color}
• Water: ${input.water_color}
• Buildings: ${input.building_color || '#e0e0e0'}
• Land: ${input.land_color}
• Parks: ${input.park_color || '#d0e5d0'}
• Labels: ${input.label_color}

**Generated Style JSON:**
\`\`\`json
${JSON.stringify(style, null, 2)}
\`\`\`

---
**Status: STYLE GENERATION COMPLETE**`
}
],
isError: false
};
}

private generateStyle(input: StyleHelperToolInput) {
const layers: Record<string, unknown>[] = [
// Background
{
id: 'land',
type: 'background',
paint: {
'background-color': input.land_color
}
},
// Water
{
id: 'water',
type: 'fill',
source: 'composite',
'source-layer': 'water',
paint: {
'fill-color': input.water_color
}
}
];

// Parks (if enabled)
if (input.show_parks) {
layers.push({
id: 'landuse_park',
type: 'fill',
source: 'composite',
'source-layer': 'landuse',
filter: ['==', ['get', 'class'], 'park'],
paint: {
'fill-color': input.park_color || '#d0e5d0',
'fill-opacity': 0.8
}
});
}

// Roads - simplified with just two layers
layers.push({
id: 'road',
type: 'line',
source: 'composite',
'source-layer': 'road',
layout: {
'line-cap': 'round',
'line-join': 'round'
},
paint: {
'line-color': input.road_color,
'line-width': [
'interpolate',
['exponential', 1.5],
['zoom'],
5,
0.5,
18,
20
]
}
});

// Buildings (if enabled)
if (input.show_buildings) {
layers.push({
id: 'building',
type: 'fill',
source: 'composite',
'source-layer': 'building',
minzoom: 14,
paint: {
'fill-color': input.building_color || '#e0e0e0',
'fill-opacity': ['interpolate', ['linear'], ['zoom'], 14, 0, 15, 1]
}
});
}

// Place labels (if enabled)
if (input.show_place_labels !== false) {
// Default to true
layers.push({
id: 'place_label',
type: 'symbol',
source: 'composite',
'source-layer': 'place_label',
layout: {
'text-field': ['get', 'name'],
'text-font': ['DIN Pro Medium', 'Arial Unicode MS Regular'],
'text-size': ['interpolate', ['linear'], ['zoom'], 8, 12, 16, 20]
},
paint: {
'text-color': input.label_color,
'text-halo-color': input.land_color,
'text-halo-width': 1.5
}
});
}

// Road labels (if enabled)
if (input.show_road_labels) {
const roadLabel: Record<string, unknown> = {
id: 'road_label',
type: 'symbol',
source: 'composite',
'source-layer': 'road',
minzoom: 13,
layout: {
'symbol-placement': 'line',
'text-field': ['get', 'name'],
'text-font': ['DIN Pro Regular', 'Arial Unicode MS Regular'],
'text-size': 12
},
paint: {
'text-color': input.label_color,
'text-halo-color': input.land_color,
'text-halo-width': 1
}
};
layers.push(roadLabel);
}

// POI labels (if enabled)
if (input.show_pois) {
const poiLabel: Record<string, unknown> = {
id: 'poi_label',
type: 'symbol',
source: 'composite',
'source-layer': 'poi_label',
minzoom: 13,
layout: {
'text-field': ['get', 'name'],
'text-font': ['DIN Pro Regular', 'Arial Unicode MS Regular'],
'text-size': 11
},
paint: {
'text-color': input.label_color,
'text-halo-color': input.land_color,
'text-halo-width': 1
}
};
layers.push(poiLabel);
}

return {
version: 8,
name: input.name,
metadata: {
'mapbox:autocomposite': true
},
sources: {
composite: {
type: 'vector',
url: 'mapbox://mapbox.mapbox-terrain-v2,mapbox.mapbox-streets-v8'
}
},
sprite: 'mapbox://sprites/mapbox/streets-v12',
glyphs: 'mapbox://fonts/mapbox/{fontstack}/{range}.pbf',
layers: layers
};
}

private getFeatureSummary(input: StyleHelperToolInput): string {
const features = [];
if (input.show_pois) features.push('POIs');
if (input.show_road_labels) features.push('road labels');
if (input.show_place_labels) features.push('place labels');
if (input.show_transit) features.push('transit');
if (input.show_buildings) features.push('buildings');
if (input.show_parks) features.push('parks');

return features.length > 0 ? features.join(', ') : 'none selected yet';
}
}
Loading
Loading