Integrate ChatGPT in Flutter

Integrate ChatGPT in Flutter

We will cover briefly:

  1. Creating a ChatGPT server locally
  2. Creating ChatGPT UI
  3. Embed ChatGPT UI in Flutter

Creating a ChatGPT server locally

To create our local host server, we’ll utilize NodeJS. We create a folder called server and run

npm init -y

This creates a package.json file in the server Next, we install the dependencies like

npm install @openai/api express cors body-parser

After that, we create a file called server.js and start writing our server-side code in it. But first, before we start programming

we require the OpenAI API key.

Create an account on the OpenAI website by clicking here. Next, we go to View API Keys

OpenAI Website
OpenAI Website

Here, you create your secret key, and that’s it!!!!!

Coming back to the codebase, we first import the dependencies using

import express from 'express'
import * as dotenv from 'dotenv'
import cors from 'cors'
import { Configuration, OpenAIApi } from 'openai'

dotenv.config()

Next, we generate a file called .env and within it, we create a variable named OPENAI_API_KEY with the secretKey obtained from the OpenAI keys as its value.

Now, we create a configuration object using the OpenAI package

const configuration = new Configuration({
  apiKey: process.env.OPENAI_API_KEY,
});

const openai = new OpenAIApi(configuration);

const app = express()
app.use(cors())
app.use(express.json())

app.post('/', async (req, res) => {
  try {
    const prompt = req.body.prompt;

    const response = await openai.createCompletion({
      model: "text-davinci-003",
      prompt: `${prompt}`,
      temperature: 0, 
      max_tokens: 3000, 
      top_p: 1, 
      frequency_penalty: 0.5,
      presence_penalty: 0, 
    });

    res.status(200).send({
      bot: response.data.choices[0].text
    });

  } catch (error) {
    console.error(error)
    res.status(500).send(error || 'Something went wrong');
  }
})

app.listen(5001, () => console.log('http://localhost:5001'))

The configuration settings for sending API calls to the OpenAI API are stored and managed by the Configuration object. For making successful API requests, it comprises parameters like the API key, endpoint URL, and request timeouts. We specify our apiKey which we got from the Open AI website

Integrate ChatGPT in Flutter
Integrate ChatGPT in Flutter

The OpenAIApi the object is a class in the OpenAI Node.js library that provides methods for interacting with the OpenAI API. It is used to authenticate the API key, configure the API request, and make API calls to the OpenAI servers

Next, we set up the express server and add the required middleware. There are various models available from OpenAI, but we choose text-davinci-003 them for our case.

A variation of OpenAI’s GPT (Generative Pre-trained Transformer) language model is the text-davinci-003 model. It is specifically trained for natural language generation tasks and has a large capacity to understand and produce human-like text. The model has been optimized for a variety of tasks, including text completion, summarization, question-answering, and more. It was trained on a vast amount of diverse text data.

OpenAI Models
OpenAI Models

Next, we create a POSTroute for ChatGPT called / and send the response back to the client.

await openai.createCompletion({
  model: "text-davinci-003",
  prompt: `${prompt}`,
  temperature: 0, 
  max_tokens: 3000, 
  top_p: 1, 
  frequency_penalty: 0.5,
  presence_penalty: 0, 
});

This snippet is using the OpenAI API to generate text completions based on a given prompt. It creates a completion by sending a request to the OpenAI API with the following parameters:

  • model: OpenAI model to use text-davinci-003 in our case
  • prompt: text prompt to generate the completion.
  • temperature: between 0 and 1. Higher values mean that the model will take more risks in generating the text.
  • max_tokens: maximum number of tokens (words or punctuation marks) that the model can generate.
  • top_p: selects the most likely tokens until a certain threshold probability is reached.
  • frequency_penalty: penalty value between -2.0 and 2.0 that penalizes the model for generating tokens already appeared in the generated text
  • presence_penalty: penalty value between -2.0 and 2.0 that penalizes the model for generating tokens similar to the text in the prompt.

We then send the JSON response inside a property called “bot” whose value is extracted from the “text” property of the first element in the “choices” array of the “response” data object.

We listen to the incoming requests on the port 5001 Finally, we start the server by running the following command in the terminal

# LISTEN TO THE PORT
app.listen(5001, () => console.log('http://localhost:5001'))

# TO RUN THE SERVER
npm run server

And if we try to curl the URL above with a sample request, we get the following

# SEND REQUEST TO LOCALHOST
curl -X \
 POST "http://localhost:5001" -H \
 "accept: application/json" -H \
 "Content-Type: application/json" -d "{\"prompt\" : \"Hello\"}"

