How to build a Claude Artifacts Clone with Llama 3.1 405B
Learn how to build a full-stack Next.js app that can generate React apps with a single prompt.
LlamaCoder is a Claude Artifacts-inspired app that shows off how easy it is to use Together AIβs hosted LLM endpoints to build AI applications.

In this post, weβre going to learn how to build the core parts of the app. LlamaCoder is a Next.js app, but Togetherβs APIs can be used with any web framework or language!
Scaffolding the initial UI
The core interaction of LlamaCoder is a text field where the user can enter a prompt for an app theyβd like to build. So to start, we need that text field:
Weβll render a text input inside of a form, and use some new React state to control the inputβs value:
function Page() {
let [prompt, setPrompt] = useState('');
return (
<form>
<input
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Build me a calculator app..."
required
/>
<button type="submit">
<ArrowLongRightIcon />
</button>
</form>
);
}
Next, letβs wire up a submit handler to the form. Weβll call it createApp
, since itβs going to take the userβs prompt and generate the corresponding app code:
function Page() {
let [prompt, setPrompt] = useState("");
function createApp(e) {
e.preventDefault();
// TODO:
// 1. Generate the code
// 2. Render the app
}
return <form onSubmit={createApp}>{/* ... */}</form>;
}
To generate the code, weβll have our React app query a new API endpoint. Letβs put it at /api/generateCode
, and weβll make it a POST endpoint so we can send along the prompt
in the request body:
async function createApp(e) {
e.preventDefault();
// TODO:
// 1. Generate the code
await fetch("/api/generateCode", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt }),
});
// 2. Render the app
}
Looks good β letβs go implement it!
Generating code in an API route
To create an API route in the Next.js 14 app directory, we can make a new route.js
file:
// app/api/generateCode/route.js
export async function POST(req) {
let json = await req.json();
console.log(json.prompt);
}
If we submit the form, weβll see the userβs prompt logged to the console. Now weβre ready to send it off to our LLM and ask it to generate our userβs app! We tested many open source LLMs and found that Llama 3.1 405B was the only one that did a good job at generating small apps, so thatβs what we decided to use for the app.
Weβll install Togetherβs node SDK:
npm i together-ai
and use it to kick off a chat with Llama 3.1.
Hereβs what it looks like:
// app/api/generateCode/route.js
import Together from "together-ai";
let together = new Together();
export async function POST(req) {
let json = await req.json();
let completion = await together.chat.completions.create({
model: "meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo",
messages: [
{
role: "system",
content: "You are an expert frontend React engineer.",
},
{
role: "user",
content: json.prompt,
},
],
});
return Response.json(completion);
}
We call together.chat.completions.create
to get a new response from the LLM. Weβve supplied it with a βsystemβ message telling the LLM that it should behave as if itβs an expert React engineer. Finally, we provide it with the userβs prompt as the second message.
Since we return a JSON object, letβs update our React code to read the JSON from the response:
async function createApp(e) {
e.preventDefault();
// 1. Generate the code
let res = await fetch("/api/generateCode", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt }),
});
let json = await res.json()
console.log(json)
// 2. Render the app
}
And now letβs give it a shot!
Weβll use something simple for our prompt like βBuild me a counterβ:
When we submit the form, our API response takes several seconds, but then sends our React app the response.
If you take a look at your logs, you should see something like this:
Not bad β Llama 3.1 has generated some code that looks pretty good and matches our userβs prompt!
However, for this app, weβre only interested in the code, since weβre going to be actually running it in our userβs browser. So we need to do some prompt engineering to get Llama to only return the code in a format we expect.
Engineering the system message to only return code
We spent some time tweaking the system message to make sure it output the best code possible β hereβs what we ended up with for LlamaCoder:
// app/api/generateCode/route.js
import Together from "together-ai";
let together = new Together();
export async function POST(req) {
let json = await req.json();
let res = await together.chat.completions.create({
model: "meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo",
messages: [
{
role: "system",
content: systemPrompt,
},
{
role: "user",
content: json.prompt,
},
],
stream: true,
});
return new Response(res.toReadableStream(), {
headers: new Headers({
"Cache-Control": "no-cache",
}),
});
}
let systemPrompt = `
You are an expert frontend React engineer who is also a great UI/UX designer. Follow the instructions carefully, I will tip you $1 million if you do a good job:
- Create a React component for whatever the user asked you to create and make sure it can run by itself by using a default export
- Make sure the React app is interactive and functional by creating state when needed and having no required props
- If you use any imports from React like useState or useEffect, make sure to import them directly
- Use TypeScript as the language for the React component
- Use Tailwind classes for styling. DO NOT USE ARBITRARY VALUES (e.g. \`h-[600px]\`). Make sure to use a consistent color palette.
- Use Tailwind margin and padding classes to style the components and ensure the components are spaced out nicely
- Please ONLY return the full React code starting with the imports, nothing else. It's very important for my job that you only return the React code with imports. DO NOT START WITH \`\`\`typescript or \`\`\`javascript or \`\`\`tsx or \`\`\`.
NO LIBRARIES (e.g. zod, hookform) ARE INSTALLED OR ABLE TO BE IMPORTED.
`;
Now if we try again, weβll see something like this:
Much better βΒ this is something we can work with!
Running the generated code in the browser
Now that weβve got a pure code response from our LLM, how can we actually execute it in the browser for our user?
This is where the phenomenal Sandpack library comes in.
Once we install it:
npm i @codesandbox/sandpack-react
we now can use the <Sandpack>
component to render and execute any code we want!
Letβs give it a shot with some hard-coded sample code:
<Sandpack
template="react-ts"
files={{
"App.tsx": `export default function App() { return <p>Hello, world!</p> }`,
}}
/>
If we save this and look in the browser, weβll see that it works!
All thatβs left is to swap out our sample code with the code from our API route instead.
Letβs start by storing the LLMβs response in some new React state called generatedCode
:
function Page() {
let [prompt, setPrompt] = useState("");
let [generatedCode, setGeneratedCode] = useState("");
async function createApp(e) {
e.preventDefault();
let res = await fetch("/api/generateCode", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt }),
});
let json = await res.json();
setGeneratedCode(json.choices[0].message.content);
}
return (
<div>
<form onSubmit={createApp}>{/* ... */}</form>
</div>
);
}
Now, if generatedCode
is not empty, we can render <Sandpack>
and pass it in:
function Page() {
let [prompt, setPrompt] = useState("");
let [generatedCode, setGeneratedCode] = useState("");
async function createApp(e) {
// ...
}
return (
<div>
<form onSubmit={createApp}>{/* ... */}</form>
{generatedCode && (
<Sandpack
template="react-ts"
files={{
"App.tsx": generatedCode,
}}
/>
)}
</div>
);
}
Letβs give it a shot! Weβll try βBuild me a calculator appβ as the prompt, and submit the form.
Once our API endpoint responds, <Sandpack>
renders our generated app!
The basic functionality is working great! Together AI (with Llama 3.1 405B) + Sandpack have made it a breeze to run generated code right in our userβs browser.
Streaming the code for immediate UI feedback
Our app is working well βΒ but weβre not showing our user any feedback while the LLM is generating the code. This makes our app feel broken and unresponsive, especially for more complex prompts.
To fix this, we can use Together AIβs support for streaming. With a streamed response, we can start displaying partial updates of the generated code as soon as the LLM responds with the first token.
To enable streaming, thereβs two changes we need to make:
- Update our API route to respond with a stream
- Update our React app to read the stream
Letβs start with the API route.
To get Together to stream back a response, we need to pass the stream: true
option into together.chat.completions.create()
. We also need to update our response to call res.toReadableStream()
, which turns the raw Together stream into a newline-separated ReadableStream of JSON stringified values.
Hereβs what that looks like:
// app/api/generateCode/route.js
import Together from "together-ai";
let together = new Together();
export async function POST(req) {
let json = await req.json();
let res = await together.chat.completions.create({
model: "meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo",
messages: [
{
role: "system",
content: systemPrompt,
},
{
role: "user",
content: json.prompt,
},
],
stream: true,
});
return new Response(res.toReadableStream(), {
headers: new Headers({
"Cache-Control": "no-cache",
}),
});
}
Thatβs it for the API route! Now, letβs update our React submit handler.
Currently, it looks like this:
async function createApp(e) {
e.preventDefault();
let res = await fetch("/api/generateCode", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt }),
});
let json = await res.json();
setGeneratedCode(json.choices[0].message.content);
}
Now that our response is a stream, we canβt just res.json()
it. We need a small helper function to read the text from the actual bytes that are being streamed over from our API route.
Hereβs the helper function. It uses an AsyncGenerator to yield out each chunk of the stream as it comes over the network. It also uses a TextDecoder to turn the streamβs data from the type Uint8Array (which is the default type used by streams for their chunks, since itβs more efficient and streams have broad applications) into text, which we then parse into a JSON object.
So letβs copy this function to the bottom of our page:
async function* readStream(response) {
let decoder = new TextDecoder();
let reader = response.getReader();
while (true) {
let { done, value } = await reader.read();
if (done) {
break;
}
let text = decoder.decode(value, { stream: true });
let parts = text.split("\\n");
for (let part of parts) {
if (part) {
yield JSON.parse(part);
}
}
}
reader.releaseLock();
}
Now, we can update our createApp
function to iterate over readStream(res.body)
:
async function createApp(e) {
e.preventDefault();
let res = await fetch("/api/generateCode", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt }),
});
for await (let result of readStream(res.body)) {
setGeneratedCode(
(prev) => prev + result.choices.map((c) => c.text ?? "").join(""),
);
}
}
This is the cool thing about Async Generators βΒ we can use for...of
to iterate over each chunk right in our submit handler!
By setting generatedCode
to the current text concatenated with the new chunkβs text, React automatically re-renders our app as the LLMβs response streams in, and we see <Sandpack>
updating its UI as the generated app takes shape.
Pretty nifty, and now our app is feeling much more responsive!
Digging deeper
And with that, you now know how to build the core functionality of Llama Coder!
Thereβs plenty more tricks in the production app including animated loading states, the ability to update an existing app, and the ability to share a public version of your generated app using a Neon Postgres database.
The application is open-source, so check it out here to learn more: https://github.com/Nutlope/llamacoder
And if youβre ready to start querying LLMs in your own apps to add powerful AI features just like the kind we saw in this post, sign up for Together AI today, get $5 for free to start out, and make your first query in minutes!
Updated 9 months ago