diff --git a/lib/canvas-tools.ts b/lib/canvas-tools.ts index 449776e..0678f45 100644 --- a/lib/canvas-tools.ts +++ b/lib/canvas-tools.ts @@ -141,6 +141,144 @@ const registry: CanvasToolDefinition[] = [ }), 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":"","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":"","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":"","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)`; + }, + }, ]; export const CANVAS_TOOLS: CanvasToolDefinition[] = [...registry]; diff --git a/lib/folk-prompt.ts b/lib/folk-prompt.ts index bdb5d31..8f636f5 100644 --- a/lib/folk-prompt.ts +++ b/lib/folk-prompt.ts @@ -386,6 +386,7 @@ export class FolkPrompt extends FolkShape { #model = "gemini-flash"; #pendingImages: string[] = []; #toolsEnabled = false; + #systemPrompt = ""; #messagesEl: HTMLElement | null = null; #promptInput: HTMLTextAreaElement | null = null; @@ -399,6 +400,20 @@ export class FolkPrompt extends FolkShape { return this.#messages; } + get toolsEnabled() { return this.#toolsEnabled; } + set toolsEnabled(v: boolean) { + this.#toolsEnabled = v; + this.#toolsBtn?.classList.toggle("active", v); + if (this.#promptInput) { + this.#promptInput.placeholder = v + ? "Ask me to create maps, notes, images, or embeds on the canvas..." + : "Type your message..."; + } + } + + get systemPrompt() { return this.#systemPrompt; } + set systemPrompt(v: string) { this.#systemPrompt = v; } + override createRenderRoot() { const root = super.createRenderRoot(); @@ -471,15 +486,15 @@ export class FolkPrompt extends FolkShape { // Tools toggle this.#toolsBtn?.addEventListener("click", (e) => { e.stopPropagation(); - this.#toolsEnabled = !this.#toolsEnabled; - this.#toolsBtn!.classList.toggle("active", this.#toolsEnabled); - if (this.#promptInput) { - this.#promptInput.placeholder = this.#toolsEnabled - ? "Ask me to create maps, notes, images, or embeds on the canvas..." - : "Type your message..."; - } + this.toolsEnabled = !this.#toolsEnabled; }); + // Sync initial state after DOM ready + this.#toolsBtn?.classList.toggle("active", this.#toolsEnabled); + if (this.#toolsEnabled && this.#promptInput) { + this.#promptInput.placeholder = "Ask me to create maps, notes, images, or embeds on the canvas..."; + } + // Attach button attachBtn?.addEventListener("click", (e) => { e.stopPropagation(); @@ -635,7 +650,7 @@ export class FolkPrompt extends FolkShape { ...(m.images?.length ? { images: m.images } : {}), })), model: this.#model, - ...(useTools ? { useTools: true } : {}), + ...(useTools ? { useTools: true, systemPrompt: this.#systemPrompt || undefined } : {}), }), }); @@ -780,6 +795,8 @@ export class FolkPrompt extends FolkShape { static override fromData(data: Record): FolkPrompt { const shape = FolkShape.fromData(data) as FolkPrompt; + if (data.toolsEnabled) shape.toolsEnabled = true; + if (data.systemPrompt) shape.systemPrompt = data.systemPrompt; return shape; } @@ -788,6 +805,8 @@ export class FolkPrompt extends FolkShape { ...super.toJSON(), type: "folk-prompt", model: this.#model, + toolsEnabled: this.#toolsEnabled || undefined, + systemPrompt: this.#systemPrompt || undefined, messages: this.messages.filter((m) => m.role !== "tool-action").map((msg) => ({ role: msg.role, content: msg.content, @@ -799,5 +818,7 @@ export class FolkPrompt extends FolkShape { override applyData(data: Record): void { super.applyData(data); + if (data.toolsEnabled !== undefined) this.toolsEnabled = !!data.toolsEnabled; + if (data.systemPrompt !== undefined) this.systemPrompt = data.systemPrompt; } } diff --git a/modules/rtrips/components/folk-trips-planner.ts b/modules/rtrips/components/folk-trips-planner.ts index 6d92d1a..644c216 100644 --- a/modules/rtrips/components/folk-trips-planner.ts +++ b/modules/rtrips/components/folk-trips-planner.ts @@ -421,6 +421,8 @@ class FolkTripsPlanner extends HTMLElement { .rapp-nav__title { font-size: 15px; font-weight: 600; flex: 1; color: var(--rs-text-primary); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .rapp-nav__btn { padding: 6px 14px; border-radius: 6px; border: none; background: #14b8a6; color: #fff; font-weight: 600; cursor: pointer; font-size: 13px; } .rapp-nav__btn:hover { background: #0d9488; } + .rapp-nav__btn--ai { background: linear-gradient(135deg, #0ea5e9, #6366f1); } + .rapp-nav__btn--ai:hover { opacity: 0.9; background: linear-gradient(135deg, #0ea5e9, #6366f1); } .trip-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; } .trip-card { @@ -504,6 +506,7 @@ class FolkTripsPlanner extends HTMLElement {
My Trips +
${this.trips.length > 0 ? `
@@ -546,6 +549,7 @@ class FolkTripsPlanner extends HTMLElement {
${this._history.canGoBack ? `` : ""} ${this.esc(t.title)} + ${st.label}
@@ -678,6 +682,9 @@ class FolkTripsPlanner extends HTMLElement { private attachListeners() { this.shadow.querySelector("#btn-tour")?.addEventListener("click", () => this.startTour()); this.shadow.getElementById("create-trip")?.addEventListener("click", () => this.createTrip()); + this.shadow.getElementById("btn-plan-ai")?.addEventListener("click", () => { + window.location.href = `/${this.space}/rspace#trip-planner`; + }); this.shadow.querySelectorAll("[data-trip]").forEach(el => { el.addEventListener("click", () => { diff --git a/website/canvas.html b/website/canvas.html index a703ec8..009087b 100644 --- a/website/canvas.html +++ b/website/canvas.html @@ -3642,6 +3642,33 @@ if (hash.startsWith("#pin-")) { const pinId = hash.slice(5); setTimeout(() => pinManager.openPinById(pinId), 500); + } else if (hash === "#trip-planner") { + history.replaceState(null, "", location.pathname + location.search); + setTimeout(() => { + const TRIP_SYSTEM_PROMPT = `You are a travel planning AI in rSpace. Help plan trips by creating visual shapes on the canvas. + +When the user describes a trip, proactively create: +- Destination cards for each city/place (with coordinates and dates) +- A map showing the destinations +- Flight/accommodation search embeds (Google Flights, Airbnb, Booking.com) +- An itinerary with activities by date +- A budget tracker with estimated costs +- A packing list tailored to the destination + +Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Ask clarifying questions if needed before creating shapes.`; + + const prompt = newShape("folk-prompt", { + toolsEnabled: true, + systemPrompt: TRIP_SYSTEM_PROMPT, + }); + if (prompt) { + setTimeout(() => { + const root = prompt.shadowRoot || prompt; + const ta = root.querySelector(".prompt-input"); + if (ta) ta.focus(); + }, 200); + } + }, 300); } });