We get the following:

Response from the localhost server
Response from the localhost server

Creating ChatGPT UI

We start by creating a Flutter Web project

Pre-Requisite

  • We should be using the flutter master channel
  • The Dart version should be 3.0.0 or above

Let’s create a project using

flutter create chatgpt_embedding --platforms web

Note: We specify the platform to be web meaning the project only supports web

By default, Flutter gives us the counter app.

We include the js package in our project to enable seamless interoperability between JavaScript and Dart code. Any function in your Dart code can be annotated with a @JSExportproperty using js, allowing you to invoke it from JavaScript code.

flutter pub add js

Changes to the Counter App

Our project has only 2 dart files as shown below

Dart Files
Dart Files

We refactor the main.dart and remove all the counter logic. Next, we create a file called gpt.dart This contains the MyHomePage which is a stateful widget and _MyHomePageState is the state class for the MyHomePage widget.

class MyHomePage extends StatefulWidget {
  
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

@js.JSExport()
class _MyHomePageState extends State<MyHomePage> {}

Next, we import the package js and js_util as

import 'package:js/js.dart' as js;
import 'package:js/js_util.dart' as js_util;

The _MyHomePageState is annotated with the @JSExportattribute, since we need to pass the user, queries to the JavaScript side. This makes the Dart object _MyHomePageState as exportable

Inside our initState we call the createDartExport for the _MyHomePageState which creates a JS object literal that forwards to our exported Dart class (which in our case is _MyHomePageState)

The createDartExport() method is used to create a JavaScript object that has a reference to a Dart object, which can then be accessed from JavaScript code. The setProperty() and callMethod() methods are used to set up JavaScript properties and call JavaScript functions, respectively

void initState() {
  super.initState();
  final export = js_util.createDartExport(this);
  
  // These two are used inside the [js/js-interop.js]
  js_util.setProperty(js_util.globalThis, '_appState', export);
  js_util.callMethod<void>(js_util.globalThis, '_stateSet', []);
}

A JsObject instance export is created by calling the createDartExport() method provided by the js_util library. This creates a JavaScript object that has a reference to the Dart object that is being exported.

Two JavaScript properties are set using the setProperty() method provided by js_util. The first property _appState, which is set to the export an object created in the previous step. This property can be accessed from JavaScript code to interact with the Dart object.

The second property_stateSet, which is a JavaScript function that is defined in another file (js-interop.js). This function is called using the callMethod() method provided by js_util. The empty array [] passed as the second argument indicates that no arguments are being passed to the function.

Note: _appState and _stateSet are present inside the js file, which we will discuss later.

We create a TextEditingController and FloatingActionButton inside our gpt.dart Basically, this will take the user queries and pass them onto the ChatGPT UI. There is a function that gets invoked when the user finishes the query or when the user presses the FAB

// This stores the user query
String _textQuery = '';

void textInputCallback(String value) {
   textFocusNode.requestFocus();
   setState(() {
     _textQuery = value;
     // This line makes sure the handler gets invoked
     _streamController.add(null);
   });
}
<script src="js/js-interop.js" defer></script>

The src the attribute specifies the URL of the JavaScript file to be loaded, in this case js-interop.js.

JavaScript File
JavaScript File

All the interop with Dart is present inside this file. We make use of IIFE to create a function

(function () {
  window._stateSet = function () {
    console.log('HELLO From Flutter!!')
  };
}());

This code sets the _stateSet function as a property of the global window object. Next, we access the _appState and save it inside a variable inside the JS

let appState = window._appState;

Integrate ChatGPT in Flutter

Integrate ChatGPT in Flutter

We will cover briefly:

