Friends & Fables
  • WorldsPricingPatch Notes
Friends & Fables

The ultimate AI RPG platform for creators and adventurers

DiscordRedditTikTokYouTube

Product

  • Worlds
  • Pricing
  • Patch Notes
  • Roadmap

Resources

  • Documentation
  • Community
  • Tools
  • SRD Library

Company

  • About
  • Blog
  • Press Kit
  • Contact Us
© 2025 Friends & Fables
Privacy PolicyTerms of ServiceAcknowledgements

This work includes material taken from the System Reference Document 5.1 ("SRD 5.1") by Wizards of the Coast LLC. The SRD 5.1 is licensed under the Creative Commons Attribution 4.0 International License.

We are not affiliated with Dungeons & Dragons or Wizards of The Coast in any way.

Introducing jsx-prompts: Build complex prompts for LLMs with JSX

9/29/2024By William Liu
Introducing jsx-prompts: Build complex prompts for LLMs with JSX
Development

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.

Introducing jsx-prompts

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:

  1. Conditionally rendering parts of the prompt is quite easy
    {interviewData.candidate.github === null && (
      <P>
        Note: Mention that you couldn't find their GitHub profile and suggest
        they provide it.
      </P>
    )}
    
  2. Clean formatting. This is really all jsx-prompts is doing. It’s basically an extremely lightweight version of MDX.
    switch (type) {
      case H1:
        return `# ${children}\n\n`;
      case H2:
        return `## ${children}\n\n`;
  3. Since this is just JSX, you can use all of the patterns and methods you are already used to using in React.
    function reactPromptTemplate(data: any[]) {
      if (data.length === 0) {
        return (
          <P>No data was provided. Tell the user they need to provide data.</P>
        );
      }
    
      return (
        <React.Fragment>
          <H1>Vegetable List</H1>
          <Ol>
            {data
              .filter((item) => item.type === 'vegetable')
              .map((item, index) => (
                <Li key={index}>{item}</Li>
              ))}
          </Ol>
          <H1>Fruit List</H1>
          <Ol>
            {data
              .filter((item) => item.type === 'fruit')
              .map((item, index) => (
                <Li key={index}>{item}</Li>
              ))}
          </Ol>
        </React.Fragment>
      );
    }
    
    const reactPrompt = buildPrompt(reactPromptTemplate(data));
    

Usage in Next.js API Route

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 });
}

So, how is it different from Priompt?

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.

Should this even be a library?

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`;
Full code here
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.

What do you think?

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!