Collaborative Storytelling App Using ChatGPT: Dynamically Generate Content in a ReactNative App

 

Eduardo Chavez

Software Developer, Cat Lover, Coffee Drinker, Video game enthusiast. I enjoy creating meaningful experiences through the apps I help build.

Updated May 8, 2023

Everyone is talking about ChatGPT, the powerful AI model that generates human-like responses to natural-language inputs. In a short time, this exciting new technology has made a huge impact across industries. The possibilities for innovation are virtually limitless, from advanced chatbots to faster software development to creative content inspiration.

Now, the organization behind the technology, OpenAI, has made ChatGPT available for commercial use, allowing developers to integrate its capabilities into their apps and services. In this article, we'll explore a simple demo app that shows the power of this approach.

The idea for this app was to use ChatGPT's API as our backend. We wanted to ask ChatGPT to provide four possible titles for a children's story, and once we pick an option, create the first paragraph, then take that first paragraph and ask ChatGPT to give us more options to continue the story. Once that next option is selected, ask ChatGPT to create the next paragraph, and so on, until we have about 3 to 4 paragraphs, all generated from ChatGPT based on the user's selection.

The app is built as a single component. It renders a Dialog with various options and a ScrollView that renders paragraphs as the server generates them. You can check the details of how the app was built in the source code here.

Screen captures of the app

The Completions/Chat API

The ChatGPT API is where the “magic” happens. The OpenAI completions API has been available for a while now, but the chat/completions API recently exited beta. It is now widely available. The first step in using any of OpenAI's APIs is to choose which large-language-model (LLM) to use. From the official OpenAI documentation:

Because gpt-3.5-turbo performs at a similar capability to text-davinci-003 but at 10% the price per token, we recommend gpt-3.5-turbo for most use cases.

In this app we will be using the chat end-point. It works best for so-called "multi-shot" interactions where the user and the system provide multiple rounds of prompt and response. Here is a comparison of the base completions and the chat/completions API calls.

Old API Call: text-davinci-003

curl <https://api.openai.com/v1/completions> \\
      -H 'Content-Type: application/json' \\
      -H "Authorization: Bearer {OPEN AI TOKEN}" \\
      -d '{
        "model": "text-davinci-003",
        "prompt": "Please provide four random titles for a children story",
        "max_tokens": 1000,
        "temperature": 1
  }'

New API Call: gpt-3.5-turbo

curl <https://api.openai.com/v1/chat/completions> \\
  -H 'Content-Type: application/json' \\
  -H "Authorization: Bearer {OPEN AI TOKEN}" \\
  -d '{
    "model": "gpt-3.5-turbo",
    "messages": [
      {"role": "user","content": "Please provide four random titles for a children story"}
    ],
    "max_tokens": 1000,
    "temperature": 1
  }'

As you can see, they are similar. They both require a Bearer token, which we can get from the OpenAI developer portal. Where they differ is in the data object that is being sent. Let's focus on the data for the gpt-3.5-turbofrom now on:

  • Our modelshould be gpt-3.5-turbo.
  • The messagesarray will contain all the messages we want to send to ChatGPT. This array helps to keep track of the conversation with the AI and allows ChatGPT to be aware of the context of the current discussion. For our use case, we just want one message from a user that requests our query. You can find more information about the messages array in the official documentation.
  • As for max_tokens we would want to stick to a low number to avoid increasing the costs of each API call.
  • We will leave temperature at 1 since we want the AI to get as creative as possible.

Now if we run that API call in our terminal, we get back something like this:

{
  "id":"chatcmpl-75G7JSfz2UuzV1gRMsHaCvB2HPzIB",
  "object":"chat.completion",
  "created":1681487949,
  "model":"gpt-3.5-turbo-0301",
  "usage":{
    "prompt_tokens":17,
    "completion_tokens":34,
    "total_tokens":51
},
  "choices":[
    {
      "message":{
        "role":"assistant",
        "content":"1. The Adventures of Doodle the Dragonfly \\n2. Bella's Magical Garden \\n3. The Quest for the Lost Rainbow \\n4. The Brave Little Snail"
      },
      "finish_reason":"stop",
      "index":0
   }
  ]
}

This will be useful to help parse the response and use it in the React Native app.

Use the completions API to generate content for us dynamically.

Here's a short demo of the app:

Here's a little diagram of what we wanted to achieve:

Architectural diagram

Each API call was pretty similar. We just needed to change the content of the message in the data object that we send to the server.

One best practice from OpenAI's documentation is that each of our API calls should have a message from the system telling ChatGPT what role to take in the conversation. This is what we sent to create our first API call:

{
  "model":"gpt-3.5-turbo",
  "messages":[
    {
      "role":"system",
      "content":"You are a creative writer, that focuses on children stories"
    },
    {
      "role":"user",
      "content":"Please provide four random titles for a children story, avoid quotes and numbers, return it in JSON format"
    }
  ],
  "max_tokens":1000,
  "temperature":1
}

And this was the response from ChatGPT

{
  "id": "chatcmpl-75KM9Mzg4ZUGzyEFLN7cNE11PYrPu",
  "object": "chat.completion",
  "created": 1681504245,
  "model": "gpt-3.5-turbo-0301",
  "usage": {
    "prompt_tokens": 28,
    "completion_tokens": 54,
    "total_tokens": 82
  },
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "{\\n  \\"title_1\\": \\"The Lost Kite\\",\\n  \\"title_2\\": \\"The Magical Forest Adventure\\",\\n  \\"title_3\\": \\"The Brave Little Knight\\",\\n  \\"title_4\\": \\"The Curious Case of the Missing Treasure\\"\\n}"
      },
      "finish_reason": "stop",
      "index": 0
    }
  ]
}

Take a look a the content string. It is a JSON object as a string. We can parse as a JSON using JSON.parse(jsonString). We did that for the responses that needed to be parsed to build the options for the Dialog.tsx component. We did run into some situations where the API would return a different type of object, and our component would break like this:

Screen capture Android error

First lesson learned: if we want ChatGPT to follow a specific format, we should give it an example. We updated the message content that we sent ChatGPT with a very specific example to follow:

{
  "model":"gpt-3.5-turbo",
  "messages":[
    {
      "role":"user",
      "content":"Please provide four random titles for a children story, avoid quotes and numbers, return it in JSON format { \\"option1\\": \\"title1\\", \\"option2\\": \\"title2\\" }"
    }
  ],
  "max_tokens":1000,
  "temperature":1
}

And that was enough to ensure that ChatGPT would always return the correct format whenever we wanted an object with different options that we could use for our Dialog.tsx component.

The other types of messages that we sent to ChatGPT were easier to work with. We just needed to get the content from the paragraph that it would generate to continue our story. This is what our helper function to create the ChatGPT messages looked like:

export function createQuery(step: ChatGPTApiStorySteps, query: string | null = null): string {
  switch (step) {
    case ChatGPTApiStorySteps.PROVIDE_TITLE:
        return `Please provide four random titles for a children story, avoid quotes and numbers, return it in JSON format, here is an example: { "option1": "title1", "option2": "title2" }`
    case ChatGPTApiStorySteps.BUILD_INITIAL_PARAGRAPH:
        return `Create a two line intro for: ${query ?? ''}`
    case ChatGPTApiStorySteps.PROVIDE_OPTIONS_TO_CONTINUE:
        return `Give me four ideas on how to continue this story:  "${query ?? ''}" , please make them a very short sentence (10 words maximum) and return it in a JSON format, here is an example: { "option1": "title1", "option2": "title2" }`
    case ChatGPTApiStorySteps.BUILD_NEXT_PARAGRAPH:
        return `Please make a short paragraph (3 lines maximum) and continue this story: ${query ?? ''}`
    case ChatGPTApiStorySteps.BUILD_ENDING:
        return `Please create a short ending for this story: ${query ?? ''}`
    case ChatGPTApiStorySteps.REWRITE_CURRENT_PARAGRAPH:
        return `Please rewrite this paragraph: ${query ?? ''}`
  }
}

As you can see, whenever we needed options, the format and requirements got a bit more specific, but whenever we needed text, we just required that it was a short paragraph.

Then it was just a matter of having our mobile app be aware of the current state of the story to know when to ask for options and when to ask the API to complete a paragraph.

Where to go from here?

This is just a small example of what you can build using the power of ChatGPT's API. Whether we want to get a suggestion from a message or a more complex data model in a JSON format, the key is to feed ChatGPT with input and specify our desired output.

At present, this is not a perfect backend service (JSON responses as strings are a big NO!). However, it is already quite useful for quick prototypes. You could also have your backend server handle the ChatGPT connectivity and clean the responses there so that our mobile or web clients get a standardized API response.

As the sophistication of ChatGPT models and APIs grows, we look forward to exploring more avenues of innovation. We expect that generative AI will start showing up in all kinds of apps, so it's never too early to start becoming familiar with the relevant tools.

How can we help?

Can we help you apply these ideas on your project? Send us a message! You'll get to talk with our awesome delivery team on your very first call.