  1. Creating a ChatGPT server locally
  2. Creating ChatGPT UI
  3. Embed ChatGPT UI in Flutter

https://flatteredwithflutter.com/media/30de1f56141c190bff4987a02af1ede8Integrate ChatGPT in Flutter

Creating a ChatGPT server locally

To create our local host server, we’ll utilize NodeJS. We create a folder called server and run

npm init -y

This creates a package.json file in the server Next, we install the dependencies like

npm install @openai/api express cors body-parser

After that, we create a file called server.js and start writing our server-side code in it. But first, before we start programming

we require the OpenAI API key.

Create an account on the OpenAI website by clicking here. Next, we go to View API Keys

OpenAI Website

Here, you create your secret key, and that’s it!!!!!

Coming back to the codebase, we first import the dependencies using

import express from 'express'
import * as dotenv from 'dotenv'
import cors from 'cors'
import { Configuration, OpenAIApi } from 'openai'

dotenv.config()

Next, we generate a file called .env and within it, we create a variable named OPENAI_API_KEY with the secretKey obtained from the OpenAI keys as its value.

Now, we create a configuration object using the OpenAI package

const configuration = new Configuration({
apiKey: process.env.OPENAI_API_KEY,
});

const openai = new OpenAIApi(configuration);

const app = express()
app.use(cors())
app.use(express.json())

app.post('/', async (req, res) => {
try {
const prompt = req.body.prompt;

const response = await openai.createCompletion({
model: "text-davinci-003",
prompt: `${prompt}`,
temperature: 0,
max_tokens: 3000,
top_p: 1,
frequency_penalty: 0.5,
presence_penalty: 0,
});

res.status(200).send({
bot: response.data.choices[0].text
});

} catch (error) {
console.error(error)
res.status(500).send(error || 'Something went wrong');
}
})

app.listen(5001, () => console.log('http://localhost:5001'))

The configuration settings for sending API calls to the OpenAI API are stored and managed by the Configuration object. For making successful API requests, it comprises parameters like the API key, endpoint URL, and request timeouts. We specify our apiKey which we got from the Open AI website.

Integrate ChatGPT in Flutter

The OpenAIApi the object is a class in the OpenAI Node.js library that provides methods for interacting with the OpenAI API. It is used to authenticate the API key, configure the API request, and make API calls to the OpenAI servers.

Special thanks to FeedSpot, for nominating me on their website! Checkout FeedSpot here for exciting Flutter content !!🙌 🎉🎉

Next, we set up the express server and add the required middleware. There are various models available from OpenAI, but we choose text-davinci-003 them for our case.

A variation of OpenAI’s GPT (Generative Pre-trained Transformer) language model is the text-davinci-003 model. It is specifically trained for natural language generation tasks and has a large capacity to understand and produce human-like text. The model has been optimized for a variety of tasks, including text completion, summarization, question-answering, and more. It was trained on a vast amount of diverse text data.

OpenAI Models

Next, we create a POSTroute for ChatGPT called / and send the response back to the client.

await openai.createCompletion({
model: "text-davinci-003",
prompt: `${prompt}`,
temperature: 0,
max_tokens: 3000,
top_p: 1,
frequency_penalty: 0.5,
presence_penalty: 0,
});

This snippet is using the OpenAI API to generate text completions based on a given prompt. It creates a completion by sending a request to the OpenAI API with the following parameters:

  • model: OpenAI model to use text-davinci-003 in our case
  • prompt: text prompt to generate the completion.
  • temperature: between 0 and 1. Higher values mean that the model will take more risks in generating the text.
  • max_tokens: maximum number of tokens (words or punctuation marks) that the model can generate.
  • top_p: selects the most likely tokens until a certain threshold probability is reached.
  • frequency_penalty: penalty value between -2.0 and 2.0 that penalizes the model for generating tokens already appeared in the generated text
  • presence_penalty: penalty value between -2.0 and 2.0 that penalizes the model for generating tokens similar to the text in the prompt.

We then send the JSON response inside a property called “bot” whose value is extracted from the “text” property of the first element in the “choices” array of the “response” data object.

We listen to the incoming requests on the port 5001 Finally, we start the server by running the following command in the terminal

# LISTEN TO THE PORT
app.listen(5001, () => console.log('http://localhost:5001'))

# TO RUN THE SERVER
npm run server

And if we try to curl the URL above with a sample request, we get the following

# SEND REQUEST TO LOCALHOST
curl -X \
POST "http://localhost:5001" -H \
"accept: application/json" -H \
"Content-Type: application/json" -d "{\"prompt\" : \"Hello\"}"

We get the following:

Response from the localhost server

Creating ChatGPT UI

We start by creating a Flutter Web project

Pre-Requisite

