Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/playground/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ const nextConfig: NextConfig = {
"date-fns",
],
},
serverExternalPackages: [
"@resvg/resvg-js",
"@react-pdf/renderer",
"@json-render/react-pdf",
"@json-render/image",
],
webpack: (config, { isServer }) => {
if (isServer) {
config.devtool = "source-map";
Expand Down
8 changes: 7 additions & 1 deletion apps/playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
"@ai-sdk/react": "3.0.29",
"@better-auth/passkey": "1.6.9",
"@hookform/resolvers": "5.2.2",
"@json-render/core": "0.11.0",
"@json-render/react": "0.11.0",
"@json-render/shadcn": "0.11.0",
"@llmgateway/ai-sdk-provider": "3.8.0",
"@llmgateway/models": "workspace:*",
"@llmgateway/shared": "workspace:*",
Expand Down Expand Up @@ -58,6 +61,8 @@
"cmdk": "^1.1.1",
"date-fns": "4.1.0",
"embla-carousel-react": "8.6.0",
"html-to-image": "^1.11.13",
"jspdf": "^2.5.2",
"lucide-react": "0.548.0",
"motion": "12.23.24",
"nanoid": "5.1.6",
Expand All @@ -74,6 +79,7 @@
"react-medium-image-zoom": "5.4.0",
"react-syntax-highlighter": "16.1.0",
"react-window": "2.2.7",
"recharts": "2.15.4",
"shiki": "3.22.0",
"sonner": "^2.0.7",
"streamdown": "2.1.0",
Expand All @@ -95,6 +101,6 @@
"tailwindcss": "^4.1.16",
"tw-animate-css": "1.4.0",
"typescript": "^5.9.3",
"zod": "3.25.76"
"zod": "4.3.6"
}
}
71 changes: 71 additions & 0 deletions apps/playground/src/app/api/canvas/generate/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { streamText } from "ai";
import { cookies } from "next/headers";

import { catalog } from "@/lib/canvas/catalog";
import { getUser } from "@/lib/getUser";

import { createLLMGateway } from "@llmgateway/ai-sdk-provider";

export const maxDuration = 300;

interface CanvasGenerateBody {
prompt: string;
model?: string;
}

export async function POST(req: Request) {
const user = await getUser();

if (!user) {
return new Response(JSON.stringify({ error: "Unauthorized" }), {
status: 401,
});
}

const body = (await req.json()) as CanvasGenerateBody;
const { prompt, model } = body;
const systemPrompt = catalog.prompt();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

if (!prompt) {
return new Response(JSON.stringify({ error: "Missing prompt" }), {
status: 400,
});
}

const cookieStore = await cookies();
const cookieApiKey =
cookieStore.get("llmgateway_playground_key")?.value ??
cookieStore.get("__Host-llmgateway_playground_key")?.value;

if (!cookieApiKey) {
return new Response(JSON.stringify({ error: "Missing API key" }), {
status: 400,
});
}

const gatewayUrl =
process.env.GATEWAY_URL ??
(process.env.NODE_ENV === "development"
? "http://localhost:4001/v1"
: "https://api.llmgateway.io/v1");

const llmgateway = createLLMGateway({
apiKey: cookieApiKey,
baseURL: gatewayUrl,
headers: {
"x-source": "chat.llmgateway.io",
},
});

const selectedModel = model ?? "anthropic/claude-sonnet-4-20250514";

const result = streamText({
model: llmgateway.chat(
selectedModel as Parameters<typeof llmgateway.chat>[0],
),
system: systemPrompt,
messages: [{ role: "user", content: prompt }],
});

return result.toTextStreamResponse();
}
Comment thread
amelitoalcuitas marked this conversation as resolved.
Resolved
121 changes: 121 additions & 0 deletions apps/playground/src/app/canvas/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { cookies } from "next/headers";
import { notFound } from "next/navigation";

import { LastUsedProjectTracker } from "@/components/last-used-project-tracker";
import CanvasPageClient from "@/components/playground/canvas-page-client";
import { fetchModels, fetchProviders } from "@/lib/fetch-models";
import { fetchServerData } from "@/lib/server-api";

import type { Project, Organization } from "@/lib/types";
import type { Metadata } from "next";

export const metadata: Metadata = {
title: "Canvas | LLM Gateway",
description:
"Build UIs from JSON specs with live preview, PDF and image export.",
};

export default async function CanvasPage({
searchParams,
}: {
searchParams: Promise<{ orgId: string; projectId: string }>;
}) {
const { orgId, projectId } = await searchParams;

const [models, providers] = await Promise.all([
fetchModels(),
fetchProviders(),
]);

const initialOrganizationsData = await fetchServerData("GET", "/orgs");

let initialProjectsData: { projects: Project[] } | null = null;
if (orgId) {
try {
initialProjectsData = (await fetchServerData(
"GET",
"/orgs/{id}/projects",
{
params: {
path: {
id: orgId,
},
},
},
)) as { projects: Project[] };
} catch (error) {
console.warn("Failed to fetch projects for organization:", orgId, error);
}
}

const organizations = (
initialOrganizationsData &&
typeof initialOrganizationsData === "object" &&
"organizations" in initialOrganizationsData
? (initialOrganizationsData as { organizations: Organization[] })
.organizations
: []
) as Organization[];
const selectedOrganization =
(orgId ? organizations.find((o) => o.id === orgId) : organizations[0]) ??
null;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

if (!initialProjectsData && selectedOrganization?.id) {
try {
initialProjectsData = (await fetchServerData(
"GET",
"/orgs/{id}/projects",
{
params: {
path: {
id: selectedOrganization.id,
},
},
},
)) as { projects: Project[] };
} catch (error) {
console.warn(
"Failed to fetch projects for organization:",
selectedOrganization?.id,
error,
);
}
}

const projects = (initialProjectsData?.projects ?? []) as Project[];

let selectedProject: Project | null = null;
if (projectId) {
selectedProject = projects.find((p) => p.id === projectId) ?? null;
if (!selectedProject) {
notFound();
}
} else if (selectedOrganization?.id) {
const cookieStore = await cookies();
const cookieName = `llmgateway-last-used-project-${selectedOrganization.id}`;
const lastUsed = cookieStore.get(cookieName)?.value;
if (lastUsed) {
selectedProject = projects.find((p) => p.id === lastUsed) ?? null;
}
}
selectedProject ??= projects[0] ?? null;

return (
<>
{selectedOrganization?.id && selectedProject?.id ? (
<LastUsedProjectTracker
orgId={selectedOrganization.id}
projectId={selectedProject.id}
/>
) : null}
<CanvasPageClient
models={models}
providers={providers}
organizations={organizations}
selectedOrganization={selectedOrganization}
projects={projects}
selectedProject={selectedProject}
/>
</>
);
}
Loading
Loading