At Friends & Fables, we’re building an AI game master, so we end up having some really complex prompts. Basic javascript string manipulation can get ugly pretty quick. We've tried building a couple of different of plain JS solutions so far, but after coming across Arvid’s Prompt Design blog post, I’m in complete agreement with him that JSX is the right paradigm for prompt design.
Prompting-as-clear-communication makes prompting sound like writing. Most of the prompting I am doing, however, is parametric: I have a number of input variables, and need to dynamically adapt my prompt to those.
Initially, I was skeptical of the comparison between prompt design and web design. But after marinating on it a bit, I started to get it. A web page is basically structured data → templating language → HTML → Text that is easy for a human to read. JSX happens to be the most popular templating language to generate text that is easy for humans to read, so why not use it to generate text that is easy for LLMs to read?
So I checked out Priompt, which is a prompt-design library that uses a JSX-like syntax. It makes a lot of sense for building Cursor, but it seems to focus more on managing messages rather than constructing the strings that make up the context. It was a level higher than what I was looking for.
Here’s an example from Priompt:
<>
<NegativeExample
message={
"Sorry I could not find the snippet of code you are talking about. Can you give me the code you're talking about?"
}
></NegativeExample>
<first>
<UserMessage p={500}>{props.lastAIMessage}</UserMessage>
<UserMessage p={501}>
{props.lastAIMessage.slice(0, props.lastAIMessage.length / 2)}
</UserMessage>
</first>
<empty p={1100} tokens={10} />
</>
As you can see here, it’s more about using JSX to manage the system and user messages and how they get ordered in messages than it is about composing the strings that go inside of them.
The mental model for jsx-prompts is you’re writing jsx, but instead of rendering an HTML page, you’re rendering a string with markdown formatting.
React: Structured data → JSX → HTML
jsx-prompts (which is still just using React): Structured data → JSX → Markdown Formatted Strings
I know it sounds kind of wacky, but here’s an example:
import React from 'react';
import { Code, H1, H2, P, Pre, buildPrompt } from 'jsx-prompts';
function feedbackEmailTemplate({ interviewData }) {
return (
<>
<H1>Interview Feedback</H1>
<P>
As {interviewData.interviewer.name}, write a feedback email to{' '}
{interviewData.candidate.name} for the{' '}
{interviewData.candidate.appliedPosition} position. Some notes: about
the interiew: ${interviewData.interviewer.notes}
</P>
{interviewData.candidate.github === null && (
<P>
Note: Mention that you couldn't find their GitHub profile and suggest
they provide it.
</P>
)}
<H2>Candidate's Code Solution</H2>
<Pre>
<Code>{interviewData.solution}</Code>
</Pre>
</>
);
}
export function buildFeedbackEmailMessages({ interviewData }) {
return [
{
role: 'system',
content: 'Your job is to write an interview feedback email',
},
{
role: 'user',
content: buildPrompt(feedbackEmailTemplate({ interviewData })),
},
];
}
Here are some reasons why I really like this:
It was a little weird for me to think about using React on the server to generate strings, but it turns out it works fine! This is how you’d use the example above in a route handler.
import { openai } from '@ai-sdk/openai';
import { buildFeedbackEmailMessages } from './prompts';
import { interviewData } from './mock-data';
import { generateText } from 'ai';
import { NextResponse } from 'next/server';
const groq = createOpenAI({
baseURL: 'https://api.groq.com/openai/v1',
apiKey: process.env.GROQ_API_KEY,
});
export async function GET() {
const messages = buildFeedbackEmailMessages({ interviewData });
const { text } = await generateText({
model: openai('gpt-4o'),
messages
});
return NextResponse.json({ message: text });
}
In Priompt, it appears its mostly for building the messages array with priorities. In their example, they have a component to wrap the user message, but they are not providing any functionality to help you build the contents of the user message.
<UserMessage p={500}>{props.lastAIMessage}</UserMessage>
<UserMessage p={501}>
{props.lastAIMessage.slice(0, props.lastAIMessage.length / 2)}
</UserMessage>
In jsx-prompts, the whole point is building the contents of your messages. I think these are two different use cases, and depending on your LLM pipeline one may be more important to you than the other. For our use case, managing the data within the contents of a message is more important. For cursor, its probably more important to manage the items in the messages array. Ultimately, it might make the most sense to put all of this functionality into Priompt. It makes sense for a prompt design library to be able to manage both levels.
It honestly isn’t doing a whole lot, so I’m unsure if this should even be thought of as a “prompt design library”. It’s really just a few utils. The whole thing can be summed up by:
switch (type) {
case H1:
return `# ${children}\n\n`;
case H2:
return `## ${children}\n\n`;
import React from 'react';
export function H1({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
export function H2({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
export function H3({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
export function H4({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
export function H5({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
export function H6({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
export function P({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
export function Ul({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
export function Ol({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
export function Li({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
export function A({
href,
children,
}: { href: string; children: React.ReactNode }) {
return <>{children}</>;
}
export function Strong({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
export function Em({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
export function Blockquote({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
export function Code({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
export function Pre({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
export function renderToMarkdown(element: React.ReactNode): string {
if (typeof element === 'string' || typeof element === 'number') {
return element.toString();
}
if (React.isValidElement(element)) {
const { type, props } = element;
const children = React.Children.map(props.children, renderToMarkdown)?.join(
''
);
switch (type) {
case H1:
return `# ${children}\n\n`;
case H2:
return `## ${children}\n\n`;
case H3:
return `### ${children}\n\n`;
case H4:
return `#### ${children}\n\n`;
case H5:
return `##### ${children}\n\n`;
case H6:
return `###### ${children}\n\n`;
case P:
return `${children}\n\n`;
case Ul:
return `${children}\n`;
case Ol:
return `${children}\n`;
case Li:
return `- ${children}\n`;
case A:
return `[${children}](${props.href})`;
case Strong:
return `**${children}**`;
case Em:
return `*${children}*`;
case Blockquote:
return `> ${children}\n\n`;
case Code:
return `\`${children}\``;
case Pre:
return `\`\`\`\n${children}\n\`\`\`\n\n`;
default:
return children ?? '';
}
}
return '';
}
export function buildPrompt(prompt: JSX.Element) {
const markdownString = renderToMarkdown(prompt).trim();
return markdownString;
}
The novelty here is really just showing that it’s totally possible to use plain old react on the server to use JSX and turn complex data into nicely formatted strings. The only thing is I haven’t found any existing libraries that do exactly this, so for now I had to make my own.
In Arvid’s original post, he says
If you try it out, please let me know your thoughts! I'd love to see more ideas in the same vein, or just be told that I'm completely wrong and prompt design is stupid :).
jsx-prompts is my response to this! I think prompt design is important, but the part I was more interested in is cleanly converting structured data into a markdown string.
So reader, now I’m passing the question off to you. What do you think? Is my problem a problem that you have or am I the only one? Do you think this is overkill? Is it absolutely dumb to be using React on the server just to generate strings? Is there a much simpler solution that I haven’t thought about? Let me know on X.
Right now jsx-prompts is just an idea I’ve been toying around with and it isn’t published yet. Hopefully this post gives you enough of an idea of how it should work - I’d love to hear your thoughts on whether it should even exist!