  • We should be using the flutter master channel
  • The Dart version should be 3.0.0 or above

Let’s create a project using

flutter create chatgpt_embedding --platforms web

Note: We specify the platform to be web meaning the project only supports web

By default, Flutter gives us the counter app.

We include the js package in our project to enable seamless interoperability between JavaScript and Dart code. Any function in your Dart code can be annotated with a @JSExportproperty using js, allowing you to invoke it from JavaScript code.

flutter pub add js

Changes to the Counter App

Our project has only 2 dart files as shown below

Dart Files

We refactor the main.dart and remove all the counter logic. Next, we create a file called gpt.dart This contains the MyHomePage which is a stateful widget and _MyHomePageState is the state class for the MyHomePage widget.

class MyHomePage extends StatefulWidget {

@override
State<MyHomePage> createState() => _MyHomePageState();
}

@js.JSExport()
class _MyHomePageState extends State<MyHomePage> {}

Next, we import the package js and js_util as

import 'package:js/js.dart' as js;
import 'package:js/js_util.dart' as js_util;

The _MyHomePageState is annotated with the @JSExportattribute, since we need to pass the user, queries to the JavaScript side. This makes the Dart object _MyHomePageState as exportable

Inside our initState we call the createDartExport for the _MyHomePageState which creates a JS object literal that forwards to our exported Dart class (which in our case is _MyHomePageState)

The createDartExport() method is used to create a JavaScript object that has a reference to a Dart object, which can then be accessed from JavaScript code. The setProperty() and callMethod() methods are used to set up JavaScript properties and call JavaScript functions, respectively.

void initState() {
super.initState();
final export = js_util.createDartExport(this);

// These two are used inside the [js/js-interop.js]
js_util.setProperty(js_util.globalThis, '_appState', export);
js_util.callMethod<void>(js_util.globalThis, '_stateSet', []);
}

A JsObject instance export is created by calling the createDartExport() method provided by the js_util library. This creates a JavaScript object that has a reference to the Dart object that is being exported.

Two JavaScript properties are set using the setProperty() method provided by js_util. The first property _appState, which is set to the export an object created in the previous step. This property can be accessed from JavaScript code to interact with the Dart object.

The second property_stateSet, which is a JavaScript function that is defined in another file (js-interop.js). This function is called using the callMethod() method provided by js_util. The empty array [] passed as the second argument indicates that no arguments are being passed to the function.

Note: _appState and _stateSet are present inside the js file, which we will discuss later.

We create a TextEditingController and FloatingActionButton inside our gpt.dart Basically, this will take the user queries and pass them onto the ChatGPT UI. There is a function that gets invoked when the user finishes the query or when the user presses the FAB

// This stores the user query
String _textQuery = '';

void textInputCallback(String value) {
textFocusNode.requestFocus();
setState(() {
_textQuery = value;
// This line makes sure the handler gets invoked
_streamController.add(null);
});
}
<script src="js/js-interop.js" defer></script>

The src the attribute specifies the URL of the JavaScript file to be loaded, in this case js-interop.js.

JavaScript File

All the interop with Dart is present inside this file. We make use of IIFE to create a function

(function () {
window._stateSet = function () {
console.log('HELLO From Flutter!!')
};
}());

This code sets the _stateSet function as a property of the global window object. Next, we access the _appState and save it inside a variable inside the JS

let appState = window._appState;

HTML and CSS for ChatGPT

ChatGPT UI in Flutter
ChatGPT UI in Flutter

For the sake of not making this article very long, attaching the css and html files for ChatGPT UI.


Embed ChatGPT UI in Flutter

Since we want to show the flutter app, we create a div with id as flutter_target

We tweak the script tag inside the index.html by first adding an event listener to the window Get the div id, which represents the flutter app in our case flutter_target, using the query selector

window.addEventListener("load", function (ev) {
  let target = document.querySelector("#flutter_target");
  
  _flutter.loader.loadEntrypoint({
    onEntrypointLoaded: async function (engineInitializer) {
      let appRunner = await engineInitializer.initializeEngine({
        hostElement: target,
      });
      await appRunner.runApp();
    },
  });
});
  • Using the _flutter.loader JavaScript API offered by flutter.js, we modify how a Flutter app is launched on the web.

The following phases make up the initialization process:

