diff --git a/modules/rbooks/components/folk-book-shelf.ts b/modules/rbooks/components/folk-book-shelf.ts index 0996859..32f780a 100644 --- a/modules/rbooks/components/folk-book-shelf.ts +++ b/modules/rbooks/components/folk-book-shelf.ts @@ -31,6 +31,10 @@ export class FolkBookShelf extends HTMLElement { private _books: BookData[] = []; private _filtered: BookData[] = []; private _spaceSlug = "personal"; + private get _basePath(): string { + if (window.location.hostname.endsWith('.rspace.online')) return '/rbooks'; + return `${this._basePath}`; + } private _searchTerm = ""; private _selectedTag: string | null = null; private _offlineUnsub: (() => void) | null = null; @@ -469,7 +473,7 @@ export class FolkBookShelf extends HTMLElement { ` : `
${books.map((b) => ` - +
${this.escapeHtml(b.title)} ${b.featured ? 'Featured' : ""} @@ -648,7 +652,7 @@ export class FolkBookShelf extends HTMLElement { } try { - const res = await fetch(`/${this._spaceSlug}/rbooks/api/books`, { + const res = await fetch(`${this._basePath}/api/books`, { method: "POST", headers: { Authorization: `Bearer ${token}` }, body: formData, @@ -660,7 +664,7 @@ export class FolkBookShelf extends HTMLElement { } // Navigate to the new book - window.location.href = `/${this._spaceSlug}/rbooks/read/${data.slug}`; + window.location.href = `${this._basePath}/read/${data.slug}`; } catch (e: any) { errorEl.textContent = e.message; errorEl.hidden = false; diff --git a/modules/rschedule/landing.ts b/modules/rschedule/landing.ts index 95a13de..e64d176 100644 --- a/modules/rschedule/landing.ts +++ b/modules/rschedule/landing.ts @@ -21,12 +21,12 @@ export function renderLanding(): string {
+ onclick="var s=document.querySelector('.rl-hero').closest('[data-space]')?.getAttribute('data-space');if(s&&window.__rspaceNavUrl)window.location.href=window.__rspaceNavUrl(s,'rschedule');return false;"> Open Scheduler + onclick="var s=document.querySelector('.rl-hero').closest('[data-space]')?.getAttribute('data-space');if(s&&window.__rspaceNavUrl)window.location.href=window.__rspaceNavUrl(s,'rschedule')+'/reminders';return false;"> Automation Canvas Learn More diff --git a/modules/rsocials/components/folk-campaign-wizard.ts b/modules/rsocials/components/folk-campaign-wizard.ts index b05c537..46bc374 100644 --- a/modules/rsocials/components/folk-campaign-wizard.ts +++ b/modules/rsocials/components/folk-campaign-wizard.ts @@ -128,6 +128,8 @@ export class FolkCampaignWizard extends HTMLElement { } private get basePath(): string { + const host = window.location.hostname; + if (host.endsWith('.rspace.online')) return '/rsocials'; return `/${encodeURIComponent(this._space)}/rsocials`; } diff --git a/modules/rsplat/components/folk-splat-viewer.ts b/modules/rsplat/components/folk-splat-viewer.ts index 119e7b7..26b5e86 100644 --- a/modules/rsplat/components/folk-splat-viewer.ts +++ b/modules/rsplat/components/folk-splat-viewer.ts @@ -41,6 +41,10 @@ export class FolkSplatViewer extends HTMLElement { private _mode: "gallery" | "viewer" = "gallery"; private _splats: SplatItem[] = []; private _spaceSlug = "demo"; + private get _basePath(): string { + if (window.location.hostname.endsWith('.rspace.online')) return '/rsplat'; + return `${this._basePath}`; + } private _tour: LightTourEngine | null = null; private _splatUrl = ""; private _splatTitle = ""; @@ -112,7 +116,7 @@ export class FolkSplatViewer extends HTMLElement { this._splats = Object.values(doc.items).map(s => ({ id: s.id, slug: s.slug, title: s.title, description: s.description, file_format: s.fileFormat, - file_url: s.filePath ? `/${this._spaceSlug}/rsplat/api/splats/${s.slug}/${s.slug}.${s.fileFormat}` : undefined, + file_url: s.filePath ? `${this._basePath}/api/splats/${s.slug}/${s.slug}.${s.fileFormat}` : undefined, file_size_bytes: s.fileSizeBytes, view_count: s.viewCount, contributor_name: s.contributorName ?? undefined, thumbnail_url: (s as any).thumbnailUrl ?? undefined, @@ -136,7 +140,7 @@ export class FolkSplatViewer extends HTMLElement { if (!token || this._spaceSlug === "demo") return; try { - const res = await fetch(`/${this._spaceSlug}/rsplat/api/splats/my-history`, { + const res = await fetch(`${this._basePath}/api/splats/my-history`, { headers: { Authorization: `Bearer ${token}` }, }); if (res.ok) { @@ -176,7 +180,7 @@ export class FolkSplatViewer extends HTMLElement { const isReady = status === "ready"; const isDemo = !!s.demoUrl; const tag = isReady ? (isDemo ? "div" : "a") : "div"; - const href = isReady && !isDemo ? ` href="/${this._spaceSlug}/rsplat/${s.slug}"` : ""; + const href = isReady && !isDemo ? ` href="${this._basePath}/${s.slug}"` : ""; const demoAttr = isDemo ? ` data-demo-url="${esc(s.demoUrl!)}" data-demo-title="${esc(s.title)}" data-demo-desc="${esc(s.description || "")}" role="button" tabindex="0"` : ""; const statusClass = !isReady ? ` splat-card--${status}` : ""; const demoClass = isDemo ? " splat-card--demo" : ""; @@ -399,7 +403,7 @@ export class FolkSplatViewer extends HTMLElement { try { const token = this.getAuthToken(); - const res = await fetch(`/${this._spaceSlug}/rsplat/api/splats`, { + const res = await fetch(`${this._basePath}/api/splats`, { method: "POST", headers: token ? { Authorization: `Bearer ${token}` } : {}, body: formData, @@ -427,7 +431,7 @@ export class FolkSplatViewer extends HTMLElement { const splat = await res.json() as SplatItem; status.textContent = "Uploaded!"; setTimeout(() => { - window.location.href = `/${this._spaceSlug}/rsplat/${splat.slug}`; + window.location.href = `${this._basePath}/${splat.slug}`; }, 500); } catch (e) { status.textContent = "Network error"; @@ -690,7 +694,7 @@ export class FolkSplatViewer extends HTMLElement { saveBtn.textContent = "Saving..."; try { const token = this.getAuthToken(); - const saveRes = await fetch(`/${this._spaceSlug}/rsplat/api/splats/save-generated`, { + const saveRes = await fetch(`${this._basePath}/api/splats/save-generated`, { method: "POST", headers: { "Content-Type": "application/json", @@ -709,7 +713,7 @@ export class FolkSplatViewer extends HTMLElement { saveBtn.textContent = "Saved!"; saveBtn.style.background = "#16a34a"; setTimeout(() => { - window.location.href = `/${this._spaceSlug}/rsplat/${data.slug}`; + window.location.href = `${this._basePath}/${data.slug}`; }, 1000); } else { const err = await saveRes.json().catch(() => ({ error: "Save failed" })); @@ -756,7 +760,7 @@ export class FolkSplatViewer extends HTMLElement { if (!token || !this._generatedUrl || this._spaceSlug === "demo") return; try { - const res = await fetch(`/${this._spaceSlug}/rsplat/api/splats/save-generated`, { + const res = await fetch(`${this._basePath}/api/splats/save-generated`, { method: "POST", headers: { "Content-Type": "application/json", @@ -782,12 +786,12 @@ export class FolkSplatViewer extends HTMLElement { private renderViewer() { const backEl = this._inlineViewer ? `` - : `← Gallery`; + : `← Gallery`; // Show "View in Gallery" if auto-saved, otherwise "Save" if generated let actionEl = ""; if (this._savedSlug) { - actionEl = `View in Gallery`; + actionEl = `View in Gallery`; } else if (this._generatedUrl && this._inlineViewer && this._spaceSlug !== "demo") { actionEl = ``; } @@ -925,7 +929,7 @@ export class FolkSplatViewer extends HTMLElement { return; } - const res = await fetch(`/${this._spaceSlug}/rsplat/api/splats/save-generated`, { + const res = await fetch(`${this._basePath}/api/splats/save-generated`, { method: "POST", headers: { "Content-Type": "application/json", @@ -962,7 +966,7 @@ export class FolkSplatViewer extends HTMLElement { // Replace save button with view link setTimeout(() => { - window.location.href = `/${this._spaceSlug}/rsplat/${data.slug}`; + window.location.href = `${this._basePath}/${data.slug}`; }, 800); } catch { saveBtn.textContent = "Network error"; diff --git a/modules/rsplat/mod.ts b/modules/rsplat/mod.ts index c0db301..5e31873 100644 --- a/modules/rsplat/mod.ts +++ b/modules/rsplat/mod.ts @@ -798,7 +798,8 @@ function renderViewerPage(spaceSlug: string, dataSpace: string, idOrSlug: string routes.get("/view/:id", async (c) => { const spaceSlug = c.req.param("space") || "demo"; const id = c.req.param("id"); - return c.redirect(`/${spaceSlug}/rsplat/${id}`, 301); + const isSub = (c as any).get?.("isSubdomain"); + return c.redirect(isSub ? `/rsplat/${id}` : `/${spaceSlug}/rsplat/${id}`, 301); }); // ── Seed template data ── diff --git a/modules/rtrips/components/folk-trips-planner.ts b/modules/rtrips/components/folk-trips-planner.ts index 93d037f..bc5ee06 100644 --- a/modules/rtrips/components/folk-trips-planner.ts +++ b/modules/rtrips/components/folk-trips-planner.ts @@ -1113,7 +1113,8 @@ Use real coordinates, YYYY-MM-DD dates, ISO currency codes. Be specific and prac const accepted = this._aiGeneratedItems.filter(i => i.accepted); if (accepted.length === 0) return; sessionStorage.setItem('rtrips-canvas-export', JSON.stringify(accepted.map(i => ({ type: i.type, props: i.props })))); - window.location.href = `/${this.space}/rspace#trip-import`; + const nav = (window as any).__rspaceNavUrl; + window.location.href = (nav ? nav(this.space, 'rspace') : `/${this.space}/rspace`) + '#trip-import'; } private goBack() { diff --git a/server/index.ts b/server/index.ts index f4a902f..11661d4 100644 --- a/server/index.ts +++ b/server/index.ts @@ -2064,6 +2064,93 @@ app.post("/api/prompt", async (c) => { return c.json({ error: `Unknown model: ${model}` }, 400); }); +// ── Trip AI Prompt (executes trip tools server-side) ── +app.post("/api/trips/ai-prompt", async (c) => { + const { messages, model = "gemini-flash", systemPrompt } = await c.req.json(); + if (!messages?.length) return c.json({ error: "messages required" }, 400); + + if (!GEMINI_MODELS[model]) return c.json({ error: `Unsupported model: ${model}` }, 400); + if (!GEMINI_API_KEY) return c.json({ error: "GEMINI_API_KEY not configured" }, 503); + + const { GoogleGenerativeAI } = await import("@google/generative-ai"); + const genAI = new GoogleGenerativeAI(GEMINI_API_KEY); + + // Combine canvas tools (create_destination, etc.) + trip tools (search_flights, etc.) + const { CANVAS_TOOL_DECLARATIONS } = await import("../lib/canvas-tools"); + const { TRIP_TOOL_DECLARATIONS, findTripTool } = await import("../lib/trip-ai-tools"); + const { findTool } = await import("../lib/canvas-tools"); + const allDeclarations = [...CANVAS_TOOL_DECLARATIONS, ...TRIP_TOOL_DECLARATIONS]; + + const geminiModel = genAI.getGenerativeModel({ + model: GEMINI_MODELS[model], + tools: [{ functionDeclarations: allDeclarations }], + systemInstruction: systemPrompt || "You are a travel planning assistant.", + }); + + const contents = messages.map((m: { role: string; content: string }) => ({ + role: m.role === "assistant" ? "model" : "user", + parts: [{ text: m.content }], + })); + + const OSRM_URL = process.env.OSRM_URL || "http://osrm-backend:5000"; + + try { + const toolCalls: { name: string; args: Record; label: string }[] = []; + const toolResults: Record = {}; + let loopContents = [...contents]; + + for (let turn = 0; turn < 5; turn++) { + const result = await geminiModel.generateContent({ contents: loopContents }); + const response = result.response; + const candidate = response.candidates?.[0]; + if (!candidate) break; + + const parts = candidate.content?.parts || []; + const fnCalls = parts.filter((p: any) => p.functionCall); + + if (fnCalls.length === 0) { + const text = response.text(); + return c.json({ content: text, toolCalls, toolResults }); + } + + const fnResponseParts: any[] = []; + for (const part of fnCalls) { + const fc = part.functionCall!; + const tripTool = findTripTool(fc.name); + if (tripTool) { + // Execute server-side and return real data + const data = await tripTool.execute(fc.args, { osrmUrl: OSRM_URL }); + const label = tripTool.actionLabel(fc.args); + toolCalls.push({ name: fc.name, args: fc.args, label }); + toolResults[`${fc.name}_${toolCalls.length}`] = data; + fnResponseParts.push({ + functionResponse: { name: fc.name, response: data }, + }); + } else { + // Canvas tool — acknowledge only + const canvasTool = findTool(fc.name); + const label = canvasTool?.actionLabel(fc.args) || fc.name; + toolCalls.push({ name: fc.name, args: fc.args, label }); + fnResponseParts.push({ + functionResponse: { + name: fc.name, + response: { success: true, message: `${label} — item will be created.` }, + }, + }); + } + } + + loopContents.push({ role: "model", parts }); + loopContents.push({ role: "user", parts: fnResponseParts }); + } + + return c.json({ content: "I've set up the requested items.", toolCalls, toolResults }); + } catch (e: any) { + console.error("[trips/ai-prompt] Gemini error:", e.message); + return c.json({ error: "Gemini request failed" }, 502); + } +}); + // ── Gemini image generation ── app.post("/api/gemini/image", async (c) => { if (!GEMINI_API_KEY) return c.json({ error: "GEMINI_API_KEY not configured" }, 503); diff --git a/server/output-list.ts b/server/output-list.ts index bc24164..00c140a 100644 --- a/server/output-list.ts +++ b/server/output-list.ts @@ -36,6 +36,8 @@ export function renderOutputListPage( var moduleId = ${JSON.stringify(mod.id)}; var outputPath = ${JSON.stringify(outputPath.path)}; var outputName = ${JSON.stringify(outputPath.name)}; + var isSubdomain = window.location.hostname.endsWith('.rspace.online'); + var pathPrefix = isSubdomain ? '/' + moduleId : '/' + space + '/' + moduleId; function timeAgo(dateStr) { if (!dateStr) return ''; @@ -66,7 +68,7 @@ export function renderOutputListPage( var desc = item.description || item.content_plain || ''; if (desc.length > 120) desc = desc.substring(0, 120) + '…'; var date = item.updated_at || item.created_at || ''; - var href = item.url || ('/' + space + '/' + moduleId + '?item=' + (item.id || item.slug || '')); + var href = item.url || (pathPrefix + '?item=' + (item.id || item.slug || '')); return '' + '
' + escapeText(title) + '
' + (desc ? '
' + escapeText(desc) + '
' : '') + @@ -81,7 +83,7 @@ export function renderOutputListPage( return d.innerHTML; } - fetch('/' + space + '/' + moduleId + '/api/' + outputPath) + fetch(pathPrefix + '/api/' + outputPath) .then(function(r) { if (!r.ok) throw new Error(r.status); return r.json(); diff --git a/server/shell.ts b/server/shell.ts index d50b62f..76843b7 100644 --- a/server/shell.ts +++ b/server/shell.ts @@ -1028,7 +1028,8 @@ export function renderShell(opts: ShellOptions): string { tabBar.setAttribute('active', ''); tabBar.setLayers([]); currentModuleId = ''; - history.pushState({ dashboard: true, spaceSlug: spaceSlug }, '', '/' + spaceSlug); + var dashUrl = window.location.hostname.endsWith('.rspace.online') ? '/' : '/' + spaceSlug; + history.pushState({ dashboard: true, spaceSlug: spaceSlug }, '', dashUrl); } else if (wasActive) { // Closed the active tab — switch to the nearest remaining tab const nextLayer = layers[Math.min(closedIdx, layers.length - 1)] || layers[0];