Skip to content

Commit

Permalink
feat: AI Chat drawer (#422)
Browse files Browse the repository at this point in the history
* feat: add basic lms chat

* refactor imports

* comment transcript code

* more enhancemen to provide context

* fix chat display for all problems

* more enhancements

* more changes for the post message

* styling of launch button

* pass context from backend + added setting for the learn ai api URL

* pre-commit fixes

* add ask tim input + fixed styling

* pre-commit fixes

* fix bu

* add chatId + pass Learning MFE URL

* remove chat button alt

* review changes
  • Loading branch information
asadali145 authored Feb 21, 2025
1 parent a273f83 commit 8e960bf
Show file tree
Hide file tree
Showing 11 changed files with 195 additions and 11 deletions.
1 change: 1 addition & 0 deletions src/ol_openedx_chat/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ python_distribution(
"src/ol_openedx_chat/static/js:ol_chat_js",
"src/ol_openedx_chat/static/css:ol_chat_css",
"src/ol_openedx_chat/static/html:ol_chat_html",
"src/ol_openedx_chat/static/images:ol_chat_images",
],
provides=setup_py(
name="ol-openedx-chat",
Expand Down
49 changes: 41 additions & 8 deletions src/ol_openedx_chat/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,30 +40,36 @@ class OLChatAside(XBlockAside):
XBlock aside that enables OL AI Chat functionality for an XBlock
"""

enabled = Boolean(
ol_chat_enabled = Boolean(
display_name=_("Open Learning Chat enabled status"),
default=False,
scope=Scope.content,
scope=Scope.settings,
help=_("Indicates whether or not Open Learning chat is enabled for a block"),
)
chat_prompts = String(
display_name=_("Open Learning Chat Prompt text"),
default="",
scope=Scope.content,
scope=Scope.settings,
help=_("Prompt hint text for chat in a block"),
)
additional_solution = String(
display_name=_("Additional solution for problem"),
default="",
scope=Scope.content,
scope=Scope.settings,
help=_("Additional solution for the problem in context of chat"),
)
llm_model = String(
display_name=_("Open Learning Chat selected LLM model"),
default="",
scope=Scope.content,
scope=Scope.settings,
help=_("Selected LLM model to be used for a block"),
)
ask_tim_drawer_title = String(
display_name=_("Open Learning Drawer Title"),
default="",
scope=Scope.settings,
help=_("Drawer title displayed in the chat drawer"),
)

@XBlockAside.aside_for(STUDENT_VIEW)
def student_view_aside(self, block, context=None):
Expand All @@ -79,7 +85,30 @@ def student_view_aside(self, block, context=None):
return self.author_view_aside(block, context)

fragment = Fragment("")
fragment.add_content(render_template("static/html/student_view.html"))
if not self.ol_chat_enabled:
return fragment

fragment.add_content(
render_template(
"static/html/student_view.html",
{
"block_key": self.scope_ids.usage_id.usage_key.block_id,
"block_type": getattr(block, "category", None),
},
)
)
fragment.add_css(get_resource_bytes("static/css/ai_chat.css"))
fragment.add_javascript(get_resource_bytes("static/js/ai_chat.js"))
extra_context = {
"starters": self.chat_prompts.split("\n") if self.chat_prompts else [],
"ask_tim_drawer_title": self.ask_tim_drawer_title,
"block_usage_key": self.scope_ids.usage_id.usage_key.block_id,
"user_id": self.runtime.user_id,
"learn_ai_api_url": settings.LEARN_AI_API_URL,
"learning_mfe_base_url": settings.LEARNING_MICROFRONTEND_URL,
}

fragment.initialize_js("AiChatAsideInit", json_args=extra_context)
return fragment

@XBlockAside.aside_for(AUTHOR_VIEW)
Expand All @@ -92,8 +121,11 @@ def author_view_aside(self, block, context=None): # noqa: ARG002
render_template(
"static/html/studio_view.html",
{
"is_enabled": self.enabled,
"is_enabled": self.ol_chat_enabled,
"chat_prompts": self.chat_prompts,
"ask_tim_drawer_title": self.ask_tim_drawer_title
if self.ask_tim_drawer_title
else f"about {block.display_name}",
"selected_llm_model": self.llm_model,
"additional_solution": self.additional_solution,
"llm_models_list": list(
Expand Down Expand Up @@ -131,7 +163,8 @@ def update_chat_config(self, request, suffix=""): # noqa: ARG002
)

self.chat_prompts = posted_data.get("chat_prompts", "")
self.ask_tim_drawer_title = posted_data.get("ask_tim_drawer_title", "")
self.llm_model = posted_data.get("selected_llm_model", "")
self.enabled = posted_data.get("is_enabled", False)
self.ol_chat_enabled = posted_data.get("is_enabled", False)
self.additional_solution = posted_data.get("additional_solution", "")
return Response()
1 change: 1 addition & 0 deletions src/ol_openedx_chat/settings/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ def plugin_settings(settings):
# .. {"MODEL_NAME1": API_KEY, "MODEL_NAME2": API_KEY}

settings.OL_CHAT_SETTINGS = env_tokens.get("OL_CHAT_SETTINGS", {})
settings.LEARN_AI_API_URL = env_tokens.get("LEARN_AI_API_URL", "")
86 changes: 86 additions & 0 deletions src/ol_openedx_chat/static/css/ai_chat.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#app-root {
width: 450px;
height: 600px;
}

.video-block-chat-button-container {
background: whitesmoke;
box-shadow:
0 -50px 0 12px whitesmoke,
-12px 12px 0 0 whitesmoke,
12px 12px 0 0 whitesmoke,
0 2px 0 0 whitesmoke;
}

.ai-chat-button {
display: flex;
flex-direction: row;
position: relative;
bottom: 70px;
left: 70%;
width: fit-content;
cursor: pointer;
z-index: 1000;
height: 48px;
padding: 0 20px 0 24px;
justify-content: center;
align-items: center;
gap: 10px;
align-self: stretch;
border-radius: 4px;
border: 2px solid #A31F34;
background: #FFF;

p {
color: #A31F34;
font-size: 16px;
font-style: normal;
font-weight: 500;
line-height: 16px;
margin-bottom: 0 !important;
}

.chat-icon {
width: 24px;
height: 24px;
}

&.video-block-chat-button {
bottom: 0;
left: 63%;
margin-top: 50px;
padding: 5px 20px 5px 24px;

p {
line-height: 20px;
margin-right: 30px;
}
}

&:hover {
background: rgba(255, 0, 0, 0.06);
}
}

@media screen and (max-width: 768px) {
.ai-chat-button {
bottom: 65px;
left: 58%;

&.video-block-chat-button {
margin-top: 10px;
left: 51%;
}
}
}

@media screen and (max-width: 425px) {
.ai-chat-button {
position: unset;
margin-left: 0;
}
}

.video {
margin: 0 0;
}
4 changes: 4 additions & 0 deletions src/ol_openedx_chat/static/css/studio.css
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,7 @@
.enabled-checkbox {
min-height: auto !important;
}

#ask-tim-drawer-title {
min-height: unset;
}
12 changes: 10 additions & 2 deletions src/ol_openedx_chat/static/html/student_view.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
<div>
<button type="button" onclick="alert('Hello World!')">Hello World!</button>
<div class="{{ block_type }}-block-chat-button-container">
<div class="ai-chat-button {{ block_type }}-block-chat-button" id="chat-button-{{ block_key }}" data-block-key="{{ block_key }}">
{% if block_type == "problem" %}
<p>Help me with this problem</p>
{% elif block_type == "video" %}
<p><b>Questions about this video?</b><br>AskTim</p>
{% endif %}
<img src="/static/images/icon.svg" class="chat-icon">
</div>
<div id="app-root-{{ block_key }}"></div>
</div>
4 changes: 4 additions & 0 deletions src/ol_openedx_chat/static/html/studio_view.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
<label for="chat-prompts">Enter Prompts to assist students</label>
<textarea id="chat-prompts" name="chat-prompts" placeholder="Where can I find more information on Large Language Models?" required>{{ chat_prompts }}</textarea>
<label class="sub-label">Learners will see these prompts when using the chatbot feature</label>
<!-- Text Fields -->
<label for="ask-tim-drawer-title">Enter the Chat Drawer title</label>
<input type="text" id="ask-tim-drawer-title" name="ask-tim-drawer-title" value="{{ ask_tim_drawer_title }}" required>
<label class="sub-label">Learners will see this as the title of the Chat Drawer.</label>
<!-- Dropdown -->
<label for="llm-model-dropdown">Choose the Large Language Model (LLM) to use for this question</label>
<select class="llm-dropdown" id="llm-model-dropdown" name="llm-model-dropdown" value="{{ llm_model }}" required>
Expand Down
4 changes: 4 additions & 0 deletions src/ol_openedx_chat/static/images/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
resources(
name="ol_chat_images",
sources=["*.svg"],
)
5 changes: 5 additions & 0 deletions src/ol_openedx_chat/static/images/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 37 additions & 0 deletions src/ol_openedx_chat/static/js/ai_chat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
(function ($) {
function AiChatAsideView(runtime, element, block_element, init_args) {
$(function ($) {

const INITIAL_MESSAGES = [
{
content: "Hi! What are you interested in learning about?",
role: "assistant",
},
];
$(`#chat-button-${init_args.block_usage_key}`).on("click", { starters: init_args.starters, askTimTitle: init_args.ask_tim_drawer_title }, function (event) {
const starters = event.data.starters.map(message => ({ content: message }));
const blockKey = $(this).data("block-key");

window.parent.postMessage(
{
type: "smoot-design::chat-open",
payload: {
chatId: blockKey,
askTimTitle: event.data.askTimTitle,
apiUrl: init_args.learn_ai_api_url,
initialMessages: INITIAL_MESSAGES,
conversationStarters: starters,
},
},
init_args.learning_mfe_base_url, // Ensure correct parent origin
);
});
});
}

function AiChatAside(runtime, element, block_element, init_args) {
return new AiChatAsideView(runtime, element, block_element, init_args);
}

window.AiChatAsideInit = AiChatAside;
})($);
3 changes: 2 additions & 1 deletion src/ol_openedx_chat/static/js/studio.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@
var studioRuntime = new window.StudioRuntime.v1();

const chatPromptsField = element.querySelector("#chat-prompts");
const askTIMTitleField = element.querySelector("#ask-tim-drawer-title");
const llmModelDropdown = element.querySelector("#llm-model-dropdown");
const additionalSolutionField = element.querySelector("#additional-solution");
const enabledCheck = element.querySelector("#is-enabled-"+chatForm.dataset.blockId);

// Get the handler URL
const handlerUrl = studioRuntime.handlerUrl(element, 'update_chat_config');
var dataToPost = {"chat_prompts": chatPromptsField.value, "selected_llm_model": llmModelDropdown.value, "is_enabled": enabledCheck.checked, "additional_solution": additionalSolutionField.value};
var dataToPost = {"chat_prompts": chatPromptsField.value, "ask_tim_drawer_title": askTIMTitleField.value, "selected_llm_model": llmModelDropdown.value, "is_enabled": enabledCheck.checked, "additional_solution": additionalSolutionField.value};

$.ajax({
url: handlerUrl,
Expand Down

0 comments on commit 8e960bf

Please sign in to comment.