  • The entry point script is loaded, and the service worker is initialized after retrieving the main.dart.js script.
  • Initializing the Flutter engine which downloads the necessary files, including CanvasKit, fonts, and assets, to launch the Flutter web engine.
  • Running the application which executes your Flutter app after preparing the DOM for it.

Interoperability between Javascript and Dart

To pass the _textQuery value from Flutter to JS, we create the textQuery function inside of the gpt.dart file and annotate it with the @JSExport property (making it accessible from the JS side)

@js.JSExport()
String get textQuery => _textQuery;

JS Changes

const form = document.querySelector('form')
const chatContainer = document.querySelector('#chat_container')
const formData = new FormData(form)

We select the first form element on the page using the document.querySelector() method. Next, we select an HTML element with an ID of chat_container using the document.querySelector() method.

Then, we create a new instance of the FormData object using the form variable. This FormData the object is used to retrieve the data from the form when it is submitted.

We generate a div with a unique ideach time the user submits their queries from the Flutter app.

Note: The id is basically the current timestamp

function chatStripe(isAi, value, uniqueId) {
    return (
        `
        <div class="wrapper ${isAi && 'ai'}">
            <div class="chat">
                <div class="profile">
                    <img 
                    src=${isAi ? './assets/bot.svg' : './assets/user.svg'} 
                    alt="${isAi ? 'bot' : 'user'}" 
                    />
                </div>
                <div class="message" id=${uniqueId}>${value}</div>
            </div>
        </div>
    `
    )
}

We also have a function named chatStripe() that takes three arguments: isAi, value, and uniqueId.

Chat Stripe
Chat Stripe
  • The isAi the argument is a boolean value that determines whether the chat stripe is for the bot or the user. If it’s true, then it’s for the bot, and if it’s false, then it’s for the user.
  • The value the argument is the message text to be displayed in the chat stripe.
  • The uniqueId the argument is the unique id that we generate from the above function

The img element’s src the attribute is set to the path of the bot.svg or user.svg icon depending on the isAi argument.

The id attribute of the message div is set to the uniqueId argument.

Finally, we have a JavaScript function that handles form submissions. The function is triggered when the user submits a message to the bot by clicking a submit button or hitting the enter key in a text input field.

  • We add the user’s chat stripe to the chat container using the chatStripe() function
  • We then generate a unique ID using the generateUniqueId() function

Add Server

We create a POST request to a local server at http://localhost:5001/ with the user’s message as a JSON payload.

Note: The server was created in the first step above

If the response from the server is OK, we retrieve the bot’s response from the JSON data and trim any trailing spaces or newlines using the trim() method.

And, we type out the bot’s response in the message div using the typeText() function.

function typeText(element, text) {
    let index = 0

    let interval = setInterval(() => {
        if (index < text.length) {
            element.innerHTML += text.charAt(index)
            index++
        } else {
            clearInterval(interval)
        }
    }, 20)
}

If the response from the server is not OK, we display an error message in the message div and alert the error message.

Integrating with Flutter

Flutter and GPT
Flutter and GPT

We define a function called updateTextState, which sets the prompt property of the form element to the value of appState.textQuery. The updateTextState the function is not called directly but is instead registered as a callback using the appState.addHandler() method. This means that the updateState the function will be called every time appState changes.

let updateTextState = function () {
   formData.set('prompt', appState.textQuery);
   handleSubmit.call(form)
};

// Register a callback to update the text field from Flutter.
appState.addHandler(updateTextState);

// CHAT GPT FUNCTIONS
form.addEventListener("submit", (e) => {
   handleSubmit(e)
});

// CHAT GPT FUNCTIONS
form.addEventListener("keyup", (e) => {
  if (e.keyCode === 13) {
    handleSubmit(e)
  }
});

We also attach event listeners for the

  • submit event of the form element. It listens for the form submission and calls the handleSubmit() function with the event object as an argument.
  • keyup event of the form element. It listens for the user to press the Enter key (keyCode 13) and, it calls the handleSubmit() function with the event object as an argument, which has the same effect as the first event listener.

The handleSubmit() function is responsible for handling the form submission, displaying the user’s message, sending it to the server, and displaying the server’s response.

On the Flutter side,

A StreamController instance _streamController is defined by the type StreamController<void>.broadcast(). The broadcast() the method creates a stream controller that can handle multiple subscribers.

final _streamController = StreamController<void>.broadcast();

@js.JSExport()
void addHandler(void Function() handler) {
  // This registers the handler we wrote in [js/js-interop.js]
  _streamController.stream.listen((event) {
    handler();
  });
}

Inside the method, a StreamSubscription is created by calling the listen() method on _streamController.stream. The listen() the method takes a callback function as an argument, which will be called each time an event is added to the stream.

Next, we modify our existing functions and add an event to the streamController to make sure the handler we wrote in the js-interop.js gets invoked.

Inside the setState() method, the _textQuery value is set by the text controller value, and then the StreamController instance _streamController is notified of the change by adding a null value to the stream using _streamController.add(null).

@js.JSExport()
void textInputCallback(String value) {
  setState(() {
    _textQuery = value;
    // This line makes sure the handler gets invoked
    _streamController.add(null);
  });
}

In this manner, the UI is updated whenever the textQuery is passed, from the Dart side and sent to the JS side.

Source Code