rspace-online/lib/canvas-tools.ts

440 lines
16 KiB
TypeScript

/**
* Canvas Tool Registry — shared by server (Gemini function declarations) and client (shape spawning).
* Pure TypeScript, no DOM or server dependencies.
*/
export interface CanvasToolDefinition {
declaration: {
name: string;
description: string;
parameters: {
type: "object";
properties: Record<string, { type: string; description: string; enum?: string[] }>;
required: string[];
};
};
tagName: string;
buildProps: (args: Record<string, any>) => Record<string, any>;
actionLabel: (args: Record<string, any>) => string;
}
const registry: CanvasToolDefinition[] = [
{
declaration: {
name: "create_map",
description: "Create an interactive map centered on a location. Use when the user wants to see a place, get directions, or explore a geographic area.",
parameters: {
type: "object",
properties: {
latitude: { type: "number", description: "Latitude of the center point" },
longitude: { type: "number", description: "Longitude of the center point" },
zoom: { type: "number", description: "Zoom level (1-18, default 12)" },
location_name: { type: "string", description: "Human-readable name of the location" },
},
required: ["latitude", "longitude", "location_name"],
},
},
tagName: "folk-map",
buildProps: (args) => ({
center: [args.longitude, args.latitude],
zoom: args.zoom || 12,
}),
actionLabel: (args) => `Created map: ${args.location_name}`,
},
{
declaration: {
name: "create_note",
description: "Create a markdown note on the canvas. Use for text content, lists, summaries, instructions, or any written information.",
parameters: {
type: "object",
properties: {
content: { type: "string", description: "Markdown content for the note" },
title: { type: "string", description: "Optional title for the note" },
},
required: ["content"],
},
},
tagName: "folk-markdown",
buildProps: (args) => ({
value: args.title ? `# ${args.title}\n\n${args.content}` : args.content,
}),
actionLabel: (args) => `Created note${args.title ? `: ${args.title}` : ""}`,
},
{
declaration: {
name: "create_embed",
description: "Embed a webpage or web app on the canvas. Use for websites, search results, booking sites, videos, or any URL the user wants to view inline.",
parameters: {
type: "object",
properties: {
url: { type: "string", description: "The URL to embed" },
title: { type: "string", description: "Descriptive title for the embed" },
},
required: ["url"],
},
},
tagName: "folk-embed",
buildProps: (args) => ({
url: args.url,
}),
actionLabel: (args) => `Embedded: ${args.title || args.url}`,
},
{
declaration: {
name: "create_image",
description: "Display an image on the canvas from a URL. Use when showing an existing image, photo, diagram, or any direct image link.",
parameters: {
type: "object",
properties: {
src: { type: "string", description: "Image URL" },
alt: { type: "string", description: "Alt text describing the image" },
},
required: ["src"],
},
},
tagName: "folk-image",
buildProps: (args) => ({
src: args.src,
alt: args.alt || "",
}),
actionLabel: (args) => `Created image${args.alt ? `: ${args.alt}` : ""}`,
},
{
declaration: {
name: "create_bookmark",
description: "Create a bookmark card for a URL. Use when the user wants to save or reference a link without embedding the full page.",
parameters: {
type: "object",
properties: {
url: { type: "string", description: "The URL to bookmark" },
},
required: ["url"],
},
},
tagName: "folk-bookmark",
buildProps: (args) => ({
url: args.url,
}),
actionLabel: (args) => `Bookmarked: ${args.url}`,
},
{
declaration: {
name: "create_image_gen",
description: "Generate an AI image from a text prompt. Use when the user wants to create, generate, or imagine a new image that doesn't exist yet.",
parameters: {
type: "object",
properties: {
prompt: { type: "string", description: "Text prompt describing the image to generate" },
style: {
type: "string",
description: "Visual style for the generated image",
enum: ["photorealistic", "illustration", "painting", "sketch", "punk-zine", "collage", "vintage", "minimalist"],
},
},
required: ["prompt"],
},
},
tagName: "folk-image-gen",
buildProps: (args) => ({
prompt: args.prompt,
style: args.style || "photorealistic",
}),
actionLabel: (args) => `Generating image: ${args.prompt.slice(0, 50)}${args.prompt.length > 50 ? "..." : ""}`,
},
// ── Trip Planning Tools ──
{
declaration: {
name: "create_destination",
description: "Create a destination card for a trip location. Use when the user mentions a city, place, or stop on their trip.",
parameters: {
type: "object",
properties: {
destName: { type: "string", description: "Name of the destination (city or place)" },
country: { type: "string", description: "Country name" },
lat: { type: "number", description: "Latitude coordinate" },
lng: { type: "number", description: "Longitude coordinate" },
arrivalDate: { type: "string", description: "Arrival date in YYYY-MM-DD format" },
departureDate: { type: "string", description: "Departure date in YYYY-MM-DD format" },
notes: { type: "string", description: "Additional notes about this destination" },
},
required: ["destName"],
},
},
tagName: "folk-destination",
buildProps: (args) => ({
destName: args.destName,
...(args.country ? { country: args.country } : {}),
...(args.lat != null ? { lat: args.lat } : {}),
...(args.lng != null ? { lng: args.lng } : {}),
...(args.arrivalDate ? { arrivalDate: args.arrivalDate } : {}),
...(args.departureDate ? { departureDate: args.departureDate } : {}),
...(args.notes ? { notes: args.notes } : {}),
}),
actionLabel: (args) => `Created destination: ${args.destName}${args.country ? `, ${args.country}` : ""}`,
},
{
declaration: {
name: "create_itinerary",
description: "Create an itinerary card with a list of activities/events organized by date. Use when planning a schedule or day-by-day plan.",
parameters: {
type: "object",
properties: {
tripTitle: { type: "string", description: "Title for the itinerary" },
itemsJson: { type: "string", description: 'JSON array of items. Each: {"id":"<uuid>","title":"...","date":"YYYY-MM-DD","startTime":"HH:MM","category":"ACTIVITY|TRANSPORT|MEAL|FREE_TIME|FLIGHT"}' },
},
required: ["tripTitle", "itemsJson"],
},
},
tagName: "folk-itinerary",
buildProps: (args) => {
let items: any[] = [];
try { items = JSON.parse(args.itemsJson); } catch { items = []; }
return { tripTitle: args.tripTitle, items };
},
actionLabel: (args) => `Created itinerary: ${args.tripTitle}`,
},
{
declaration: {
name: "create_booking",
description: "Create a booking card for a flight, hotel, transport, activity, or restaurant reservation.",
parameters: {
type: "object",
properties: {
bookingType: {
type: "string",
description: "Type of booking",
enum: ["FLIGHT", "HOTEL", "CAR_RENTAL", "TRAIN", "BUS", "FERRY", "ACTIVITY", "RESTAURANT", "OTHER"],
},
provider: { type: "string", description: "Provider/company name (e.g. airline, hotel name)" },
cost: { type: "number", description: "Cost amount" },
currency: { type: "string", description: "ISO currency code (e.g. USD, EUR)" },
startDate: { type: "string", description: "Start/check-in date in YYYY-MM-DD format" },
endDate: { type: "string", description: "End/check-out date in YYYY-MM-DD format" },
bookingStatus: { type: "string", description: "Booking status", enum: ["PENDING", "CONFIRMED", "CANCELLED"] },
details: { type: "string", description: "Additional booking details or notes" },
},
required: ["bookingType", "provider"],
},
},
tagName: "folk-booking",
buildProps: (args) => ({
bookingType: args.bookingType,
provider: args.provider,
...(args.cost != null ? { cost: args.cost } : {}),
...(args.currency ? { currency: args.currency } : {}),
...(args.startDate ? { startDate: args.startDate } : {}),
...(args.endDate ? { endDate: args.endDate } : {}),
...(args.bookingStatus ? { bookingStatus: args.bookingStatus } : {}),
...(args.details ? { details: args.details } : {}),
}),
actionLabel: (args) => `Created booking: ${args.bookingType}${args.provider}`,
},
{
declaration: {
name: "create_budget",
description: "Create a budget tracker card with total budget and expense line items. Use when the user wants to track trip costs.",
parameters: {
type: "object",
properties: {
budgetTotal: { type: "number", description: "Total budget amount" },
currency: { type: "string", description: "ISO currency code (e.g. USD, EUR)" },
expensesJson: { type: "string", description: 'JSON array of expenses. Each: {"id":"<uuid>","category":"TRANSPORT|ACCOMMODATION|FOOD|ACTIVITY|SHOPPING|OTHER","description":"...","amount":123,"date":"YYYY-MM-DD"}' },
},
required: ["budgetTotal"],
},
},
tagName: "folk-budget",
buildProps: (args) => {
let expenses: any[] = [];
try { expenses = JSON.parse(args.expensesJson); } catch { expenses = []; }
return {
budgetTotal: args.budgetTotal,
...(args.currency ? { currency: args.currency } : {}),
expenses,
};
},
actionLabel: (args) => `Created budget: ${args.currency || "USD"} ${args.budgetTotal}`,
},
{
declaration: {
name: "create_packing_list",
description: "Create a packing list card with checkable items organized by category. Use when the user needs help with what to pack.",
parameters: {
type: "object",
properties: {
itemsJson: { type: "string", description: 'JSON array of packing items. Each: {"id":"<uuid>","name":"...","category":"CLOTHING|FOOTWEAR|ELECTRONICS|GEAR|PERSONAL|DOCUMENTS|SAFETY|SUPPLIES","quantity":1,"packed":false}' },
},
required: ["itemsJson"],
},
},
tagName: "folk-packing-list",
buildProps: (args) => {
let items: any[] = [];
try { items = JSON.parse(args.itemsJson); } catch { items = []; }
return { items };
},
actionLabel: (args) => {
let count = 0;
try { count = JSON.parse(args.itemsJson).length; } catch {}
return `Created packing list (${count} items)`;
},
},
];
// ── Mermaid Diagram Tool ──
registry.push({
declaration: {
name: "create_mermaid_diagram",
description: "Create a mermaid diagram on the canvas. Use when the user wants to create flowcharts, sequence diagrams, class diagrams, state diagrams, ER diagrams, Gantt charts, or any diagram that can be expressed in Mermaid syntax.",
parameters: {
type: "object",
properties: {
prompt: { type: "string", description: "Description of the diagram to generate (e.g. 'CI/CD pipeline with build, test, deploy stages')" },
},
required: ["prompt"],
},
},
tagName: "folk-mermaid-gen",
buildProps: (args) => ({
prompt: args.prompt,
}),
actionLabel: (args) => `Creating diagram: ${args.prompt.slice(0, 50)}${args.prompt.length > 50 ? "..." : ""}`,
});
// ── Social Media / Campaign Tools ──
registry.push(
{
declaration: {
name: "create_social_post",
description: "Create a social media post card for scheduling across platforms.",
parameters: {
type: "object",
properties: {
platform: { type: "string", description: "Target platform", enum: ["x", "linkedin", "instagram", "youtube", "threads", "bluesky", "tiktok", "facebook"] },
content: { type: "string", description: "Post text content" },
postType: { type: "string", description: "Format", enum: ["text", "image", "video", "carousel", "thread", "article"] },
scheduledAt: { type: "string", description: "ISO datetime to schedule" },
hashtags: { type: "string", description: "Comma-separated hashtags" },
},
required: ["platform", "content"],
},
},
tagName: "folk-social-post",
buildProps: (args) => ({
platform: args.platform || "x",
content: args.content,
postType: args.postType || "text",
scheduledAt: args.scheduledAt || "",
hashtags: args.hashtags ? args.hashtags.split(",").map((t: string) => t.trim()).filter(Boolean) : [],
status: "draft",
}),
actionLabel: (args) => `Created ${args.platform || "social"} post`,
},
{
declaration: {
name: "create_social_thread",
description: "Create a tweet thread card on the canvas. Use when the user wants to draft a multi-tweet thread.",
parameters: {
type: "object",
properties: {
title: { type: "string", description: "Thread title" },
platform: { type: "string", description: "Target platform", enum: ["x", "bluesky", "threads"] },
tweetsJson: { type: "string", description: "JSON array of tweet strings" },
status: { type: "string", description: "Thread status", enum: ["draft", "ready", "published"] },
},
required: ["title"],
},
},
tagName: "folk-social-thread",
buildProps: (args) => {
let tweets: string[] = [];
try { tweets = JSON.parse(args.tweetsJson || "[]"); } catch { tweets = []; }
return {
title: args.title,
platform: args.platform || "x",
tweets,
status: args.status || "draft",
};
},
actionLabel: (args) => `Created thread: ${args.title}`,
},
{
declaration: {
name: "create_campaign_card",
description: "Create a campaign dashboard card on the canvas. Use when the user wants to plan or track a social media campaign.",
parameters: {
type: "object",
properties: {
title: { type: "string", description: "Campaign title" },
description: { type: "string", description: "Campaign description" },
platforms: { type: "string", description: "Comma-separated platform names" },
duration: { type: "string", description: "Campaign duration (e.g. '4 weeks')" },
},
required: ["title"],
},
},
tagName: "folk-social-campaign",
buildProps: (args) => ({
title: args.title,
description: args.description || "",
platforms: args.platforms ? args.platforms.split(",").map((p: string) => p.trim().toLowerCase()).filter(Boolean) : [],
duration: args.duration || "",
}),
actionLabel: (args) => `Created campaign: ${args.title}`,
},
{
declaration: {
name: "create_newsletter_card",
description: "Create a newsletter/email campaign card on the canvas. Use when the user wants to draft or schedule an email newsletter.",
parameters: {
type: "object",
properties: {
subject: { type: "string", description: "Email subject line" },
listName: { type: "string", description: "Mailing list name" },
status: { type: "string", description: "Newsletter status", enum: ["draft", "scheduled", "sent"] },
scheduledAt: { type: "string", description: "ISO datetime to schedule" },
},
required: ["subject"],
},
},
tagName: "folk-social-newsletter",
buildProps: (args) => ({
subject: args.subject,
listName: args.listName || "",
status: args.status || "draft",
scheduledAt: args.scheduledAt || "",
}),
actionLabel: (args) => `Created newsletter: ${args.subject}`,
},
);
// ── Design Agent Tool ──
registry.push({
declaration: {
name: "create_design_agent",
description: "Open the design agent to create print layouts in Scribus. Use when the user wants to design a poster, flyer, brochure, or any print-ready document.",
parameters: {
type: "object",
properties: {
brief: { type: "string", description: "Design brief describing what to create (e.g. 'A4 event poster for Mushroom Festival with title, date, and image area')" },
},
required: ["brief"],
},
},
tagName: "folk-design-agent",
buildProps: (args) => ({ brief: args.brief || "" }),
actionLabel: (args) => `Opened design agent${args.brief ? `: ${args.brief.slice(0, 50)}` : ""}`,
});
export const CANVAS_TOOLS: CanvasToolDefinition[] = [...registry];
export const CANVAS_TOOL_DECLARATIONS = CANVAS_TOOLS.map((t) => t.declaration);
export function findTool(name: string): CanvasToolDefinition | undefined {
return CANVAS_TOOLS.find((t) => t.declaration.name === name);
}
export function registerCanvasTool(def: CanvasToolDefinition): void {
CANVAS_TOOLS.push(def);
}