' : ""}
@@ -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];