We will cover briefly:
- Creating a ChatGPT server locally
- Creating ChatGPT UI
- 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

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

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.

Next, we create a POST
route 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 usetext-davinci-003
in our caseprompt
: 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 textpresence_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:

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 beweb
meaning the project only supportsweb
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 @JSExport
property 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

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 @JSExport
attribute, 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
.

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:
- Creating a ChatGPT server locally
- Creating ChatGPT UI
- 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

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.

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.

Next, we create a POST
route 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 usetext-davinci-003
in our caseprompt
: 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 textpresence_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:

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 beweb
meaning the project only supportsweb
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 @JSExport
property 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

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 @JSExport
attribute, 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
.

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

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 byflutter.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 id
each 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
.

- The
isAi
the argument is a boolean value that determines whether the chat stripe is for the bot or the user. If it’strue
, then it’s for the bot, and if it’sfalse
, 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’ssrc
the attribute is set to the path of thebot.svg
oruser.svg
icon depending on theisAi
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

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 theform
element. It listens for the form submission and calls thehandleSubmit()
function with theevent
object as an argument.keyup
event of theform
element. It listens for the user to press the Enter key (keyCode
13) and, it calls thehandleSubmit()
function with theevent
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.