Skip to content

Commit

Permalink
[FEAT] Support for displaying live conversation on dedicated URL (#105)
Browse files Browse the repository at this point in the history
* renamed klatchat page title to Chatbots Forum

* Added logic of the `live` URL: destination which renders latest CCAI conversation

* Added docstring annotation to `displayLiveChat` function

* Refactored logic to have a dedicated type for the live conversations
  • Loading branch information
NeonKirill authored Nov 20, 2024
1 parent 2605d29 commit 83d60d5
Show file tree
Hide file tree
Showing 13 changed files with 253 additions and 60 deletions.
47 changes: 18 additions & 29 deletions chat_client/blueprints/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
from fastapi import APIRouter, Request
from fastapi.templating import Jinja2Templates

from chat_client.client_config import client_config
from chat_client.client_utils.template_utils import (
render_conversation_page,
render_nano_page,
)

router = APIRouter(
prefix="/chats",
Expand All @@ -48,38 +51,24 @@ async def chats(request: Request):
:returns chats template response
"""
return conversation_templates.TemplateResponse(
"conversation/base.html",
{
"request": request,
"section": "Followed Conversations",
"add_sio": True,
"redirect_to_https": client_config.get("FORCE_HTTPS", False),
},
)
return render_conversation_page(request=request)


@router.get("/live")
async def live_chats(request: Request):
"""
Renders live chats page HTML as a response related to the input request
:param request: input Request object
:returns chats template response
"""
return render_conversation_page(request=request, additional_context={"live": True})


@router.get("/nano_demo")
async def nano_demo(request: Request):
"""
Minimal working Example of Nano
"""
client_url = f'"{request.url.scheme}://{request.url.netloc}"'
server_url = f'"{client_config["SERVER_URL"]}"'
if client_config.get("FORCE_HTTPS", False):
client_url = client_url.replace("http://", "https://")
server_url = server_url.replace("http://", "https://")
client_url_unquoted = client_url.replace('"', "")
return conversation_templates.TemplateResponse(
"sample_nano.html",
{
"request": request,
"title": "Nano Demonstration",
"description": "Klatchat Nano is injectable JS module, "
"allowing to render Klat conversations on any third-party pages, "
"supporting essential features.",
"server_url": server_url,
"client_url": client_url,
"client_url_unquoted": client_url_unquoted,
},
)
return render_nano_page(request=request)
42 changes: 40 additions & 2 deletions chat_client/client_utils/template_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,50 @@
from starlette.requests import Request
from starlette.templating import Jinja2Templates

from chat_client.client_config import client_config

component_templates = Jinja2Templates(

jinja_templates_factory = Jinja2Templates(
directory=os.environ.get("TEMPLATES_DIR", "chat_client/templates")
)


def render_conversation_page(request: Request, additional_context: dict | None = None):
return jinja_templates_factory.TemplateResponse(
"conversation/base.html",
{
"request": request,
"section": "Followed Conversations",
"add_sio": True,
"redirect_to_https": client_config.get("FORCE_HTTPS", False),
**(additional_context or {}),
},
)


def render_nano_page(request: Request, additional_context: dict | None = None):
client_url = f'"{request.url.scheme}://{request.url.netloc}"'
server_url = f'"{client_config["SERVER_URL"]}"'
if client_config.get("FORCE_HTTPS", False):
client_url = client_url.replace("http://", "https://")
server_url = server_url.replace("http://", "https://")
client_url_unquoted = client_url.replace('"', "")
return jinja_templates_factory.TemplateResponse(
"sample_nano.html",
{
"request": request,
"title": "Nano Demonstration",
"description": "Klatchat Nano is injectable JS module, "
"allowing to render Klat conversations on any third-party pages, "
"supporting essential features.",
"server_url": server_url,
"client_url": client_url,
"client_url_unquoted": client_url_unquoted,
**(additional_context or {}),
},
)


def callback_template(request: Request, template_name: str, context: dict = None):
"""
Returns template response based on provided params
Expand All @@ -49,6 +87,6 @@ def callback_template(request: Request, template_name: str, context: dict = None
context["request"] = request
# Preventing exiting to the source code files
template_name = template_name.replace("../", "").replace(".", "/")
return component_templates.TemplateResponse(
return jinja_templates_factory.TemplateResponse(
f"components/{template_name}.html", context
)
85 changes: 71 additions & 14 deletions chat_client/static/js/chat_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -491,17 +491,52 @@ function updateCIDStoreProperty(cid, property, value){
}

/**
* Custom Event fired on supported languages init
* @type {CustomEvent<string>}
* Boolean function that checks whether live chats must be displayed based on page meta properties
* @returns {boolean} true if live chat should be displayed, false otherwise
*/
const chatAlignmentRestoredEvent = new CustomEvent("chatAlignmentRestored", { "detail": "Event that is fired when chat alignment is restored" });
const shouldDisplayLiveChat = () => {
const liveMetaElem = document.querySelector("meta[name='live']");
if (liveMetaElem){
return liveMetaElem.getAttribute("content") === "1"
}
return false
}

/**
* Restores chats alignment from the local storage
*
* @param keyName: name of the local storage key
**/
async function restoreChatAlignment(keyName=conversationAlignmentKey){
* Fetches latest live conversation from the klat server API and builds its HTML
* @returns {Promise<*>} fetched conversation data
*/
const displayLiveChat = async () => {
return await fetchServer('chat_api/live')
.then(response => {
if(response.ok){
return response.json();
}else{
throw response.statusText;
}
})
.then(data => {
if (getUserMessages(data, null).length === 0){
console.debug('All of the messages are already displayed');
setDefault(setDefault(conversationState, data['_id'], {}), 'all_messages_displayed', true);
}
return data;
})
.then(
async data => {
await buildConversation(data, data.skin, true);
return data;
}
)
.catch(async err=> {
console.warn('Failed to display live chat:',err);
});
}

/**
* Restores chat alignment based on the page cache
*/
const restoreChatAlignmentFromCache = async () => {
let cachedItems = await retrieveItemsLayout();
if (cachedItems.length === 0){
cachedItems = [{'cid': '1', 'added_on': getCurrentTimestamp(), 'skin': CONVERSATION_SKINS.BASE}]
Expand All @@ -519,6 +554,23 @@ async function restoreChatAlignment(keyName=conversationAlignmentKey){
}
});
}
}

/**
* Custom Event fired on supported languages init
* @type {CustomEvent<string>}
*/
const chatAlignmentRestoredEvent = new CustomEvent("chatAlignmentRestored", { "detail": "Event that is fired when chat alignment is restored" });

/**
* Restores chats alignment from the local storage
**/
async function restoreChatAlignment(){
if (shouldDisplayLiveChat()){
await displayLiveChat();
} else {
await restoreChatAlignmentFromCache();
}
console.log('Chat Alignment Restored');
document.dispatchEvent(chatAlignmentRestoredEvent);
}
Expand Down Expand Up @@ -689,19 +741,21 @@ async function displayConversation(searchStr, skin=CONVERSATION_SKINS.BASE, aler

/**
* Handles requests on creation new conversation by the user
* @param conversationName: New Conversation Name
* @param isPrivate: if conversation should be private (defaults to false)
* @param conversationID: New Conversation ID (optional)
* @param boundServiceID: id of the service to bind to conversation (optional)
* @param conversationName - New Conversation Name
* @param isPrivate - if conversation should be private (defaults to false)
* @param conversationID - New Conversation ID (optional)
* @param boundServiceID - id of the service to bind to conversation (optional)
* @param createLiveConversation - if conversation should be treated as live conversation (defaults to false)
*/
async function createNewConversation(conversationName, isPrivate=false, conversationID=null, boundServiceID=null) {
async function createNewConversation(conversationName, isPrivate=false, conversationID=null, boundServiceID=null, createLiveConversation=false) {

let formData = new FormData();

formData.append('conversation_name', conversationName);
formData.append('conversation_id', conversationID);
formData.append('is_private', isPrivate? '1': '0')
formData.append('bound_service', boundServiceID?boundServiceID: '');
formData.append('is_live_conversation', createLiveConversation? '1': '0')

await fetchServer(`chat_api/new`, REQUEST_METHODS.POST, formData).then(async response => {
const responseJson = await response.json();
Expand Down Expand Up @@ -743,7 +797,9 @@ document.addEventListener('DOMContentLoaded', (e)=>{
const newConversationID = document.getElementById('conversationID');
const newConversationName = document.getElementById('conversationName');
const isPrivate = document.getElementById('isPrivate');
const createLiveConversation = document.getElementById("createLiveConversation");
let boundServiceID = bindServiceSelect.value;

if (boundServiceID){
const targetItem = document.getElementById(boundServiceID);
if (targetItem.value) {
Expand All @@ -757,7 +813,8 @@ document.addEventListener('DOMContentLoaded', (e)=>{
return -1;
}
}
createNewConversation(newConversationName.value, isPrivate.checked, newConversationID ? newConversationID.value : null, boundServiceID).then(responseOk=>{

createNewConversation(newConversationName.value, isPrivate.checked, newConversationID ? newConversationID.value : null, boundServiceID, createLiveConversation.checked).then(responseOk=>{
newConversationName.value = "";
newConversationID.value = "";
isPrivate.checked = false;
Expand Down
2 changes: 1 addition & 1 deletion chat_client/static/js/http_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const fetchServer = async (urlSuffix, method=REQUEST_METHODS.GET, body=null, jso
return fetch(`${configData["CHAT_SERVER_URL_BASE"]}/${urlSuffix}`, options).then(async response => {
if (response.status === 401){
const responseJson = await response.json();
if (responseJson['msg'] === 'Session Expired'){
if (responseJson['msg'] === 'Requested user is not authorized to perform this action'){
localStorage.removeItem('session');
location.reload();
}
Expand Down
19 changes: 19 additions & 0 deletions chat_client/static/js/user_utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,24 @@ function updateNavbar(forceUpdate=false){
}
}


/**
* Refreshes HTML components appearance based on the current user
* NOTE: this must have only visual impact, the actual validation is done on the backend
*/
const refreshComponentsAppearance = () => {
const currentUserRoles = currentUser?.roles ?? [];
const isAdmin = currentUserRoles.includes("admin");

const createLiveConversationWrapper = document.getElementById("createLiveConversationWrapper");

if (isAdmin){
createLiveConversationWrapper.style.display = "";
}else{
createLiveConversationWrapper.style.display = "none";
}
}

/**
* Custom Event fired on current user loaded
* @type {CustomEvent<string>}
Expand All @@ -268,6 +286,7 @@ async function refreshCurrentUser(refreshChats=false, conversationContainer=null
if(refreshChats) {
refreshChatView(conversationContainer);
}
refreshComponentsAppearance()
console.log('current user loaded');
document.dispatchEvent(currentUserLoaded);
return data;
Expand Down
2 changes: 2 additions & 0 deletions chat_client/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
<head>
<meta charset="UTF-8">
<meta name="referrer" content="unsafe-url">
{% block meta %}
{% endblock %}
{% if redirect_to_https is defined and redirect_to_https %}
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">
{% endif %}
Expand Down
3 changes: 2 additions & 1 deletion chat_client/templates/base_header.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
id="home-link"
href="#"
target="_parent">
<strong style="color: #fff">Klatchat</strong>
<strong style="color: #fff">Chatbots Forum</strong>
</a>
</span>
<button class="navbar-toggler"
Expand Down Expand Up @@ -41,6 +41,7 @@
data-target="#newConversationModal">Create</a>
<a class="dropdown-item" id="importConversationOpener" href="#" data-toggle="modal"
data-target="#importConversationModal">Join</a>
<a class="dropdown-item" href="/chats/live">Open Live Conversation</a>
</div>
</li>
{% endif %}
Expand Down
10 changes: 5 additions & 5 deletions chat_client/templates/components/modals/new_conversation.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,6 @@ <h5 class="modal-title w-100 d-flex justify-content-center" id="newConversationM
<label for="conversationName">Conversation Name *</label>
<input type="text" class="form-control" id="conversationName" placeholder="Enter conversation name">
</div>
<div class="form-group">
<label for="conversationID">Conversation ID</label>
<input type="text" class="form-control" id="conversationID" placeholder="Set id for the conversation">
</div>
<div class="form-group">
<label data-toggle="tooltip" title="Conversation will be pointed to particular service"> Bind to service
<select class="custom-select" id="bind-service-select">
Expand All @@ -34,7 +30,11 @@ <h5 class="modal-title w-100 d-flex justify-content-center" id="newConversationM
<option value="neon.duckduckgo">DuckDuckGo</option>
</select>
</div>
<div class="form-check">
<div class="form-check mt-2" id="createLiveConversationWrapper" style="display: none">
<input type="checkbox" class="form-check-input" id="createLiveConversation">
<label class="form-check-label" for="createLiveConversation">Live</label>
</div>
<div class="form-check mt-2">
<input type="checkbox" class="form-check-input" id="isPrivate">
<label class="form-check-label" for="isPrivate">Private</label>
</div>
Expand Down
6 changes: 6 additions & 0 deletions chat_client/templates/conversation/base.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
{% extends 'base.html' %}

{% block meta %}
{% if live is sameas true %}
<meta name="live" content="1" />
{% endif %}
{% endblock %}

{% block css_imports %}
{{ super() }}
<link rel="stylesheet" href="{{ url_for('css', path='chat.css') }}">
Expand Down
Loading

0 comments on commit 83d60d5

Please sign in to comment.