fix(routing): eliminate /{space} from paths on subdomain routing
On subdomain routing (e.g. demo.rspace.online), the space slug belongs
only in the subdomain — never in the URL path. This fixes all server
and client code that was generating /{space}/module paths on subdomains.
Server fixes:
- index.ts: notification actionUrls, template/disabled-module redirects,
subdomain passthrough (now redirects HTML, rewrites API), WS notifications
- output-list.ts: subdomain-aware path prefix for hrefs and fetch URLs
- shell.ts: dashboard pushState URL
Client fixes (basePath getter pattern):
- folk-book-shelf.ts, folk-splat-viewer.ts: _basePath getter
- folk-campaign-wizard.ts: basePath subdomain check
- folk-trips-planner.ts: use __rspaceNavUrl for canvas export
- rschedule/landing.ts: onclick handlers use __rspaceNavUrl
- rsplat/mod.ts: legacy view redirect subdomain check
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
75ad3f8194
commit
193588443e
|
|
@ -31,6 +31,10 @@ export class FolkBookShelf extends HTMLElement {
|
||||||
private _books: BookData[] = [];
|
private _books: BookData[] = [];
|
||||||
private _filtered: BookData[] = [];
|
private _filtered: BookData[] = [];
|
||||||
private _spaceSlug = "personal";
|
private _spaceSlug = "personal";
|
||||||
|
private get _basePath(): string {
|
||||||
|
if (window.location.hostname.endsWith('.rspace.online')) return '/rbooks';
|
||||||
|
return `${this._basePath}`;
|
||||||
|
}
|
||||||
private _searchTerm = "";
|
private _searchTerm = "";
|
||||||
private _selectedTag: string | null = null;
|
private _selectedTag: string | null = null;
|
||||||
private _offlineUnsub: (() => void) | null = null;
|
private _offlineUnsub: (() => void) | null = null;
|
||||||
|
|
@ -469,7 +473,7 @@ export class FolkBookShelf extends HTMLElement {
|
||||||
</div>`
|
</div>`
|
||||||
: `<div class="grid">
|
: `<div class="grid">
|
||||||
${books.map((b) => `
|
${books.map((b) => `
|
||||||
<a class="book-card" data-collab-id="book:${b.id}" href="/${this._spaceSlug}/rbooks/read/${b.slug}">
|
<a class="book-card" data-collab-id="book:${b.id}" href="${this._basePath}/read/${b.slug}">
|
||||||
<div class="book-cover" style="background:${b.cover_color}">
|
<div class="book-cover" style="background:${b.cover_color}">
|
||||||
<span class="book-cover-title">${this.escapeHtml(b.title)}</span>
|
<span class="book-cover-title">${this.escapeHtml(b.title)}</span>
|
||||||
${b.featured ? '<span class="featured-badge">Featured</span>' : ""}
|
${b.featured ? '<span class="featured-badge">Featured</span>' : ""}
|
||||||
|
|
@ -648,7 +652,7 @@ export class FolkBookShelf extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/${this._spaceSlug}/rbooks/api/books`, {
|
const res = await fetch(`${this._basePath}/api/books`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
body: formData,
|
body: formData,
|
||||||
|
|
@ -660,7 +664,7 @@ export class FolkBookShelf extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to the new book
|
// 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) {
|
} catch (e: any) {
|
||||||
errorEl.textContent = e.message;
|
errorEl.textContent = e.message;
|
||||||
errorEl.hidden = false;
|
errorEl.hidden = false;
|
||||||
|
|
|
||||||
|
|
@ -21,12 +21,12 @@ export function renderLanding(): string {
|
||||||
<div class="rl-cta-row">
|
<div class="rl-cta-row">
|
||||||
<a href="#" class="rl-cta-primary" id="ml-primary"
|
<a href="#" class="rl-cta-primary" id="ml-primary"
|
||||||
style="background:linear-gradient(to right,#f59e0b,#f97316);color:#0b1120"
|
style="background:linear-gradient(to right,#f59e0b,#f97316);color:#0b1120"
|
||||||
onclick="document.querySelector('.rl-hero').closest('[data-space]')?.getAttribute('data-space') ? window.location.href='/' + document.querySelector('.rl-hero').closest('[data-space]').getAttribute('data-space') + '/rschedule' : void 0; return false;">
|
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
|
Open Scheduler
|
||||||
</a>
|
</a>
|
||||||
<a href="#" class="rl-cta-primary" id="ml-automations"
|
<a href="#" class="rl-cta-primary" id="ml-automations"
|
||||||
style="background:linear-gradient(to right,#8b5cf6,#6366f1);color:#fff"
|
style="background:linear-gradient(to right,#8b5cf6,#6366f1);color:#fff"
|
||||||
onclick="document.querySelector('.rl-hero').closest('[data-space]')?.getAttribute('data-space') ? window.location.href='/' + document.querySelector('.rl-hero').closest('[data-space]').getAttribute('data-space') + '/rschedule/reminders' : window.location.href='https://demo.rspace.online/rschedule/reminders'; return false;">
|
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
|
Automation Canvas
|
||||||
</a>
|
</a>
|
||||||
<a href="#features" class="rl-cta-secondary">Learn More</a>
|
<a href="#features" class="rl-cta-secondary">Learn More</a>
|
||||||
|
|
|
||||||
|
|
@ -128,6 +128,8 @@ export class FolkCampaignWizard extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
private get basePath(): string {
|
private get basePath(): string {
|
||||||
|
const host = window.location.hostname;
|
||||||
|
if (host.endsWith('.rspace.online')) return '/rsocials';
|
||||||
return `/${encodeURIComponent(this._space)}/rsocials`;
|
return `/${encodeURIComponent(this._space)}/rsocials`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,10 @@ export class FolkSplatViewer extends HTMLElement {
|
||||||
private _mode: "gallery" | "viewer" = "gallery";
|
private _mode: "gallery" | "viewer" = "gallery";
|
||||||
private _splats: SplatItem[] = [];
|
private _splats: SplatItem[] = [];
|
||||||
private _spaceSlug = "demo";
|
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 _tour: LightTourEngine | null = null;
|
||||||
private _splatUrl = "";
|
private _splatUrl = "";
|
||||||
private _splatTitle = "";
|
private _splatTitle = "";
|
||||||
|
|
@ -112,7 +116,7 @@ export class FolkSplatViewer extends HTMLElement {
|
||||||
this._splats = Object.values(doc.items).map(s => ({
|
this._splats = Object.values(doc.items).map(s => ({
|
||||||
id: s.id, slug: s.slug, title: s.title, description: s.description,
|
id: s.id, slug: s.slug, title: s.title, description: s.description,
|
||||||
file_format: s.fileFormat,
|
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,
|
file_size_bytes: s.fileSizeBytes,
|
||||||
view_count: s.viewCount, contributor_name: s.contributorName ?? undefined,
|
view_count: s.viewCount, contributor_name: s.contributorName ?? undefined,
|
||||||
thumbnail_url: (s as any).thumbnailUrl ?? undefined,
|
thumbnail_url: (s as any).thumbnailUrl ?? undefined,
|
||||||
|
|
@ -136,7 +140,7 @@ export class FolkSplatViewer extends HTMLElement {
|
||||||
if (!token || this._spaceSlug === "demo") return;
|
if (!token || this._spaceSlug === "demo") return;
|
||||||
|
|
||||||
try {
|
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}` },
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
|
|
@ -176,7 +180,7 @@ export class FolkSplatViewer extends HTMLElement {
|
||||||
const isReady = status === "ready";
|
const isReady = status === "ready";
|
||||||
const isDemo = !!s.demoUrl;
|
const isDemo = !!s.demoUrl;
|
||||||
const tag = isReady ? (isDemo ? "div" : "a") : "div";
|
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 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 statusClass = !isReady ? ` splat-card--${status}` : "";
|
||||||
const demoClass = isDemo ? " splat-card--demo" : "";
|
const demoClass = isDemo ? " splat-card--demo" : "";
|
||||||
|
|
@ -399,7 +403,7 @@ export class FolkSplatViewer extends HTMLElement {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const token = this.getAuthToken();
|
const token = this.getAuthToken();
|
||||||
const res = await fetch(`/${this._spaceSlug}/rsplat/api/splats`, {
|
const res = await fetch(`${this._basePath}/api/splats`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
||||||
body: formData,
|
body: formData,
|
||||||
|
|
@ -427,7 +431,7 @@ export class FolkSplatViewer extends HTMLElement {
|
||||||
const splat = await res.json() as SplatItem;
|
const splat = await res.json() as SplatItem;
|
||||||
status.textContent = "Uploaded!";
|
status.textContent = "Uploaded!";
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = `/${this._spaceSlug}/rsplat/${splat.slug}`;
|
window.location.href = `${this._basePath}/${splat.slug}`;
|
||||||
}, 500);
|
}, 500);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
status.textContent = "Network error";
|
status.textContent = "Network error";
|
||||||
|
|
@ -690,7 +694,7 @@ export class FolkSplatViewer extends HTMLElement {
|
||||||
saveBtn.textContent = "Saving...";
|
saveBtn.textContent = "Saving...";
|
||||||
try {
|
try {
|
||||||
const token = this.getAuthToken();
|
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",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
@ -709,7 +713,7 @@ export class FolkSplatViewer extends HTMLElement {
|
||||||
saveBtn.textContent = "Saved!";
|
saveBtn.textContent = "Saved!";
|
||||||
saveBtn.style.background = "#16a34a";
|
saveBtn.style.background = "#16a34a";
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = `/${this._spaceSlug}/rsplat/${data.slug}`;
|
window.location.href = `${this._basePath}/${data.slug}`;
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} else {
|
} else {
|
||||||
const err = await saveRes.json().catch(() => ({ error: "Save failed" }));
|
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;
|
if (!token || !this._generatedUrl || this._spaceSlug === "demo") return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`/${this._spaceSlug}/rsplat/api/splats/save-generated`, {
|
const res = await fetch(`${this._basePath}/api/splats/save-generated`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
@ -782,12 +786,12 @@ export class FolkSplatViewer extends HTMLElement {
|
||||||
private renderViewer() {
|
private renderViewer() {
|
||||||
const backEl = this._inlineViewer
|
const backEl = this._inlineViewer
|
||||||
? `<button class="splat-viewer__back" id="splat-back-btn">← Back to Gallery</button>`
|
? `<button class="splat-viewer__back" id="splat-back-btn">← Back to Gallery</button>`
|
||||||
: `<a class="splat-viewer__back" href="/${this._spaceSlug}/rsplat">← Gallery</a>`;
|
: `<a class="splat-viewer__back" href="${this._basePath}">← Gallery</a>`;
|
||||||
|
|
||||||
// Show "View in Gallery" if auto-saved, otherwise "Save" if generated
|
// Show "View in Gallery" if auto-saved, otherwise "Save" if generated
|
||||||
let actionEl = "";
|
let actionEl = "";
|
||||||
if (this._savedSlug) {
|
if (this._savedSlug) {
|
||||||
actionEl = `<a class="splat-viewer__save" href="/${this._spaceSlug}/rsplat/${this._savedSlug}">View in Gallery</a>`;
|
actionEl = `<a class="splat-viewer__save" href="${this._basePath}/${this._savedSlug}">View in Gallery</a>`;
|
||||||
} else if (this._generatedUrl && this._inlineViewer && this._spaceSlug !== "demo") {
|
} else if (this._generatedUrl && this._inlineViewer && this._spaceSlug !== "demo") {
|
||||||
actionEl = `<button class="splat-viewer__save" id="splat-save-btn">Save to Gallery</button>`;
|
actionEl = `<button class="splat-viewer__save" id="splat-save-btn">Save to Gallery</button>`;
|
||||||
}
|
}
|
||||||
|
|
@ -925,7 +929,7 @@ export class FolkSplatViewer extends HTMLElement {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(`/${this._spaceSlug}/rsplat/api/splats/save-generated`, {
|
const res = await fetch(`${this._basePath}/api/splats/save-generated`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
@ -962,7 +966,7 @@ export class FolkSplatViewer extends HTMLElement {
|
||||||
|
|
||||||
// Replace save button with view link
|
// Replace save button with view link
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = `/${this._spaceSlug}/rsplat/${data.slug}`;
|
window.location.href = `${this._basePath}/${data.slug}`;
|
||||||
}, 800);
|
}, 800);
|
||||||
} catch {
|
} catch {
|
||||||
saveBtn.textContent = "Network error";
|
saveBtn.textContent = "Network error";
|
||||||
|
|
|
||||||
|
|
@ -798,7 +798,8 @@ function renderViewerPage(spaceSlug: string, dataSpace: string, idOrSlug: string
|
||||||
routes.get("/view/:id", async (c) => {
|
routes.get("/view/:id", async (c) => {
|
||||||
const spaceSlug = c.req.param("space") || "demo";
|
const spaceSlug = c.req.param("space") || "demo";
|
||||||
const id = c.req.param("id");
|
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 ──
|
// ── Seed template data ──
|
||||||
|
|
|
||||||
|
|
@ -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);
|
const accepted = this._aiGeneratedItems.filter(i => i.accepted);
|
||||||
if (accepted.length === 0) return;
|
if (accepted.length === 0) return;
|
||||||
sessionStorage.setItem('rtrips-canvas-export', JSON.stringify(accepted.map(i => ({ type: i.type, props: i.props }))));
|
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() {
|
private goBack() {
|
||||||
|
|
|
||||||
|
|
@ -2064,6 +2064,93 @@ app.post("/api/prompt", async (c) => {
|
||||||
return c.json({ error: `Unknown model: ${model}` }, 400);
|
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<string, any>; label: string }[] = [];
|
||||||
|
const toolResults: Record<string, any> = {};
|
||||||
|
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 ──
|
// ── Gemini image generation ──
|
||||||
app.post("/api/gemini/image", async (c) => {
|
app.post("/api/gemini/image", async (c) => {
|
||||||
if (!GEMINI_API_KEY) return c.json({ error: "GEMINI_API_KEY not configured" }, 503);
|
if (!GEMINI_API_KEY) return c.json({ error: "GEMINI_API_KEY not configured" }, 503);
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ export function renderOutputListPage(
|
||||||
var moduleId = ${JSON.stringify(mod.id)};
|
var moduleId = ${JSON.stringify(mod.id)};
|
||||||
var outputPath = ${JSON.stringify(outputPath.path)};
|
var outputPath = ${JSON.stringify(outputPath.path)};
|
||||||
var outputName = ${JSON.stringify(outputPath.name)};
|
var outputName = ${JSON.stringify(outputPath.name)};
|
||||||
|
var isSubdomain = window.location.hostname.endsWith('.rspace.online');
|
||||||
|
var pathPrefix = isSubdomain ? '/' + moduleId : '/' + space + '/' + moduleId;
|
||||||
|
|
||||||
function timeAgo(dateStr) {
|
function timeAgo(dateStr) {
|
||||||
if (!dateStr) return '';
|
if (!dateStr) return '';
|
||||||
|
|
@ -66,7 +68,7 @@ export function renderOutputListPage(
|
||||||
var desc = item.description || item.content_plain || '';
|
var desc = item.description || item.content_plain || '';
|
||||||
if (desc.length > 120) desc = desc.substring(0, 120) + '…';
|
if (desc.length > 120) desc = desc.substring(0, 120) + '…';
|
||||||
var date = item.updated_at || item.created_at || '';
|
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 '<a class="output-card" href="' + href + '">' +
|
return '<a class="output-card" href="' + href + '">' +
|
||||||
'<div class="output-card__title">' + escapeText(title) + '</div>' +
|
'<div class="output-card__title">' + escapeText(title) + '</div>' +
|
||||||
(desc ? '<div class="output-card__desc">' + escapeText(desc) + '</div>' : '') +
|
(desc ? '<div class="output-card__desc">' + escapeText(desc) + '</div>' : '') +
|
||||||
|
|
@ -81,7 +83,7 @@ export function renderOutputListPage(
|
||||||
return d.innerHTML;
|
return d.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetch('/' + space + '/' + moduleId + '/api/' + outputPath)
|
fetch(pathPrefix + '/api/' + outputPath)
|
||||||
.then(function(r) {
|
.then(function(r) {
|
||||||
if (!r.ok) throw new Error(r.status);
|
if (!r.ok) throw new Error(r.status);
|
||||||
return r.json();
|
return r.json();
|
||||||
|
|
|
||||||
|
|
@ -1028,7 +1028,8 @@ export function renderShell(opts: ShellOptions): string {
|
||||||
tabBar.setAttribute('active', '');
|
tabBar.setAttribute('active', '');
|
||||||
tabBar.setLayers([]);
|
tabBar.setLayers([]);
|
||||||
currentModuleId = '';
|
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) {
|
} else if (wasActive) {
|
||||||
// Closed the active tab — switch to the nearest remaining tab
|
// Closed the active tab — switch to the nearest remaining tab
|
||||||
const nextLayer = layers[Math.min(closedIdx, layers.length - 1)] || layers[0];
|
const nextLayer = layers[Math.min(closedIdx, layers.length - 1)] || layers[0];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue