e30e75b5d4
Changesets / Create Version PR (push) Has been cancelled
Deploy Shadcn Registry / Deploy Production (push) Has been cancelled
Template Metrics / LOC + Bundle Size (push) Has been cancelled
Code Quality / Oxlint + Oxfmt (push) Has been cancelled
Code Quality / Template Sync (push) Has been cancelled
Code Quality / Build Changed Packages (push) Has been cancelled
Code Quality / Test Changed Packages (push) Has been cancelled
Deploy Expo Example / Deploy Production (push) Has been cancelled
Deploy Ink Example / Deploy Production (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-stream, 3.12) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.10) (push) Has been cancelled
Python Tests / pytest (assistant-ui-sync-server-api, 3.12) (push) Has been cancelled
366 lines
14 KiB
TypeScript
366 lines
14 KiB
TypeScript
import { getXuluxHostedTemplatesCatalog } from "@/lib/xulux/templates-catalog";
|
|
import type { XuluxTemplate } from "@/components/xulux/templates/types";
|
|
import { getDemoDownloadManifest } from "@/lib/xulux/demo-downloads/manifest";
|
|
import { fetchSandboxResource } from "@/lib/xulux/fetch-sandbox";
|
|
import {
|
|
CONFIG_ROOTS_SCHEMAS,
|
|
RULES,
|
|
TEMPLATE_LIST_META,
|
|
TOOLS_META,
|
|
fixedDemoListMeta,
|
|
} from "@/lib/xulux/template-knowledge";
|
|
import { tool, zodSchema } from "ai";
|
|
import z from "zod";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function templateId(t: XuluxTemplate): string {
|
|
return t.templateId ?? t.id;
|
|
}
|
|
|
|
function findFirstByTemplateId(
|
|
templates: XuluxTemplate[],
|
|
tid: string,
|
|
): XuluxTemplate | undefined {
|
|
return templates.find((t) => templateId(t) === tid);
|
|
}
|
|
|
|
function toAbsolute(baseUrl: string, url: string): string {
|
|
if (/^https?:\/\//.test(url)) return url;
|
|
return `${baseUrl}${url.startsWith("/") ? "" : "/"}${url}`;
|
|
}
|
|
|
|
function withVersion(url: string, versionId: string | undefined): string {
|
|
if (!versionId) return url;
|
|
const [path, query = ""] = url.split("?");
|
|
const params = new URLSearchParams(query);
|
|
if (!params.has("v")) params.set("v", versionId);
|
|
return `${path}?${params.toString()}`;
|
|
}
|
|
|
|
function hasConfig(config: Record<string, unknown> | undefined): boolean {
|
|
return !!config && Object.keys(config).length > 0;
|
|
}
|
|
|
|
function isFixedDemo(entry: XuluxTemplate): boolean {
|
|
return entry.kind === "example";
|
|
}
|
|
|
|
async function fetchTemplateContract(entry: XuluxTemplate, versionId?: string) {
|
|
if (!entry.sandboxBaseUrl) return null;
|
|
try {
|
|
const url = new URL("/api/template/contract", entry.sandboxBaseUrl);
|
|
if (versionId) url.searchParams.set("v", versionId);
|
|
const res = await fetchSandboxResource(url.toString());
|
|
if (!res.ok) return null;
|
|
return (await res.json()) as Record<string, unknown>;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Tools
|
|
// ---------------------------------------------------------------------------
|
|
|
|
export function createTemplateTools() {
|
|
return {
|
|
getTemplateList: tool({
|
|
description:
|
|
"Get all available hosted app templates and fixed demos with their features, customizable surfaces, and versions. " +
|
|
"Call this first for any app-building request. Use the features and customizable fields to decide which template fits. " +
|
|
"If customizable is empty, the entry is a fixed demo that should be shown as-is rather than configured. " +
|
|
"Call getTemplateDetails on the chosen template before opening a preview.",
|
|
inputSchema: zodSchema(z.object({})),
|
|
execute: async () => {
|
|
const { templates } = getXuluxHostedTemplatesCatalog();
|
|
const seen = new Set<string>();
|
|
const list: Array<{
|
|
id: string;
|
|
name: string;
|
|
summary: string;
|
|
assistantPlacement: string;
|
|
features: string[];
|
|
customizable: string[];
|
|
versions: Array<{ id: string; name: string; description: string }>;
|
|
}> = [];
|
|
|
|
for (const t of templates) {
|
|
const tid = templateId(t);
|
|
if (seen.has(tid)) continue;
|
|
seen.add(tid);
|
|
const meta = TEMPLATE_LIST_META[tid] ?? fixedDemoListMeta(t);
|
|
if (!meta) continue;
|
|
list.push({
|
|
id: tid,
|
|
name: meta.name,
|
|
summary: meta.summary,
|
|
assistantPlacement: meta.assistantPlacement,
|
|
features: meta.features,
|
|
customizable: meta.customizable,
|
|
versions: (t.versions ?? []).map((v) => ({
|
|
id: v.id,
|
|
name: v.title,
|
|
description: v.description,
|
|
})),
|
|
});
|
|
}
|
|
|
|
return { templates: list };
|
|
},
|
|
}),
|
|
|
|
getTemplateDetails: tool({
|
|
description:
|
|
"Get the full authoring surface for a specific template. " +
|
|
"Returns configRoots schemas (with types, defaults, and enums), rules, built-in tool input/outputShape, renderer contracts, and an exampleConfig. " +
|
|
"Fixed demos return no configRoots; open those as-is without config. " +
|
|
"Use this before calling openTemplatePreview to understand exactly what config to write. " +
|
|
"If openTemplatePreview returns validationWarnings, call this again and cross-reference configRoots to correct the config.",
|
|
inputSchema: zodSchema(
|
|
z.object({
|
|
templateId: z
|
|
.string()
|
|
.describe("The template id from getTemplateList"),
|
|
versionId: z
|
|
.string()
|
|
.optional()
|
|
.describe(
|
|
"Optional version id. When provided, exampleConfig reflects that version's resolved defaults.",
|
|
),
|
|
}),
|
|
),
|
|
execute: async ({ templateId: tid, versionId }) => {
|
|
const { templates } = getXuluxHostedTemplatesCatalog();
|
|
const entry =
|
|
(versionId
|
|
? templates.find(
|
|
(t) => templateId(t) === tid && t.versionId === versionId,
|
|
)
|
|
: undefined) ?? findFirstByTemplateId(templates, tid);
|
|
|
|
if (!entry) {
|
|
return { error: `Template "${tid}" not found.` };
|
|
}
|
|
|
|
if (isFixedDemo(entry)) {
|
|
const demo = getDemoDownloadManifest(entry.id);
|
|
const meta = fixedDemoListMeta(entry);
|
|
return {
|
|
id: tid,
|
|
name: demo?.name ?? meta?.name ?? entry.title,
|
|
selectedVersionId: null,
|
|
summary: demo?.description ?? meta?.summary ?? entry.description,
|
|
assistantPlacement:
|
|
meta?.assistantPlacement ?? "full-page demo route",
|
|
features: demo?.features ?? meta?.features ?? [],
|
|
customizable: [],
|
|
previewUrl: entry.previewUrl,
|
|
previewFrame: entry.previewFrame,
|
|
downloadUrl: entry.downloadUrl,
|
|
sourcePath: demo?.entry ?? entry.sourcePath,
|
|
tools: {
|
|
builtIn: [],
|
|
customToolSupported: false,
|
|
renderers: [],
|
|
},
|
|
rules: {
|
|
required: [
|
|
"No configRoots are returned for this entry, so it is not schema-customizable.",
|
|
"Use openTemplatePreview without config to show this demo as-is.",
|
|
"If the user needs domain-specific content or behavior changes, choose a configurable hosted template or produce a custom implementation brief instead.",
|
|
],
|
|
},
|
|
fixedDemoNote: entry.downloadUrl
|
|
? "This is a fixed assistant-ui demo. It supports preview and download, but not template config."
|
|
: "This is a fixed assistant-ui demo. It supports preview, but download is not wired for this platform yet.",
|
|
};
|
|
}
|
|
|
|
const effectiveVersionId = versionId ?? entry.versionId;
|
|
const configRootsSchema = CONFIG_ROOTS_SCHEMAS[tid];
|
|
const toolsMeta = TOOLS_META[tid];
|
|
const rules = RULES[tid] ?? [];
|
|
|
|
if (!configRootsSchema || !toolsMeta) {
|
|
return { error: `No authoring schema found for template "${tid}".` };
|
|
}
|
|
|
|
// Fetch exampleConfig from the live sandbox contract endpoint.
|
|
// Falls back to null if the sandbox is unreachable — the static schema
|
|
// above is still returned so the agent can author config.
|
|
const contract = await fetchTemplateContract(entry, effectiveVersionId);
|
|
const exampleConfig =
|
|
(contract?.exampleCompleteConfig as Record<string, unknown> | null) ??
|
|
null;
|
|
|
|
return {
|
|
id: tid,
|
|
name: TEMPLATE_LIST_META[tid]?.name ?? tid,
|
|
selectedVersionId: effectiveVersionId,
|
|
configRoots: configRootsSchema,
|
|
rules: { required: rules },
|
|
tools: toolsMeta,
|
|
exampleConfig,
|
|
exampleConfigNote: exampleConfig
|
|
? `Resolved defaults for version "${effectiveVersionId}". Use as a complete working starting point.`
|
|
: `Could not reach sandbox to resolve exampleConfig. Use configRoots schemas and defaults to author config manually.`,
|
|
};
|
|
},
|
|
}),
|
|
|
|
openTemplatePreview: tool({
|
|
description:
|
|
"Open a hosted template preview in the canvas. " +
|
|
"If you are using an existing version, you can pass versionId to select a specific variant. " +
|
|
"If you are customizing the template, you can pass config to apply customizations and generate a preview. " +
|
|
"Do not pass config for fixed demos that have no configRoots in getTemplateDetails. " +
|
|
"Returns previewUrl and downloadUrl on success. " +
|
|
"Only after a successful call, include an open-in block with the exact downloadUrl from this result. " +
|
|
"If no template fits, do not call this tool — read docs instead and use a prompt-only open-in block without downloadUrl.",
|
|
inputSchema: zodSchema(
|
|
z.object({
|
|
templateId: z
|
|
.string()
|
|
.describe("The template id from getTemplateList"),
|
|
versionId: z
|
|
.string()
|
|
.optional()
|
|
.describe(
|
|
"Version id to open. Uses the template default if omitted.",
|
|
),
|
|
config: z
|
|
.record(z.string(), z.unknown())
|
|
.optional()
|
|
.describe(
|
|
"Customization config for the preview. Must contain only the top-level keys: hostUi, assistant, and brandTheme. " +
|
|
"Use the schemas from getTemplateDetails.configRoots as the source of truth for each root. " +
|
|
"Do not pass any other root keys.",
|
|
),
|
|
}),
|
|
),
|
|
execute: async ({ templateId: tid, versionId, config }) => {
|
|
const { templates } = getXuluxHostedTemplatesCatalog();
|
|
|
|
let entry: XuluxTemplate | undefined;
|
|
if (versionId) {
|
|
entry = templates.find(
|
|
(t) => templateId(t) === tid && t.versionId === versionId,
|
|
);
|
|
}
|
|
if (!entry) entry = findFirstByTemplateId(templates, tid);
|
|
|
|
if (!entry) {
|
|
return { success: false, error: `Template "${tid}" not found.` };
|
|
}
|
|
|
|
if (isFixedDemo(entry)) {
|
|
if (hasConfig(config)) {
|
|
return {
|
|
success: false,
|
|
error: `Template "${tid}" is a fixed demo and does not support config.`,
|
|
retryHint:
|
|
"Call getTemplateDetails for this template. If no configRoots are returned, call openTemplatePreview again without config or choose a configurable hosted template.",
|
|
};
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
templateId: tid,
|
|
versionId: null,
|
|
previewUrl: entry.previewUrl ?? `/demos/${entry.id}`,
|
|
downloadUrl: entry.downloadUrl,
|
|
previewFrame: entry.previewFrame,
|
|
title: entry.title,
|
|
customized: false,
|
|
summary: `Opened ${entry.title} as a fixed demo.`,
|
|
};
|
|
}
|
|
|
|
const baseUrl = entry.sandboxBaseUrl;
|
|
if (!baseUrl) {
|
|
return {
|
|
success: false,
|
|
error: `Template "${tid}" has no sandbox URL configured.`,
|
|
};
|
|
}
|
|
|
|
if (hasConfig(config)) {
|
|
try {
|
|
const sessionUrl = new URL("/api/preview/session", baseUrl);
|
|
if (versionId) sessionUrl.searchParams.set("v", versionId);
|
|
const res = await fetchSandboxResource(sessionUrl.toString(), {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(config),
|
|
});
|
|
if (!res.ok) {
|
|
const details = await res.text();
|
|
return {
|
|
success: false,
|
|
error: `Preview session failed: HTTP ${res.status}`,
|
|
details,
|
|
retryHint:
|
|
"Check validationWarnings for the specific fields that failed. " +
|
|
"Call getTemplateDetails for this template and use configRoots schemas to correct the config. " +
|
|
"Pass only hostUi, assistant, and brandTheme at the top level.",
|
|
};
|
|
}
|
|
const data = (await res.json()) as {
|
|
previewUrl?: string;
|
|
downloadUrl?: string;
|
|
validationWarnings?: unknown[];
|
|
};
|
|
if (!data.previewUrl) {
|
|
return {
|
|
success: false,
|
|
error: "Session endpoint did not return a previewUrl.",
|
|
};
|
|
}
|
|
return {
|
|
success: true,
|
|
templateId: tid,
|
|
versionId: versionId ?? entry.versionId,
|
|
previewUrl: toAbsolute(
|
|
baseUrl,
|
|
withVersion(data.previewUrl, versionId),
|
|
),
|
|
downloadUrl: toAbsolute(
|
|
baseUrl,
|
|
withVersion(data.downloadUrl ?? "/api/download", versionId),
|
|
),
|
|
title: entry.title,
|
|
customized: true,
|
|
config,
|
|
summary: `Opened ${entry.title} with custom configuration.`,
|
|
validationWarnings: data.validationWarnings ?? [],
|
|
...(entry.previewFrame
|
|
? { previewFrame: entry.previewFrame }
|
|
: {}),
|
|
};
|
|
} catch (err) {
|
|
return {
|
|
success: false,
|
|
error: err instanceof Error ? err.message : String(err),
|
|
};
|
|
}
|
|
}
|
|
|
|
return {
|
|
success: true,
|
|
templateId: tid,
|
|
versionId: versionId ?? entry.versionId,
|
|
previewUrl: entry.previewUrl ?? baseUrl,
|
|
downloadUrl: entry.downloadUrl ?? `${baseUrl}/api/download`,
|
|
...(entry.previewFrame ? { previewFrame: entry.previewFrame } : {}),
|
|
title: entry.title,
|
|
customized: false,
|
|
summary: `Opened ${entry.title}.`,
|
|
};
|
|
},
|
|
}),
|
|
};
|
|
}
|