Django ChatGPT Tutorial Series:
- Introduction
- Create Django Project with Modern Frontend Tooling
- Create Chat App
- Partial Form Submission With Turbo Frame
- Use Turbo Stream To Manipulate DOM Elements
- Send Turbo Stream Over Websocket
- Using OpenAI Streaming API With Celery
- Use Stimulus to Better Organize Javascript Code in Django
- Use Stimulus to Render Markdown and Highlight Code Block
- Use Stimulus to Improve UX of Message Form
- Source Code chatgpt-django-project
In this article, we will learn what is Turbo Stream and how to use it to manipulate the DOM elements on the web page.
Objective
- Learn what is Turbo Stream
- Use Turbo Stream element to update web page in the HTTP response.
What is Turbo Stream
Turbo Streams deliver page changes as fragments of HTML wrapped in self-executing
<turbo-stream>
elements. Each stream element specifies an action together with a target ID to declare what should happen to the HTML inside it.
With Turbo Stream, we can use HTML snippet from web server to append
, update
, or remove
the target DOM elements.
<turbo-stream action="append" target="messages">
<template>
<div id="message_1">
This div will be appended to the element with the DOM ID "messages".
</div>
</template>
</turbo-stream>
<turbo-stream action="remove" target="message_1">
<!-- The element with DOM ID "message_1" will be removed.
The contents of this stream element are ignored. -->
</turbo-stream>
If we return HTML like this from Django server, then we have the ability to manipulate the DOM elements, without touching Javascript
Install django-turbo-response
It is tedious to write raw HTML in Django view, let's use some tool to help us.
Next, let's use django-turbo-response to help us better render Turbo Frame.
Add django-turbo-response==0.0.52
to the requirements.txt
django-turbo-response==0.0.52
(venv)$ pip install -r requirements.txt
Update hotwire_django_app/settings.py
INSTALLED_APPS = [
'turbo_response', # new
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'turbo_response.middleware.TurboMiddleware', # new
...
]
Notes:
- Add
turbo_response
to theINSTALLED_APPS
- Add
turbo_response.middleware.TurboMiddleware
to theMIDDLEWARE
Now django-turbo-response
has been installed, let's use it in our project.
Use Turbo Stream To Reset Form
In the previous section, we used turbo-frame
to reset the form of the page after successful form submission.
Let's build this feature using Turbo Stream.
Update chatgpt_django_app/chat/views.py
from turbo_response import TurboStream, TurboStreamResponse # new
class MessageCreateView(CreateView):
model = Message
template_name = "message_create.html"
form_class = MessageForm
def get_success_url(self):
return None
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["chat_pk"] = self.kwargs.get("chat_pk")
kwargs["role"] = Message.USER
return kwargs
def get_empty_form(self):
"""
Return empty form so we can reset the form
"""
form_class = self.get_form_class()
kwargs = self.get_form_kwargs()
kwargs.pop("data")
kwargs.pop("files")
kwargs.pop("instance")
return form_class(**kwargs)
def form_valid(self, form):
super().form_valid(form)
request = self.request
# return Turbo Stream to do partial updates on the page
return TurboStreamResponse(
[
TurboStream("message-create-frame")
.replace.template(
self.template_name,
{
"form": self.get_empty_form(),
"request": request,
"view": self,
},
).response(request).rendered_content,
]
)
Notes:
- In
form_valid
, we useTurboStreamResponse
to return Turbo Stream to the client - The
replace
means the turbo stream action isreplace
If we launch server and submit, we can see the form is also reset after the successful submission.
If we check the network tab, the response look like this
<turbo-stream action="replace" target="message-create-frame">
<template>
<turbo-frame id="message-create-frame" data-turbo="true">
<form method="post" action="/chat/1/message/create/" class="p-2 m-2">
<input type="hidden" name="csrfmiddlewaretoken" value="hoVEov9njngrPw8ScC1qWvt1Dwz0dJvuhJ9kwgKqD6SAAyuB6HLIO1t8x41fUDxe">
<div class="mb-3">
<textarea name="content" cols="40" rows="10" class="rounded-lg border-gray-300 block leading-normal border px-4 text-gray-700 bg-white focus:outline-none py-2 appearance-none w-full" required id="id_content"></textarea>
<div class="text-xs italic text-red-500"></div>
</div>
<button type="submit" class="transition-all bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded">
Submit
</button>
</form>
</turbo-frame>
</template>
</turbo-stream>
It seems Turbo Stream can do the same thing as turbo-frame
, but what is the difference?
Use Turbo Stream to Update Message List
- With Turbo Frame, we can do partial updates within the frame.
- With Turbo Stream, we can manipulate any elements on the web page
Create chatgpt_django_app/templates/message_list.html
<div id="chat-{{ chat_pk }}-message-list">
{% for instance in object_list %}
<div id="message_{{ instance.pk }}">
{% if instance.role_label == 'User' %}
<div class="p-4 m-4 max-w-full text-black rounded-lg bg-sky-100 prose" >{{ instance.content }}</div>
{% else %}
<div class="p-4 m-4 max-w-full bg-gray-200 rounded-lg prose">{{ instance.content }}</div>
{% endif %}
</div>
{% endfor %}
</div>
Notes:
- To let us better reuse the HTML code, we move the message list code to a separate template
- And we set the
id=chat-{{ chat_pk }}-message-list
Let's update chatgpt_django_app/templates/message_list_page.html
<! --- code omitted for brevity --->
<!-- Message List -->
<div class="overflow-y-auto flex-1">
{% include 'message_list.html' with chat_pk=view.kwargs.chat_pk %}
</div>
<! --- code omitted for brevity --->
Here we use include
to reuse the HTML code in message_list.html
Update chatgpt_django_app/chat/views.py
class MessageCreateView(CreateView):
model = Message
template_name = "message_create.html"
form_class = MessageForm
def get_success_url(self):
return None
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["chat_pk"] = self.kwargs.get("chat_pk")
kwargs["role"] = Message.USER
return kwargs
def get_empty_form(self):
"""
Return empty form so we can reset the form
"""
form_class = self.get_form_class()
kwargs = self.get_form_kwargs()
kwargs.pop("data")
kwargs.pop("files")
kwargs.pop("instance")
return form_class(**kwargs)
def form_valid(self, form):
super().form_valid(form)
request = self.request
# return Turbo Stream to do partial updates on the page
return TurboStreamResponse(
[
TurboStream("message-create-frame")
.replace.template(
self.template_name,
{
"form": self.get_empty_form(),
"request": request,
"view": self,
},
).response(request).rendered_content,
TurboStream(f"chat-{self.kwargs['chat_pk']}-message-list")
.replace.template(
"message_list.html",
{
"chat_pk": self.kwargs['chat_pk'],
"object_list": Message.objects.filter(chat_id=self.kwargs['chat_pk']),
},
).response(request).rendered_content,
]
)
Notes:
- In
form_valid
, we still return Turbo Stream response. - This time, the single TurboStreamResponse can contain two Turbo Stream elements, actually, one response can contain multiple Turbo Stream elements.
- After the message is created, we use Turbo Stream to reset the form and replace the whole message list.
If we launch the server and submit in the form, we can see the form is reset after the successful submission, and the message list is also updated, without full page reload.
Note: for now, the message list can not auto scroll to the bottom to show the latest message, we will fix this later.
Use Turbo Stream to Append New Message
Let's keep moving.
It seems tedious to update the whole message list after message is created.
Let's only append
the new message to the message list.
Create chatgpt_django_app/templates/message_item.html
<div id="message_{{ instance.pk }}">
{% if instance.role_label == 'User' %}
<div class="p-4 m-4 max-w-full text-black rounded-lg bg-sky-100 prose" >{{ instance.content }}</div>
{% else %}
<div class="p-4 m-4 max-w-full bg-gray-200 rounded-lg prose">{{ instance.content }}</div>
{% endif %}
</div>
We extract message instance HTML code from message_list.html to message_item.html.
Update chatgpt_django_app/templates/message_list.html
<div id="chat-{{ chat_pk }}-message-list">
{% for instance in object_list %}
{% include "message_item.html" with instance=instance %}
{% endfor %}
</div>
Update chatgpt_django_app/chat/views.py
class MessageCreateView(CreateView):
model = Message
template_name = "message_create.html"
form_class = MessageForm
def get_success_url(self):
return None
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["chat_pk"] = self.kwargs.get("chat_pk")
kwargs["role"] = Message.USER
return kwargs
def get_empty_form(self):
"""
Return empty form so we can reset the form
"""
form_class = self.get_form_class()
kwargs = self.get_form_kwargs()
kwargs.pop("data")
kwargs.pop("files")
kwargs.pop("instance")
return form_class(**kwargs)
def form_valid(self, form):
super().form_valid(form)
request = self.request
# return Turbo Stream to do partial updates on the page
return TurboStreamResponse(
[
TurboStream("message-create-frame")
.replace.template(
self.template_name,
{
"form": self.get_empty_form(),
"request": request,
"view": self,
},
).response(request).rendered_content,
TurboStream(f"chat-{self.kwargs['chat_pk']}-message-list")
.append.template(
"message_item.html",
{
"instance": self.object,
},
).response(request).rendered_content,
]
)
Notes:
- In
form_valid
method, we append message HTML code to the message list, instead of updating the whole message list.
If we run the server and submit in the form, we can see the form is reset after the successful submission, and the message list is updated to contain the new message.
Note: for now, the message list can not auto scroll to the bottom to show the latest message, we will fix this in later chapter.
OpenAI
Add openai
to the requirements.txt
(venv)$ pip install -r requirements.txt
Update chatgpt_django_app/chat/views.py
import openai
def get_ai_response(message_pk):
chat_instance = Message.objects.get(pk=message_pk).chat
message_instance = Message.objects.create(
role=Message.ASSISTANT,
content="",
chat=chat_instance,
)
messages = Message.for_openai(chat_instance.messages.all())
try:
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=messages,
temperature=0,
)
message_instance.content = response['choices'][0]['message']['content']
message_instance.save(update_fields=["content"])
except Exception as e:
message_instance.content = str(e)
message_instance.save(update_fields=["content"])
# return AI message
return message_instance
Notes:
- In the
get_ai_response
method, we get user message instance, create a new message instance withrole=Message.ASSISTANT
, and useopenai
SDK to get AI response. - As for the OpenAI API key, please set in the environment variable
export OPENAI_API_KEY='sk-...'
or usingopenai.api_key = "sk-..."
Update chatgpt_django_app/chat/views.py
class MessageCreateView(CreateView):
model = Message
template_name = "message_create.html"
form_class = MessageForm
def get_success_url(self):
return None
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["chat_pk"] = self.kwargs.get("chat_pk")
kwargs["role"] = Message.USER
return kwargs
def get_empty_form(self):
"""
Return empty form so we can reset the form
"""
form_class = self.get_form_class()
kwargs = self.get_form_kwargs()
kwargs.pop("data")
kwargs.pop("files")
kwargs.pop("instance")
return form_class(**kwargs)
def form_valid(self, form):
super().form_valid(form)
request = self.request
ai_message = get_ai_response(self.object.pk)
# return Turbo Stream to do partial updates on the page
return TurboStreamResponse(
[
TurboStream("message-create-frame")
.replace.template(
self.template_name,
{
"form": self.get_empty_form(),
"request": request,
"view": self,
},
).response(request).rendered_content,
# user message
TurboStream(f"chat-{self.kwargs['chat_pk']}-message-list")
.append.template(
"message_item.html",
{
"instance": self.object,
},
).response(request).rendered_content,
# AI message
TurboStream(f"chat-{self.kwargs['chat_pk']}-message-list")
.append.template(
"message_item.html",
{
"instance": ai_message,
},
).response(request).rendered_content,
]
)
Notes:
- In the
form_valid
method, we return three turbo stream elements in the turbo stream response. - The first is to reset the form.
- The second is to append the user message to the message list.
- The third is to append the AI message to the message list.
You can run the code and submit a message in the form, you can see the AI response is appended to the message list.
And you will also notice, for some long AI message, you need to wait some seconds to see the full message, we will fix this in later chapter.
Conclusion
In this chapter, we learned how to use Turbo Stream to do partial updates on the page.
We do not need to write any Javascript code, just reuse server-side template to get the job done, which is awesome.
Django ChatGPT Tutorial Series:
- Introduction
- Create Django Project with Modern Frontend Tooling
- Create Chat App
- Partial Form Submission With Turbo Frame
- Use Turbo Stream To Manipulate DOM Elements
- Send Turbo Stream Over Websocket
- Using OpenAI Streaming API With Celery
- Use Stimulus to Better Organize Javascript Code in Django
- Use Stimulus to Render Markdown and Highlight Code Block
- Use Stimulus to Improve UX of Message Form
- Source Code chatgpt-django-project