feat: add APIMart AI generation and guided Studio onboarding

This commit is contained in:
Clintchiz committed 2026-10-19 13:47:33 +05:30
1 parent 9a6a2e0a8d
commit 60467a4c05
9 files changed
+455 -2

No files matched your search

+190
View File
@@ -0,0 +1,190 @@
import type {
GenerationProvider,
GenerationRequest,
} from "@wrnexus/design-studio";
export const APIMART_MODEL = "gpt-6-luna";
export function apimartProvider(
apiKey: string,
transport: typeof fetch = fetch,
): GenerationProvider {
return {
name: "APIMart",
async generate(prompt, options = {}) {
const response = await transport(
"https://api.apimart.ai/v1/chat/completions",
{
method: "POST",
signal: options.signal,
redirect: "error",
headers: {
authorization: `Bearer ${apiKey}`,
"content-type": "application/json",
},
body: JSON.stringify({
model: APIMART_MODEL,
stream: false,
max_tokens: options.maxTokens ?? 4096,
messages: [
{
role: "system",
content:
options.system ?? "Return valid design proposal JSON only.",
},
{ role: "user", content: prompt },
],
}),
},
);
// Never forward upstream bodies: they may echo credentials or submitted context.
if (!response.ok)
throw Error(`APIMart request failed (${response.status}).`);
const raw = await readBounded(response, 200_000);
const result = JSON.parse(raw);
const choice = result.choices?.[0];
if (choice?.finish_reason === "length")
throw Error("APIMart output was truncated.");
if (
typeof choice?.message?.content !== "string" ||
!choice.message.content.trim()
)
throw Error("APIMart returned no design proposal.");
return {
value: choice.message.content,
provider: "APIMart",
model: APIMART_MODEL,
};
},
};
}
async function readBounded(source: Request | Response, limit: number) {
const reader = source.body?.getReader();
const decoder = new TextDecoder();
let text = "",
bytes = 0;
if (!reader) return text;
try {
while (true) {
const chunk = await reader.read();
if (chunk.done) break;
bytes += chunk.value.byteLength;
if (bytes > limit) {
await reader.cancel();
throw Error("Request or response is too large.");
}
text += decoder.decode(chunk.value, { stream: true });
}
return text + decoder.decode();
} finally {
reader.releaseLock();
}
}
export function studioAI(apiKey?: string, transport: typeof fetch = fetch) {
let active = 0,
windowStart = Date.now(),
count = 0;
const json = (value: unknown, status = 200) =>
new Response(JSON.stringify(value), {
status,
headers: {
"content-type": "application/json",
"cache-control": "no-store",
},
});
return async (request: Request): Promise<Response | undefined> => {
const url = new URL(request.url);
if (
!apiKey ||
![
"/design-studio/api/generation",
"/design-studio/api/generate",
].includes(url.pathname)
)
return;
const scheme = request.headers.get("x-forwarded-proto");
if (scheme === "https" || scheme === "http") url.protocol = scheme + ":";
if (
request.headers.get("origin") &&
request.headers.get("origin") !== url.origin
)
return json({ error: "Same-origin requests only." }, 403);
if (url.pathname.endsWith("/generation") && request.method === "GET")
return json({
provider: "APIMart",
model: APIMART_MODEL,
templateVersion: 1,
maxAttempts: 2,
maxOutputTokens: 4096,
timeoutMs: 20000,
});
if (!url.pathname.endsWith("/generate") || request.method !== "POST")
return json({ error: "Method not allowed." }, 405);
if (
!(request.headers.get("content-type") ?? "").startsWith(
"application/json",
)
)
return json({ error: "JSON required." }, 415);
if (Date.now() - windowStart >= 60000) {
windowStart = Date.now();
count = 0;
}
if (active >= 2 || count >= 10)
return json(
{ error: "AI generation is busy. Try again in a minute." },
429,
);
active++;
try {
const input = JSON.parse(
await readBounded(request, 2_000_000),
) as GenerationRequest;
count++;
// Preserve the installed engine's validation, locks, revision checks and repair budget.
const module = import.meta.resolve("@wrnexus/design-studio");
const { generateDesigns } = (await import(
module
)) as typeof import("@wrnexus/design-studio");
const result = await generateDesigns(input, {
provider: apimartProvider(apiKey, transport),
model: APIMART_MODEL,
signal: request.signal,
});
return json(result);
} catch (error) {
const message =
error instanceof Error ? error.message : "Invalid generation request.";
if (message.includes("WRN-STUDIO-CONFLICT"))
return json(
{
error: "Your draft changed. Generate again from the current draft.",
},
409,
);
if (message.includes("too large"))
return json({ error: "Request is too large." }, 413);
if (
request.signal.aborted ||
(error instanceof Error && error.name === "AbortError")
)
return json(
{
error:
"AI generation timed out or was cancelled. Your draft is unchanged.",
},
408,
);
return json(
{
error:
"Could not generate a design. Check the brief and server AI configuration. Your draft is unchanged.",
},
400,
);
} finally {
active--;
}
};
}
+11
View File
@@ -0,0 +1,11 @@
export const studioHelpScript = `window.addEventListener("error",function(){var s=document.getElementById("validation-state"),d=document.getElementById("diagnostics");if(s&&s.textContent==="Checking"){s.textContent="Editor could not start";if(d)d.textContent="Reload the page. If this continues, rebuild and restart the site with the latest Studio configuration. Your saved draft has not been deleted.";}});document.addEventListener("DOMContentLoaded",function(){var b=document.querySelector('[data-tab="guided"]');if(b)b.click();});`;
export function addStudioGuide(html: string, configured: boolean) {
const guide = `<details open class="help"><summary><strong>How to use Design Studio</strong></summary><ol><li><strong>Describe your website.</strong> Open Guided design, enter your product and audience, then choose colors, shapes and spacing with Next.</li><li><strong>Generate and compare.</strong> Click Generate three directions, review an alternative, then choose Use reviewed proposal.</li><li><strong>Refine it.</strong> Try “make buttons square and use blue as the primary color” under Edit with words. Lock decisions you want to keep.</li><li><strong>Use it in your app.</strong> Open Review &amp; apply and export Theme config or Compiled CSS. This website previews and exports designs; it does not change your deployed app automatically.</li></ol><p>${configured ? "APIMart AI is enabled with gpt-6-luna. Generation sends your brief and current design to APIMart only when you request it." : "AI is not configured. Local presets and supported rule-based edits still work. Set APIMART_API_KEY in the server .env and restart to enable gpt-6-luna."}</p></details>`;
return html
.replace(
'<script type="module"',
'<script src="./site-help.js"></script><script type="module"',
)
.replace('<div id="diagnostics"', guide + '<div id="diagnostics"');
}