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
53 changes: 52 additions & 1 deletion demo/vue/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
</span>
</div>
<button
@click="clearMessages"
@click="clearAll"
class="px-3 py-1.5 text-sm bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300"
>
Clear Chat
Expand Down Expand Up @@ -112,6 +112,31 @@
</div>
</div>

<!-- Grouped Maps (groupId) — one merged card per groupId.
Try the "Trip A ①→④" samples in order: markers + a
route accumulate here on ONE map. "Trip B" makes a
separate card. -->
<div
v-for="group in groups"
:key="group.groupId"
v-show="ViewComponent"
class="bg-white rounded-lg shadow-md overflow-hidden"
>
<div class="p-3 border-b border-gray-200 flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-700">Map: {{ group.groupId }}</h2>
<span class="text-xs text-gray-500">{{ group.results.length }} operation(s)</span>
</div>
<div class="h-96">
<component
:is="ViewComponent"
:results="group.results"
:selectedResult="group.results[group.results.length - 1]"
:googleMapKey="googleMapKey"
:sendTextMessage="handleSendTextMessage"
/>
</div>
</div>

<!-- Preview Component -->
<div v-if="PreviewComponent && result" class="bg-white rounded-lg shadow-md overflow-hidden">
<div class="p-3 border-b border-gray-200">
Expand Down Expand Up @@ -163,10 +188,28 @@ import { ref, computed, nextTick, watch } from "vue";
import { plugin } from "../../src/vue";
import { useChat } from "./useChat";
import type { ToolPlugin, ToolSample, ToolResult } from "gui-chat-protocol/vue";
import type { MapToolData } from "../../src/core/types";

// Plugin configuration
const currentPlugin = plugin as unknown as ToolPlugin;

// Grouped maps: results carrying the same `groupId` accumulate into
// ONE card (markers layer, route overlays) via the View's `results`
// replay prop. This mirrors what a host (e.g. MulmoClaude) does to
// merge same-groupId map operations onto one map surface.
const groups = ref<{ groupId: string; results: ToolResult[] }[]>([]);

const routeToGroup = (toolResult: ToolResult): void => {
const groupId = (toolResult.data as MapToolData | undefined)?.groupId;
if (!groupId) return;
const existing = groups.value.find((g) => g.groupId === groupId);
if (existing) {
existing.results = [...existing.results, toolResult];
} else {
groups.value.push({ groupId, results: [toolResult] });
}
};

// Google Map API Key
const googleMapKey = import.meta.env.VITE_GOOGLE_MAP_KEY || "";

Expand Down Expand Up @@ -235,9 +278,17 @@ const handleUpdateResult = (updated: ToolResult) => {
console.log("Result updated:", updated);
};

const clearAll = () => {
clearMessages();
groups.value = [];
};

