Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
File renamed without changes.
File renamed without changes.
16 changes: 16 additions & 0 deletions apps/web/app/(main)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Header } from "@/components/header";
import { Footer } from "@/components/footer";

export default function MainLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen flex flex-col">
<Header />
<main className="flex-1">{children}</main>
<Footer />
</div>
);
}
6 changes: 3 additions & 3 deletions apps/web/app/page.tsx → apps/web/app/(main)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ export default function Home() {
{/* Hero */}
<section className="max-w-5xl mx-auto px-6 pt-24 pb-16 text-center">
<h1 className="text-5xl sm:text-6xl md:text-7xl font-bold tracking-tighter mb-6">
Predictable. Guardrailed. Fast.
AI → json-render → UI
</h1>
<p className="text-lg text-muted-foreground max-w-2xl mx-auto mb-12 leading-relaxed">
Let users generate dashboards, widgets, apps, and data visualizations
from prompts — safely constrained to components you define.
Define a component catalog. Users prompt. AI outputs JSON constrained
to your catalog. Your components render it.
</p>

<Demo />
Expand Down
28 changes: 25 additions & 3 deletions apps/web/app/api/generate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,40 @@ EXAMPLE (Blog with responsive grid):

Generate JSONL:`;

const MAX_PROMPT_LENGTH = 140;
const MAX_PROMPT_LENGTH = 500;
const DEFAULT_MODEL = "anthropic/claude-haiku-4.5";

export async function POST(req: Request) {
const { prompt } = await req.json();
const { prompt, context } = await req.json();
const previousTree = context?.previousTree;

const sanitizedPrompt = String(prompt || "").slice(0, MAX_PROMPT_LENGTH);

// Build the user prompt, including previous tree for iteration
let userPrompt = sanitizedPrompt;
if (
previousTree &&
previousTree.root &&
Object.keys(previousTree.elements || {}).length > 0
) {
userPrompt = `CURRENT UI STATE (already loaded, DO NOT recreate existing elements):
${JSON.stringify(previousTree, null, 2)}

USER REQUEST: ${sanitizedPrompt}

IMPORTANT: The current UI is already loaded. Output ONLY the patches needed to make the requested change:
- To add a new element: {"op":"add","path":"/elements/new-key","value":{...}}
- To modify an existing element: {"op":"set","path":"/elements/existing-key","value":{...}}
- To update the root: {"op":"set","path":"/root","value":"new-root-key"}
- To add children: update the parent element with new children array

DO NOT output patches for elements that don't need to change. Only output what's necessary for the requested modification.`;
}

const result = streamText({
model: process.env.AI_GATEWAY_MODEL || DEFAULT_MODEL,
system: SYSTEM_PROMPT,
prompt: sanitizedPrompt,
prompt: userPrompt,
temperature: 0.7,
});

Expand Down
10 changes: 1 addition & 9 deletions apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
import { Header } from "@/components/header";
import { Footer } from "@/components/footer";
import { ThemeProvider } from "@/components/theme-provider";
import { Analytics } from "@vercel/analytics/next";
import { SpeedInsights } from "@vercel/speed-insights/next";
Expand Down Expand Up @@ -76,13 +74,7 @@ export default function RootLayout({
return (
<html lang="en" suppressHydrationWarning>
<body className={`${geistSans.variable} ${geistMono.variable}`}>
<ThemeProvider>
<div className="min-h-screen flex flex-col">
<Header />
<main className="flex-1">{children}</main>
<Footer />
</div>
</ThemeProvider>
<ThemeProvider>{children}</ThemeProvider>
<Analytics />
<SpeedInsights />
</body>
Expand Down
9 changes: 9 additions & 0 deletions apps/web/app/playground/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default function PlaygroundLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="h-screen flex flex-col overflow-hidden">{children}</div>
);
}
67 changes: 2 additions & 65 deletions apps/web/app/playground/page.tsx
Original file line number Diff line number Diff line change
@@ -1,72 +1,9 @@
import { Button } from "@/components/ui/button";
import { Playground } from "@/components/playground";

export const metadata = {
title: "Playground | json-render",
};

export default function PlaygroundPage() {
return (
<div className="max-w-4xl mx-auto px-6 py-16">
<h1 className="text-3xl font-bold mb-4">Playground</h1>
<p className="text-muted-foreground mb-12">
Try json-render with a live example.
</p>

<div className="space-y-12">
<section>
<h2 className="text-xl font-semibold mb-4">Run locally</h2>
<p className="text-sm text-muted-foreground mb-4">
Clone the repository and run the example dashboard.
</p>
<pre className="text-sm mb-4">
<code>{`git clone https://github.com/vercel-labs/json-render
cd json-render
pnpm install
pnpm dev`}</code>
</pre>
<p className="text-sm text-muted-foreground">
Open <code>http://localhost:3001</code> for the example dashboard.
</p>
</section>

<section>
<h2 className="text-xl font-semibold mb-4">Example prompts</h2>
<p className="text-sm text-muted-foreground mb-4">
Try these prompts in the example dashboard:
</p>
<div className="space-y-2">
{[
"Create a revenue dashboard with monthly metrics",
"Build a user management panel with a table",
"Design a settings form with text inputs",
"Make a notification center with alerts",
].map((prompt) => (
<div
key={prompt}
className="p-3 border border-border rounded text-sm font-mono"
>
{prompt}
</div>
))}
</div>
</section>

<section>
<h2 className="text-xl font-semibold mb-4">Interactive playground</h2>
<p className="text-sm text-muted-foreground mb-6">
A browser-based playground is coming soon.
</p>
<Button variant="outline" asChild>
<a
href="https://github.com/vercel-labs/json-render"
target="_blank"
rel="noopener noreferrer"
>
Star on GitHub
</a>
</Button>
</section>
</div>
</div>
);
return <Playground />;
}
84 changes: 66 additions & 18 deletions apps/web/components/demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,28 @@ type Phase = "typing" | "streaming" | "complete";
type Tab = "stream" | "json";
type RenderView = "dynamic" | "static";

export function Demo() {
const [mode, setMode] = useState<Mode>("simulation");
const [phase, setPhase] = useState<Phase>("typing");
interface DemoProps {
fullscreen?: boolean;
skipSimulation?: boolean;
}

const EXAMPLE_PROMPTS = [
"Create a login form with email and password",
"Build a feedback form with rating stars",
"Design a contact card with avatar",
"Make a settings panel with toggles",
];

export function Demo({
fullscreen = false,
skipSimulation = false,
}: DemoProps) {
const [mode, setMode] = useState<Mode>(
skipSimulation ? "interactive" : "simulation",
);
const [phase, setPhase] = useState<Phase>(
skipSimulation ? "complete" : "typing",
);
const [typedPrompt, setTypedPrompt] = useState("");
const [userPrompt, setUserPrompt] = useState("");
const [stageIndex, setStageIndex] = useState(-1);
Expand Down Expand Up @@ -909,10 +928,19 @@ Open [http://localhost:3000](http://localhost:3000) to view.
const isStreamingSimulation = mode === "simulation" && phase === "streaming";
const showLoadingDots = isStreamingSimulation || isStreaming;

const handleExampleClick = useCallback((prompt: string) => {
setMode("interactive");
setPhase("complete");
setUserPrompt(prompt);
setTimeout(() => inputRef.current?.focus(), 0);
}, []);

return (
<div className="w-full max-w-4xl mx-auto text-left">
<div
className={`w-full text-left ${fullscreen ? "h-full flex flex-col" : "max-w-5xl mx-auto"}`}
>
{/* Prompt input */}
<div className="mb-6">
<div className={fullscreen ? "mb-4" : "mb-6"}>
<div
className="border border-border rounded p-3 bg-background font-mono text-sm min-h-[44px] flex items-center justify-between cursor-text"
onClick={() => {
Expand Down Expand Up @@ -1000,16 +1028,32 @@ Open [http://localhost:3000](http://localhost:3000) to view.
</button>
)}
</div>
<div className="mt-2 text-xs text-muted-foreground text-center">
Try: &quot;Create a login form&quot; or &quot;Build a feedback form
with rating&quot;
</div>
{fullscreen ? (
<div className="mt-3 flex flex-wrap gap-2 justify-center">
{EXAMPLE_PROMPTS.map((prompt) => (
<button
key={prompt}
onClick={() => handleExampleClick(prompt)}
className="text-xs px-3 py-1.5 rounded-full border border-border text-muted-foreground hover:text-foreground hover:border-foreground/50 transition-colors"
>
{prompt}
</button>
))}
</div>
) : (
<div className="mt-2 text-xs text-muted-foreground text-center">
Try: &quot;Create a login form&quot; or &quot;Build a feedback form
with rating&quot;
</div>
)}
</div>

<div className="grid lg:grid-cols-2 gap-4">
<div
className={`grid lg:grid-cols-2 gap-4 ${fullscreen ? "flex-1 min-h-0" : ""}`}
>
{/* Tabbed code/stream/json panel */}
<div className="min-w-0">
<div className="flex items-center gap-4 mb-2 h-6">
<div className={`min-w-0 ${fullscreen ? "flex flex-col" : ""}`}>
<div className="flex items-center gap-4 mb-2 h-6 shrink-0">
{(["json", "stream"] as const).map((tab) => (
<button
key={tab}
Expand All @@ -1024,7 +1068,9 @@ Open [http://localhost:3000](http://localhost:3000) to view.
</button>
))}
</div>
<div className="border border-border rounded bg-background font-mono text-xs h-96 text-left grid relative group">
<div
className={`border border-border rounded bg-background font-mono text-xs text-left grid relative group ${fullscreen ? "flex-1 min-h-0" : "h-[28rem]"}`}
>
<div className="absolute top-2 right-2 z-10">
<CopyButton
text={
Expand All @@ -1034,7 +1080,7 @@ Open [http://localhost:3000](http://localhost:3000) to view.
/>
</div>
<div
className={`overflow-auto h-full ${activeTab === "stream" ? "" : "hidden"}`}
className={`overflow-auto ${activeTab === "stream" ? "" : "hidden"}`}
>
{streamLines.length > 0 ? (
<>
Expand All @@ -1059,7 +1105,7 @@ Open [http://localhost:3000](http://localhost:3000) to view.
)}
</div>
<div
className={`overflow-auto h-full ${activeTab === "json" ? "" : "hidden"}`}
className={`overflow-auto ${activeTab === "json" ? "" : "hidden"}`}
>
<CodeBlock
code={jsonCode}
Expand All @@ -1072,8 +1118,8 @@ Open [http://localhost:3000](http://localhost:3000) to view.
</div>

{/* Rendered output using json-render */}
<div className="min-w-0">
<div className="flex items-center justify-between mb-2 h-6">
<div className={`min-w-0 ${fullscreen ? "flex flex-col" : ""}`}>
<div className="flex items-center justify-between mb-2 h-6 shrink-0">
<div className="flex items-center gap-4">
{(
[
Expand Down Expand Up @@ -1126,7 +1172,9 @@ Open [http://localhost:3000](http://localhost:3000) to view.
</button>
</div>
</div>
<div className="border border-border rounded bg-background h-96 grid relative group">
<div
className={`border border-border rounded bg-background grid relative group ${fullscreen ? "flex-1 min-h-0" : "h-[28rem]"}`}
>
{renderView === "static" && (
<div className="absolute top-2 right-2 z-10">
<CopyButton
Expand Down
6 changes: 6 additions & 0 deletions apps/web/components/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ export function Header() {
</Link>
</div>
<nav className="flex items-center gap-4">
<Link
href="/playground"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Playground
</Link>
<Link
href="/docs"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
Expand Down
Loading