const executeSample = async (sample: ToolSample) => {
const toolResult = await executePlugin(sample.args);
result.value = toolResult;
// Route into the grouped-maps section when the sample carries a
// groupId (same id → same card accumulates).
routeToGroup(toolResult);

// Add messages in correct order for OpenAI API
const toolCallId = `sample_${Date.now()}`;
Expand Down
5 changes: 5 additions & 0 deletions src/core/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,11 @@ Available actions:
description:
"Single character label displayed on the marker when using addMarker.",
},
groupId: {
type: "string",
description:
"Optional grouping key. Reuse the SAME groupId across calls that should appear on ONE shared map — e.g. searching places then drawing a route for the same trip, or later updating that map. Markers accumulate, directions overlay, and the center follows the latest call. Use a NEW groupId to start a separate, unrelated map. Omit for a one-off standalone map. (Unrelated to Google Maps' own map style id.)",
},
},
required: [],
},
Expand Down
37 changes: 33 additions & 4 deletions src/core/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ export const executeMapControl = async (
args: MapArgs
): Promise<ToolResult<MapToolData, MapJsonData>> => {
const action: MapAction = args.action || DEFAULT_ACTION;
// Threaded onto every returned data shape so the host can group
// results onto one map. Undefined stays undefined (ungrouped /
// legacy single-result behaviour).
const groupId = args.groupId;

switch (action) {
case "showLocation":
Expand All @@ -47,7 +51,7 @@ export const executeMapControl = async (
);
}
const location = getLocationFromArgs(args);
const data: MapToolData = { action, location };
const data: MapToolData = { action, location, groupId };

if (action === "addMarker") {
data.marker = {
Expand Down Expand Up @@ -82,14 +86,14 @@ export const executeMapControl = async (
}
return {
message: `Setting zoom level to ${zoom}`,
data: { action, zoom },
data: { action, zoom, groupId },
};
}

case "clearMarkers": {
return {
message: "Clearing all markers from the map",
data: { action },
data: { action, groupId },
};
}

Expand All @@ -108,6 +112,7 @@ export const executeMapControl = async (
action,
searchQuery: args.searchQuery,
placeType: args.placeType,
groupId,
},
};
}
Expand All @@ -126,6 +131,7 @@ export const executeMapControl = async (
origin: args.origin,
destination: args.destination,
travelMode,
groupId,
},
};
}
Expand Down Expand Up @@ -196,13 +202,36 @@ mapControl(action: "getDirections", origin: "Tokyo Station", destination: "Tokyo
\`\`\`
Travel modes: DRIVING, WALKING, BICYCLING, TRANSIT

## Grouping operations onto one map (groupId)

By default each call renders its own map. To build ONE map from
several calls — e.g. show a city, drop several markers, then draw a
route — pass the SAME \`groupId\` on every related call. Markers
accumulate, directions overlay, and the center follows the latest
call, all on a single shared map.

\`\`\`
mapControl(action: "findPlaces", searchQuery: "ramen", location: "Shibuya", groupId: "tokyo-food-trip")
mapControl(action: "addMarker", location: "Ichiran Shibuya", groupId: "tokyo-food-trip")
mapControl(action: "getDirections", origin: "Shibuya Station", destination: "Ichiran Shibuya", travelMode: "WALKING", groupId: "tokyo-food-trip")
\`\`\`

To update that same map later (move a marker, change the route),
reuse the same \`groupId\`. To start a separate, unrelated map, use a
NEW \`groupId\`. Omit \`groupId\` for a one-off standalone map.

Pick a short, descriptive, stable id per logical map (e.g.
"tokyo-food-trip", "office-commute"). Reusing one id across
unrelated topics will pile unrelated markers onto one map.

## Response Data

The map will return JSON data with the results of each action, including:
- Current center coordinates and zoom level
- Markers on the map
- Place search results with ratings and addresses
- Route information with distance and duration`;
- Route information with distance and duration
- The \`groupId\` this result belongs to (echoed back)`;

export const pluginCore: ToolPluginCore<MapToolData, MapJsonData, MapArgs> = {
toolDefinition: TOOL_DEFINITION,
Expand Down
52 changes: 52 additions & 0 deletions src/core/samples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,56 @@ export const samples: ToolSample[] = [
travelMode: "DRIVING",
},
},

// groupId samples — click "Trip A ①→④" in order to watch markers
// and a route accumulate onto ONE map (they share groupId
// "tokyo-food-trip"). "Trip B" uses a different groupId, so in the
// grouped-maps demo it renders as a separate card instead of
// piling onto the first map.
{
name: "Trip A ①: center Shibuya",
args: {
action: "showLocation",
location: "Shibuya, Tokyo",
groupId: "tokyo-food-trip",
},
},
{
name: "Trip A ②: + marker Ichiran",
args: {
action: "addMarker",
location: "Ichiran Shibuya",
markerTitle: "Ichiran",
markerLabel: "I",
groupId: "tokyo-food-trip",
},
},
{
name: "Trip A ③: + marker Hachiko",
args: {
action: "addMarker",
location: "Hachiko Statue, Shibuya",
markerTitle: "Hachiko",
markerLabel: "H",
groupId: "tokyo-food-trip",
},
},
{
name: "Trip A ④: + walking route",
args: {
action: "getDirections",
origin: "Shibuya Station",
destination: "Ichiran Shibuya",
travelMode: "WALKING",
groupId: "tokyo-food-trip",
},
},
{
name: "Trip B: Paris (separate map)",
args: {
action: "showLocation",
location: "Eiffel Tower, Paris",
groupId: "paris-day",
},
},
];
12 changes: 12 additions & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,15 @@ export interface DirectionRoute {
polyline: string;
}

// `groupId` (shared by the three shapes below) groups related map
// operations onto ONE map: results carrying the same `groupId`
// accumulate (markers layer, directions overlay, center follows the
// latest) instead of each spawning a separate map; a new `groupId`
// starts a fresh map. The host groups results by this key and
// replays a group in order onto a single View. Undefined =
// ungrouped (legacy single-result map). NOT to be confused with
// Google Maps' own `mapId` (cloud styling id).

// Tool data returned from execute function
export interface MapToolData {
action: MapAction;
Expand All @@ -169,6 +178,7 @@ export interface MapToolData {
origin?: string | LatLng;
destination?: string | LatLng;
travelMode?: TravelMode;
groupId?: string;
}

// JSON data returned from View component to LLM
Expand All @@ -181,6 +191,7 @@ export interface MapJsonData {
places?: PlaceResult[];
route?: DirectionRoute;
error?: string;
groupId?: string;
}

// Arguments passed to the tool
Expand All @@ -197,4 +208,5 @@ export interface MapArgs {
travelMode?: TravelMode;
markerTitle?: string;
markerLabel?: string;
groupId?: string;
}
Loading
Loading