From 5d445f66afc57765cf7b53955405114f73b0b376 Mon Sep 17 00:00:00 2001 From: Jashon Osala Date: Mon, 2 Oct 2023 03:11:29 +0300 Subject: [PATCH 01/16] spike: Org selection menu sample. --- web/index.py | 11 +++- web/utils/custom/nav_ui/__init__.py | 19 ++++++ web/utils/custom/nav_ui/static/nav.js | 89 +++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 web/utils/custom/nav_ui/__init__.py create mode 100644 web/utils/custom/nav_ui/static/nav.js diff --git a/web/index.py b/web/index.py index 710fd1a1..689a852c 100644 --- a/web/index.py +++ b/web/index.py @@ -9,8 +9,8 @@ production_layout() -with st.sidebar: - org_selection_ui() +# with st.sidebar: +# org_selection_ui() show_pages( [ @@ -59,4 +59,11 @@ """ ) + st.markdown("Enjoy [Docq](https://docq.ai)!") +org_menu_options = [ + {"label": "Org 1", "value": "org1"}, + {"label": "Org 2", "value": "org2"}, + {"label": "Org 3", "value": "org3"}, + {"label": "Org 4", "value": "org4"}, +] diff --git a/web/utils/custom/nav_ui/__init__.py b/web/utils/custom/nav_ui/__init__.py new file mode 100644 index 00000000..4e21e629 --- /dev/null +++ b/web/utils/custom/nav_ui/__init__.py @@ -0,0 +1,19 @@ +"""Nav UI example.""" +import json +import os + +import streamlit.components.v1 as components + +parent_dir = os.path.dirname(os.path.abspath(__file__)) +script_path = os.path.join(parent_dir, "static", "nav.js") + +script = "" +with open(script_path) as f: + script = f.read() + +def my_component(options: list) -> None: + """SIDE BAR.""" + list_str = json.dumps(options) + print(f"\x1b[32m{list_str}\x1b[0m") + components.html(f"", height=0) + diff --git a/web/utils/custom/nav_ui/static/nav.js b/web/utils/custom/nav_ui/static/nav.js new file mode 100644 index 00000000..e28900b8 --- /dev/null +++ b/web/utils/custom/nav_ui/static/nav.js @@ -0,0 +1,89 @@ +parent = window.parent.document || window.document + +const findSideBar = () => { + const sideBar = parent.querySelectorAll('section[data-testid="stSidebar"]'); + console.log(`sideBar: ${sideBar}, body: ${parent.body}`); + if (sideBar) { + return sideBar[0]; + } + return null; +} + +// Container for the logo +const docqLogoContainer = document.createElement('div'); +docqLogoContainer.setAttribute('class', 'docq-logo-container'); +docqLogoContainer.setAttribute('id', 'docq-logo-container'); +docqLogoContainer.setAttribute('style', 'display: flex; justify-content: center; align-items: center; width: 100%; position: sticky; top: 0; z-index: 1000; background-color: red; flex-direction: column; padding: 10px;'); + + +// Close button +const closeButton = document.createElement('button'); +closeButton.setAttribute('id', 'docq-close-button'); +closeButton.setAttribute('style', 'position: absolute; right: 10px; top: 10px; background-color: transparent; border: none; outline: none; cursor: pointer;'); +closeButton.setAttribute('onclick', 'docqClose()'); +closeButton.innerText = 'X'; + +docqLogoContainer.appendChild(closeButton); + + +// Logo +const docqLogo = document.createElement('img'); +docqLogo.setAttribute('src', 'https://github.com/docqai/docq/blob/main/docs/assets/logo.jpg?raw=true'); +docqLogo.setAttribute('alt', 'docq logo'); +docqLogo.setAttribute('style', 'width: 50px; height: 50px;'); +docqLogo.setAttribute('id', 'docq-logo') +docqLogo.setAttribute('async', '1') + + +docqLogoContainer.appendChild(docqLogo); + + +// Create a dropdown menu +const menuTitle = document.createElement('label'); +menuTitle.setAttribute('for', 'docq-menu'); +menuTitle.innerText = 'Select Organization:'; + +const dropdownMenu = document.createElement('select'); +dropdownMenu.setAttribute('name', 'docq-menu'); +dropdownMenu.setAttribute('id', 'docq-menu'); +dropdownMenu.setAttribute('style', 'width: 100%; height: 40px; padding: 10px; border-radius: 5px; border: 1px solid #ccc; margin-top: 10'); + +const options = JSON.parse('{{org-menu-options}}'); +options.forEach((option) => { + const optionElement = document.createElement('option'); + optionElement.setAttribute('value', option.value); + optionElement.innerText = option.label; + dropdownMenu.appendChild(optionElement); +}); + +docqLogoContainer.appendChild(menuTitle); +docqLogoContainer.appendChild(dropdownMenu); + + +const sideBar = findSideBar(); + + +// Add close script to parent +const closeScript = document.createElement('script'); +closeScript.innerHTML = ` + function docqClose() { + const sideBar = document.querySelectorAll('section[data-testid="stSidebar"]'); + if (sideBar) { + console.log('Closing sidebar') + sideBar[0].setAttribute("aria-expanded", "false"); + sideBar[0].setAttribute("aria-hidden", "true"); + sideBar[0].setAttribute("style", "display: none;"); + } + } + `; + +parent.body.appendChild(closeScript); + +if (sideBar) { + // Check if the logo already exists + const docqLogo = parent.getElementById('docq-logo-container'); + if (docqLogo) { + docqLogo.remove(); + } + sideBar.insertBefore(docqLogoContainer, sideBar.firstChild); +} From eb7bab891b0c510ca67250a4e07794f0498035a8 Mon Sep 17 00:00:00 2001 From: Jashon Osala Date: Thu, 5 Oct 2023 05:04:42 +0300 Subject: [PATCH 02/16] chore: Nav UI update --- web/index.py | 6 +- web/personal_ask.py | 4 +- web/st_components/page_header/__init__.py | 14 +++ .../page_header/static/header.js | 53 +++++++++++ .../sidebar_header}/__init__.py | 14 ++- .../sidebar_header/static/sidebar.css | 6 ++ .../sidebar_header/static/sidebar.js | 95 +++++++++++++++++++ web/utils/custom/nav_ui/static/nav.js | 89 ----------------- web/utils/layout.py | 8 ++ 9 files changed, 194 insertions(+), 95 deletions(-) create mode 100644 web/st_components/page_header/__init__.py create mode 100644 web/st_components/page_header/static/header.js rename web/{utils/custom/nav_ui => st_components/sidebar_header}/__init__.py (57%) create mode 100644 web/st_components/sidebar_header/static/sidebar.css create mode 100644 web/st_components/sidebar_header/static/sidebar.js delete mode 100644 web/utils/custom/nav_ui/static/nav.js diff --git a/web/index.py b/web/index.py index 689a852c..2e1455ff 100644 --- a/web/index.py +++ b/web/index.py @@ -4,6 +4,8 @@ from st_pages import Page, Section, add_page_title, show_pages from utils.layout import init_with_pretty_error_ui, org_selection_ui, production_layout, public_access +from web.st_components.sidebar_header import sidebar + init_with_pretty_error_ui() @@ -65,5 +67,7 @@ {"label": "Org 1", "value": "org1"}, {"label": "Org 2", "value": "org2"}, {"label": "Org 3", "value": "org3"}, - {"label": "Org 4", "value": "org4"}, + {"label": "Org 4", "value": "org4"} ] + +sidebar(org_menu_options) diff --git a/web/personal_ask.py b/web/personal_ask.py index 8be1c53d..a2019f2f 100644 --- a/web/personal_ask.py +++ b/web/personal_ask.py @@ -3,7 +3,7 @@ from docq.config import FeatureType from docq.domain import FeatureKey from st_pages import add_page_title -from utils.layout import auth_required, chat_ui, feature_enabled +from utils.layout import auth_required, chat_ui, feature_enabled, header_ui from utils.sessions import get_authenticated_user_id auth_required() @@ -15,3 +15,5 @@ feature = FeatureKey(FeatureType.ASK_PERSONAL, get_authenticated_user_id()) chat_ui(feature) + +header_ui() diff --git a/web/st_components/page_header/__init__.py b/web/st_components/page_header/__init__.py new file mode 100644 index 00000000..12dc4a2e --- /dev/null +++ b/web/st_components/page_header/__init__.py @@ -0,0 +1,14 @@ +"""Header bar.""" +from streamlit.components.v1 import html + +script = "" +with open("web/st_components/page_header/static/header.js") as f: + script = f.read() + + + +def header(username: str, avatar_src: str) -> None: + """Header bar.""" + s = script.replace('{{avatar-src}}', avatar_src) + j = s.replace('{{username}}', username) + html(f"",height=0,) diff --git a/web/st_components/page_header/static/header.js b/web/st_components/page_header/static/header.js new file mode 100644 index 00000000..22d970ee --- /dev/null +++ b/web/st_components/page_header/static/header.js @@ -0,0 +1,53 @@ + +// Container +const docqContainer = document.createElement("div"); +docqContainer.setAttribute("id", "docq-header-container"); +docqContainer.setAttribute( + "style", + "display: flex; justify-content: center; align-items: flex-start; position: sticky; top: 0; z-index: 1000; background-color: red; flex-direction: column; padding: 10px; gap: 10px;" +); + + +// Avatar +const loadAvatar = () => { + const avatar = document.createElement("img"); + avatar.setAttribute("src", "{{avatar-src}}"); + avatar.setAttribute("alt", "user avatar"); + avatar.setAttribute("style", "width: 25px; height: 25px; border-radius: 50%;"); + avatar.setAttribute("id", "docq-avatar"); + avatar.setAttribute("async", "1"); + return avatar; +} + +// Avatar container +const avatarContainer = document.createElement("div"); +avatarContainer.setAttribute("id", "docq-avatar-container"); +avatarContainer.setAttribute( + "style", + "position: absolute; right: 10px; top: 10px; background-color: transparent; border: none; outline: none; cursor: pointer;" +); +avatarContainer.setAttribute("onclick", "docqToggle()"); +avatarContainer.appendChild(loadAvatar()); + + +// User name +const userName = document.createElement("p"); +userName.innerText = "{{username}}"; +userName.setAttribute("id", "docq-user-name"); +userName.setAttribute("style", "margin-right: 10px;"); + +parent.addEventListener("load", () => { + const avatar = loadAvatar(); + avatarContainer.appendChild(avatar); + }); + + +docqContainer.appendChild(avatarContainer); +docqContainer.appendChild(userName); + +// Insert docq container in the DOM +const headerBar = parent.document.querySelector("header"); +if (headerBar) { + headerBar.insertBefore(docqContainer, headerBar.firstChild); +} + diff --git a/web/utils/custom/nav_ui/__init__.py b/web/st_components/sidebar_header/__init__.py similarity index 57% rename from web/utils/custom/nav_ui/__init__.py rename to web/st_components/sidebar_header/__init__.py index 4e21e629..539c0234 100644 --- a/web/utils/custom/nav_ui/__init__.py +++ b/web/st_components/sidebar_header/__init__.py @@ -1,19 +1,25 @@ """Nav UI example.""" import json import os +from contextlib import contextmanager +from typing import Self +import streamlit as st import streamlit.components.v1 as components parent_dir = os.path.dirname(os.path.abspath(__file__)) -script_path = os.path.join(parent_dir, "static", "nav.js") +script_path = os.path.join(parent_dir, "static", "sidebar.js") -script = "" +script, style = "", "" with open(script_path) as f: script = f.read() -def my_component(options: list) -> None: +with open(os.path.join(parent_dir, "static", "sidebar.css")) as f: + style = f.read() + + +def sidebar(options: list) -> None: """SIDE BAR.""" list_str = json.dumps(options) print(f"\x1b[32m{list_str}\x1b[0m") components.html(f"", height=0) - diff --git a/web/st_components/sidebar_header/static/sidebar.css b/web/st_components/sidebar_header/static/sidebar.css new file mode 100644 index 00000000..478eebee --- /dev/null +++ b/web/st_components/sidebar_header/static/sidebar.css @@ -0,0 +1,6 @@ +div[docq-data-sidebar='true'][key='{{key}}'] { + position: sticky ; + top: 0; + width: 100% !important; + background-color: red !important; +} \ No newline at end of file diff --git a/web/st_components/sidebar_header/static/sidebar.js b/web/st_components/sidebar_header/static/sidebar.js new file mode 100644 index 00000000..75044207 --- /dev/null +++ b/web/st_components/sidebar_header/static/sidebar.js @@ -0,0 +1,95 @@ +parent = window.parent.document || window.document; + +const findSideBar = () => { + const sideBar = parent.querySelectorAll('section[data-testid="stSidebar"]'); + console.log(`sideBar: ${sideBar}, body: ${parent.body}`); + if (sideBar) { + return sideBar[0]; + } + return null; +}; + +// Container for the logo +const docqLogoContainer = document.createElement("div"); +docqLogoContainer.setAttribute("class", "docq-logo-container"); +docqLogoContainer.setAttribute("id", "docq-logo-container"); +docqLogoContainer.setAttribute( + "style", + "display: flex; justify-content: center; align-items: center; width: 100%; position: sticky; top: 0; z-index: 1000; background-color: red; flex-direction: column; padding: 10px;" +); + +// Close button +const closeButton = document.createElement("button"); +closeButton.setAttribute("id", "docq-close-button"); +closeButton.setAttribute( + "style", + "position: absolute; right: 10px; top: 10px; background-color: transparent; border: none; outline: none; cursor: pointer;" +); +closeButton.setAttribute("onclick", "docqClose()"); +closeButton.innerText = "X"; + +docqLogoContainer.appendChild(closeButton); + +// Logo +const docqLogo = document.createElement("img"); +docqLogo.setAttribute( + "src", + "https://github.com/docqai/docq/blob/main/docs/assets/logo.jpg?raw=true" +); +docqLogo.setAttribute("alt", "docq logo"); +docqLogo.setAttribute("style", "width: 50px; height: 50px;"); +docqLogo.setAttribute("id", "docq-logo"); +docqLogo.setAttribute("async", "1"); + +docqLogoContainer.appendChild(docqLogo); + +// Create a dropdown menu +const menuTitle = document.createElement("label"); +menuTitle.setAttribute("for", "docq-menu"); +menuTitle.innerText = "Select Organization:"; + +const dropdownMenu = document.createElement("select"); +dropdownMenu.setAttribute("name", "docq-menu"); +dropdownMenu.setAttribute("id", "docq-menu"); +dropdownMenu.setAttribute( + "style", + "width: 100%; height: 40px; padding: 10px; border-radius: 5px; border: 1px solid #ccc; margin-top: 10" +); + +const options = JSON.parse('{{org-menu-options}}'); +options.forEach((option) => { + const optionElement = document.createElement("option"); + optionElement.setAttribute("value", option.value); + optionElement.innerText = option.label; + dropdownMenu.appendChild(optionElement); +}); + +docqLogoContainer.appendChild(menuTitle); +docqLogoContainer.appendChild(dropdownMenu); + +const sideBar = findSideBar(); + +// Add close script to parent +const closeScript = document.createElement("script"); +closeScript.innerHTML = ` + function docqClose() { + const sideBar = document.querySelectorAll('section[data-testid="stSidebar"]'); + if (sideBar) { + console.log('Closing sidebar') + sideBar[0].setAttribute("aria-expanded", "false"); + sideBar[0].setAttribute("aria-hidden", "true"); + sideBar[0].setAttribute("style", "display: none;"); + } + } + `; + +parent.body.appendChild(closeScript); + +if (sideBar) { + // Check if the logo already exists + const docqLogo = parent.getElementById("docq-logo-container"); + if (docqLogo) { + docqLogo.remove(); + } + sideBar.insertBefore(docqLogoContainer, sideBar.firstChild); +} diff --git a/web/utils/custom/nav_ui/static/nav.js b/web/utils/custom/nav_ui/static/nav.js deleted file mode 100644 index e28900b8..00000000 --- a/web/utils/custom/nav_ui/static/nav.js +++ /dev/null @@ -1,89 +0,0 @@ -parent = window.parent.document || window.document - -const findSideBar = () => { - const sideBar = parent.querySelectorAll('section[data-testid="stSidebar"]'); - console.log(`sideBar: ${sideBar}, body: ${parent.body}`); - if (sideBar) { - return sideBar[0]; - } - return null; -} - -// Container for the logo -const docqLogoContainer = document.createElement('div'); -docqLogoContainer.setAttribute('class', 'docq-logo-container'); -docqLogoContainer.setAttribute('id', 'docq-logo-container'); -docqLogoContainer.setAttribute('style', 'display: flex; justify-content: center; align-items: center; width: 100%; position: sticky; top: 0; z-index: 1000; background-color: red; flex-direction: column; padding: 10px;'); - - -// Close button -const closeButton = document.createElement('button'); -closeButton.setAttribute('id', 'docq-close-button'); -closeButton.setAttribute('style', 'position: absolute; right: 10px; top: 10px; background-color: transparent; border: none; outline: none; cursor: pointer;'); -closeButton.setAttribute('onclick', 'docqClose()'); -closeButton.innerText = 'X'; - -docqLogoContainer.appendChild(closeButton); - - -// Logo -const docqLogo = document.createElement('img'); -docqLogo.setAttribute('src', 'https://github.com/docqai/docq/blob/main/docs/assets/logo.jpg?raw=true'); -docqLogo.setAttribute('alt', 'docq logo'); -docqLogo.setAttribute('style', 'width: 50px; height: 50px;'); -docqLogo.setAttribute('id', 'docq-logo') -docqLogo.setAttribute('async', '1') - - -docqLogoContainer.appendChild(docqLogo); - - -// Create a dropdown menu -const menuTitle = document.createElement('label'); -menuTitle.setAttribute('for', 'docq-menu'); -menuTitle.innerText = 'Select Organization:'; - -const dropdownMenu = document.createElement('select'); -dropdownMenu.setAttribute('name', 'docq-menu'); -dropdownMenu.setAttribute('id', 'docq-menu'); -dropdownMenu.setAttribute('style', 'width: 100%; height: 40px; padding: 10px; border-radius: 5px; border: 1px solid #ccc; margin-top: 10'); - -const options = JSON.parse('{{org-menu-options}}'); -options.forEach((option) => { - const optionElement = document.createElement('option'); - optionElement.setAttribute('value', option.value); - optionElement.innerText = option.label; - dropdownMenu.appendChild(optionElement); -}); - -docqLogoContainer.appendChild(menuTitle); -docqLogoContainer.appendChild(dropdownMenu); - - -const sideBar = findSideBar(); - - -// Add close script to parent -const closeScript = document.createElement('script'); -closeScript.innerHTML = ` - function docqClose() { - const sideBar = document.querySelectorAll('section[data-testid="stSidebar"]'); - if (sideBar) { - console.log('Closing sidebar') - sideBar[0].setAttribute("aria-expanded", "false"); - sideBar[0].setAttribute("aria-hidden", "true"); - sideBar[0].setAttribute("style", "display: none;"); - } - } - `; - -parent.body.appendChild(closeScript); - -if (sideBar) { - // Check if the logo already exists - const docqLogo = parent.getElementById('docq-logo-container'); - if (docqLogo) { - docqLogo.remove(); - } - sideBar.insertBefore(docqLogoContainer, sideBar.firstChild); -} diff --git a/web/utils/layout.py b/web/utils/layout.py index a53203c6..350b9675 100644 --- a/web/utils/layout.py +++ b/web/utils/layout.py @@ -22,6 +22,8 @@ from streamlit.components.v1 import html from streamlit.delta_generator import DeltaGenerator +from web.st_components.page_header import header + from .constants import ALLOWED_DOC_EXTS, SessionKeyNameForAuth, SessionKeyNameForChat from .error_ui import _handle_error_state_ui from .formatters import format_archived, format_datetime, format_filesize, format_timestamp @@ -984,3 +986,9 @@ def init_with_pretty_error_ui() -> None: st.error("Something went wrong starting Docq.") log.fatal("Error: setup.init() failed with %s", e) st.stop() + + +def header_ui() -> None: + """Header UI.""" + avatar_src = handle_get_gravatar_url() + header("DocQ@Test-Org", avatar_src, ) From 289e72d79e4c0d4b7a5cdb14d37efe30efcfa20f Mon Sep 17 00:00:00 2001 From: Jashon Osala Date: Fri, 6 Oct 2023 17:33:18 +0300 Subject: [PATCH 03/16] feat: Add sidebar header and headerbar --- web/personal_ask.py | 2 - web/st_components/page_header/__init__.py | 5 +-- .../page_header/static/header.js | 43 ++++++++++-------- web/st_components/sidebar_header/__init__.py | 4 -- .../sidebar_header/static/sidebar.js | 45 ++++++++++--------- web/utils/layout.py | 9 ++-- 6 files changed, 55 insertions(+), 53 deletions(-) diff --git a/web/personal_ask.py b/web/personal_ask.py index a2019f2f..d31f68cf 100644 --- a/web/personal_ask.py +++ b/web/personal_ask.py @@ -15,5 +15,3 @@ feature = FeatureKey(FeatureType.ASK_PERSONAL, get_authenticated_user_id()) chat_ui(feature) - -header_ui() diff --git a/web/st_components/page_header/__init__.py b/web/st_components/page_header/__init__.py index 12dc4a2e..444f1dc3 100644 --- a/web/st_components/page_header/__init__.py +++ b/web/st_components/page_header/__init__.py @@ -6,9 +6,8 @@ script = f.read() - -def header(username: str, avatar_src: str) -> None: +def header(username: str, avatar_src: str, org: str) -> None: """Header bar.""" s = script.replace('{{avatar-src}}', avatar_src) j = s.replace('{{username}}', username) - html(f"",height=0,) + html(f"",height=0,) diff --git a/web/st_components/page_header/static/header.js b/web/st_components/page_header/static/header.js index 22d970ee..21405e51 100644 --- a/web/st_components/page_header/static/header.js +++ b/web/st_components/page_header/static/header.js @@ -4,19 +4,24 @@ const docqContainer = document.createElement("div"); docqContainer.setAttribute("id", "docq-header-container"); docqContainer.setAttribute( "style", - "display: flex; justify-content: center; align-items: flex-start; position: sticky; top: 0; z-index: 1000; background-color: red; flex-direction: column; padding: 10px; gap: 10px;" + `display: flex; justify-content: center + align-items: flex-end; position: sticky; + top: 0; z-index: 1000; background-color: transparent; left: 0; + flex-direction: column; padding: 10px; gap: 10px; + width: 50%; height: 50px; + ` ); // Avatar const loadAvatar = () => { - const avatar = document.createElement("img"); - avatar.setAttribute("src", "{{avatar-src}}"); - avatar.setAttribute("alt", "user avatar"); - avatar.setAttribute("style", "width: 25px; height: 25px; border-radius: 50%;"); - avatar.setAttribute("id", "docq-avatar"); - avatar.setAttribute("async", "1"); - return avatar; + const avatar = document.createElement("img"); + avatar.setAttribute("src", "{{avatar-src}}"); + avatar.setAttribute("alt", "user avatar"); + avatar.setAttribute("style", "width: 25px; height: 25px; border-radius: 50%;"); + avatar.setAttribute("id", "docq-avatar"); + avatar.setAttribute("async", "1"); + return avatar; } // Avatar container @@ -31,23 +36,23 @@ avatarContainer.appendChild(loadAvatar()); // User name -const userName = document.createElement("p"); -userName.innerText = "{{username}}"; +const userName = document.createElement("span"); +userName.innerHTML = "{{username}}@{{org}}"; userName.setAttribute("id", "docq-user-name"); -userName.setAttribute("style", "margin-right: 10px;"); +userName.setAttribute("style", "margin-right: 20px; margin-left: 16px; width: 100px; text-align: right;"); -parent.addEventListener("load", () => { - const avatar = loadAvatar(); - avatarContainer.appendChild(avatar); - }); +avatarContainer.appendChild(userName); docqContainer.appendChild(avatarContainer); -docqContainer.appendChild(userName); // Insert docq container in the DOM -const headerBar = parent.document.querySelector("header"); -if (headerBar) { - headerBar.insertBefore(docqContainer, headerBar.firstChild); +stApp = window.parent.document.querySelector("header[data-testid='stHeader']"); +if (stApp) { + const prevDocqContainer = window.parent.document.getElementById("docq-header-container"); + if (prevDocqContainer) { + prevDocqContainer.remove(); + } + stApp.insertBefore(docqContainer, stApp.firstChild); } diff --git a/web/st_components/sidebar_header/__init__.py b/web/st_components/sidebar_header/__init__.py index 539c0234..3ce58ecb 100644 --- a/web/st_components/sidebar_header/__init__.py +++ b/web/st_components/sidebar_header/__init__.py @@ -1,10 +1,7 @@ """Nav UI example.""" import json import os -from contextlib import contextmanager -from typing import Self -import streamlit as st import streamlit.components.v1 as components parent_dir = os.path.dirname(os.path.abspath(__file__)) @@ -17,7 +14,6 @@ with open(os.path.join(parent_dir, "static", "sidebar.css")) as f: style = f.read() - def sidebar(options: list) -> None: """SIDE BAR.""" list_str = json.dumps(options) diff --git a/web/st_components/sidebar_header/static/sidebar.js b/web/st_components/sidebar_header/static/sidebar.js index 75044207..8e0017b8 100644 --- a/web/st_components/sidebar_header/static/sidebar.js +++ b/web/st_components/sidebar_header/static/sidebar.js @@ -15,7 +15,7 @@ docqLogoContainer.setAttribute("class", "docq-logo-container"); docqLogoContainer.setAttribute("id", "docq-logo-container"); docqLogoContainer.setAttribute( "style", - "display: flex; justify-content: center; align-items: center; width: 100%; position: sticky; top: 0; z-index: 1000; background-color: red; flex-direction: column; padding: 10px;" + "display: flex; justify-content: center; align-items: center; width: 100%; position: sticky; top: 0; z-index: 1000; background-color: transparent; flex-direction: column; padding: 10px;" ); // Close button @@ -25,9 +25,10 @@ closeButton.setAttribute( "style", "position: absolute; right: 10px; top: 10px; background-color: transparent; border: none; outline: none; cursor: pointer;" ); -closeButton.setAttribute("onclick", "docqClose()"); +closeButton.setAttribute("kind", "header"); closeButton.innerText = "X"; +parent.querySelector("button[kind='header']")?.remove(); docqLogoContainer.appendChild(closeButton); // Logo @@ -43,29 +44,29 @@ docqLogo.setAttribute("async", "1"); docqLogoContainer.appendChild(docqLogo); -// Create a dropdown menu -const menuTitle = document.createElement("label"); -menuTitle.setAttribute("for", "docq-menu"); -menuTitle.innerText = "Select Organization:"; +// Selcted org info +const selectedOrgInfo = document.createElement("div"); +selectedOrgInfo.setAttribute("id", "docq-selected-org-info"); +selectedOrgInfo.setAttribute("style", "margin-top: 10px;"); +selectedOrgInfo.innerHTML = ` + Selected org:
+ + {{org}} + +`; -const dropdownMenu = document.createElement("select"); -dropdownMenu.setAttribute("name", "docq-menu"); -dropdownMenu.setAttribute("id", "docq-menu"); -dropdownMenu.setAttribute( - "style", - "width: 100%; height: 40px; padding: 10px; border-radius: 5px; border: 1px solid #ccc; margin-top: 10" -); +docqLogoContainer.appendChild(selectedOrgInfo); + +// Change org link +const changeOrgButton = document.createElement("a"); +changeOrgButton.setAttribute("id", "docq-change-org-button"); +changeOrgButton.setAttribute("style", "margin-top: 10px;"); +changeOrgButton.setAttribute("href", "http://172.26.68.148:8501/Admin_Orgs"); +// changeOrgButton.setAttribute("target", "_blank"); +changeOrgButton.innerHTML = "Change org"; -const options = JSON.parse('{{org-menu-options}}'); -options.forEach((option) => { - const optionElement = document.createElement("option"); - optionElement.setAttribute("value", option.value); - optionElement.innerText = option.label; - dropdownMenu.appendChild(optionElement); -}); +docqLogoContainer.appendChild(changeOrgButton); -docqLogoContainer.appendChild(menuTitle); -docqLogoContainer.appendChild(dropdownMenu); const sideBar = findSideBar(); diff --git a/web/utils/layout.py b/web/utils/layout.py index 350b9675..341bb863 100644 --- a/web/utils/layout.py +++ b/web/utils/layout.py @@ -317,7 +317,7 @@ def auth_required(show_login_form: bool = True, requiring_admin: bool = False, s if requiring_admin: __not_authorised() return False - + header_ui(auth.get(SessionKeyNameForAuth.NAME.name, "")) return True else: log.debug("auth_required(): No valid auth session found. User needs to re-authenticate.") @@ -988,7 +988,10 @@ def init_with_pretty_error_ui() -> None: st.stop() -def header_ui() -> None: +def header_ui(name: str) -> None: """Header UI.""" avatar_src = handle_get_gravatar_url() - header("DocQ@Test-Org", avatar_src, ) + selected_org_id = get_selected_org_id() + orgs = handle_list_orgs() + selected_org = next((o for o in orgs if o[0] == selected_org_id), None) + header(username=name, avatar_src=avatar_src, org=selected_org[1] if selected_org else None) From f43e22471febc1b2588f5bf7db4131765dc006fd Mon Sep 17 00:00:00 2001 From: Jashon Osala Date: Sun, 8 Oct 2023 14:27:13 +0300 Subject: [PATCH 04/16] feat: Added sidebar header component. --- web/st_components/sidebar_header/__init__.py | 119 +++++++++++++-- .../sidebar_header/static/sidebar.css | 35 ++++- .../sidebar_header/static/sidebar.js | 144 ++++++++++++------ web/st_components/static_utils.py | 11 ++ 4 files changed, 249 insertions(+), 60 deletions(-) create mode 100644 web/st_components/static_utils.py diff --git a/web/st_components/sidebar_header/__init__.py b/web/st_components/sidebar_header/__init__.py index 3ce58ecb..6e02071e 100644 --- a/web/st_components/sidebar_header/__init__.py +++ b/web/st_components/sidebar_header/__init__.py @@ -1,21 +1,120 @@ """Nav UI example.""" import json import os +from typing import Self +import streamlit as st import streamlit.components.v1 as components +from ..static_utils import load_file_variables + parent_dir = os.path.dirname(os.path.abspath(__file__)) script_path = os.path.join(parent_dir, "static", "sidebar.js") +css_path = os.path.join(parent_dir, "static", "sidebar.css") + +class _SideBarHeaderAPI: + """Custom API for the st_components package.""" + + __selected_org: str = None + __org_options_json: str = None + __logo_url: str = None + __header_script: str = None + __header_style: str = None + + def __init__(self: Self,) -> None: + """Initialize the class.""" + self._load_script() + self._load_style() + + @property + def selected_org(self: Self,) -> str: + """Get the currently selected org.""" + return self.__selected_org + + @selected_org.setter + def selected_org(self: Self, value: str) -> None: + """Set the currently selected org.""" + self.__selected_org = value + self._load_script() + + @property + def org_options_json(self: Self,) -> str: + """Get the json string containing available orgs.""" + return self.__org_options_json + + @org_options_json.setter + def org_options_json(self: Self, value: dict) -> None: + """Set the json string containing available orgs.""" + self.__org_options_json = json.dumps(value) + self._load_script() + + @property + def logo_url(self: Self,) -> str: + """Get the URL to logo.""" + return self.__logo_url + + @logo_url.setter + def logo_url(self: Self, value: str) -> None: + """Set the URL to logo.""" + self.__logo_url = value + self._load_script() + + @property + def script(self: Self,) -> str: + """Get the script.""" + return self.__header_script + + @property + def style(self: Self,) -> str: + """Get the style.""" + return self.__header_style + + def _load_script(self: Self,) -> None: + params = { + "selected_org": self.selected_org, + "org_options_json": self.org_options_json, + "logo_url": self.logo_url, + } + self.__header_script = load_file_variables(script_path, params) + + def _load_style(self: Self,) -> None: + params = {} + self.__header_style = load_file_variables(css_path, params) + + +__side_bar_header_api = _SideBarHeaderAPI() + + +def render_sidebar(selected_org: str, org_options: list, logo_url: str = None) -> None: + """Docq sidebar header component. + + Args: + selected_org: The currently selected org. + org_options: json string containing available orgs. + logo_url: URL to logo. + """ + __side_bar_header_api.selected_org = selected_org + __side_bar_header_api.org_options_json = org_options + __side_bar_header_api.logo_url = logo_url + st.markdown(f"", unsafe_allow_html=True) + components.html(f""" + // ST-SIDEBAR-SCRIPT-CONTAINER + + """, + height=0 + ) + + +def set_selected_org(selected_org: str) -> None: + """Set the current org.""" + __side_bar_header_api.selected_org = selected_org + -script, style = "", "" -with open(script_path) as f: - script = f.read() +def update_org_options(org_options: list) -> None: + """Update the org options.""" + __side_bar_header_api.org_options_json = org_options -with open(os.path.join(parent_dir, "static", "sidebar.css")) as f: - style = f.read() -def sidebar(options: list) -> None: - """SIDE BAR.""" - list_str = json.dumps(options) - print(f"\x1b[32m{list_str}\x1b[0m") - components.html(f"", height=0) +def get_selected_org() -> str: + """Get the current org.""" + return st.experimental_get_query_params().get("org", [None])[0] diff --git a/web/st_components/sidebar_header/static/sidebar.css b/web/st_components/sidebar_header/static/sidebar.css index 478eebee..30a0ccd5 100644 --- a/web/st_components/sidebar_header/static/sidebar.css +++ b/web/st_components/sidebar_header/static/sidebar.css @@ -1,6 +1,29 @@ -div[docq-data-sidebar='true'][key='{{key}}'] { - position: sticky ; - top: 0; - width: 100% !important; - background-color: red !important; -} \ No newline at end of file +section[data-testid="stSidebar"] ul { + padding-top: 1rem !important; +} + +section[data-testid="stSidebar"][aria-expanded="true"] button[kind="header"] { + display: none !important; +} + +#docq-org-dropdown { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; + padding: 0 1rem; +} + +#docq-org-dropdown-select { + width: 100%; + height: 2.5rem; + border-radius: 8px; + margin-top: 4px; + padding: 0 1rem; +} + +.docq-select-label { + font-weight: 500; + padding: 0 1rem; +} diff --git a/web/st_components/sidebar_header/static/sidebar.js b/web/st_components/sidebar_header/static/sidebar.js index 8e0017b8..cb64df5d 100644 --- a/web/st_components/sidebar_header/static/sidebar.js +++ b/web/st_components/sidebar_header/static/sidebar.js @@ -1,5 +1,16 @@ parent = window.parent.document || window.document; +// === Required params === +const selectedOrg = "{{selected_org}}"; +const orgOptionsJson = `{{org_options_json}}`; + +// === Optional params === +const logoUrl = "{{logo_url}}"; + + +const matchParamNotSet = /\{\{.*\}\}/g; + +// === Util functions ====================================================================================================================================================== const findSideBar = () => { const sideBar = parent.querySelectorAll('section[data-testid="stSidebar"]'); console.log(`sideBar: ${sideBar}, body: ${parent.body}`); @@ -9,7 +20,24 @@ const findSideBar = () => { return null; }; -// Container for the logo +/** + * Create dropdown option + * @param {string} value dropdown option value + * @param {string} text dropdown option text + * @returns {HTMLOptionElement} HTML option element + */ +function createSelectOption (value, text) { + const option = document.createElement("option"); + option.setAttribute("value", value); + option.setAttribute("class", "docq-select-option") + option.innerHTML = text; + return option; +} + +// === End util functions ============================== + + +// === Container for the logo ============================================================================================================================================= const docqLogoContainer = document.createElement("div"); docqLogoContainer.setAttribute("class", "docq-logo-container"); docqLogoContainer.setAttribute("id", "docq-logo-container"); @@ -18,25 +46,37 @@ docqLogoContainer.setAttribute( "display: flex; justify-content: center; align-items: center; width: 100%; position: sticky; top: 0; z-index: 1000; background-color: transparent; flex-direction: column; padding: 10px;" ); -// Close button +// === Close button ========================================================== +const closeIcon = ``; const closeButton = document.createElement("button"); closeButton.setAttribute("id", "docq-close-button"); closeButton.setAttribute( "style", "position: absolute; right: 10px; top: 10px; background-color: transparent; border: none; outline: none; cursor: pointer;" ); -closeButton.setAttribute("kind", "header"); -closeButton.innerText = "X"; +closeButton.innerHTML = closeIcon; + +// Close sidebar on click +closeButton.addEventListener("click", () => { + const closeBtn = parent.querySelector( + 'section[data-testid="stSidebar"][aria-expanded="true"] button[kind="header"]' + ); + if (closeBtn) { + console.log("Close button found", closeBtn); + closeBtn.click(); + } else { + console.log("Close button not found", closeBtn); + } +}); -parent.querySelector("button[kind='header']")?.remove(); docqLogoContainer.appendChild(closeButton); -// Logo +// === Logo ================================================================================================= const docqLogo = document.createElement("img"); -docqLogo.setAttribute( - "src", - "https://github.com/docqai/docq/blob/main/docs/assets/logo.jpg?raw=true" -); + +const logoSrc = logoUrl && !logoUrl.match(matchParamNotSet) ? logoUrl : "https://github.com/docqai/docq/blob/main/docs/assets/logo.jpg?raw=true" + +docqLogo.setAttribute("src", logoSrc); docqLogo.setAttribute("alt", "docq logo"); docqLogo.setAttribute("style", "width: 50px; height: 50px;"); docqLogo.setAttribute("id", "docq-logo"); @@ -44,47 +84,44 @@ docqLogo.setAttribute("async", "1"); docqLogoContainer.appendChild(docqLogo); -// Selcted org info -const selectedOrgInfo = document.createElement("div"); -selectedOrgInfo.setAttribute("id", "docq-selected-org-info"); -selectedOrgInfo.setAttribute("style", "margin-top: 10px;"); -selectedOrgInfo.innerHTML = ` - Selected org:
- - {{org}} - -`; - -docqLogoContainer.appendChild(selectedOrgInfo); -// Change org link -const changeOrgButton = document.createElement("a"); -changeOrgButton.setAttribute("id", "docq-change-org-button"); -changeOrgButton.setAttribute("style", "margin-top: 10px;"); -changeOrgButton.setAttribute("href", "http://172.26.68.148:8501/Admin_Orgs"); -// changeOrgButton.setAttribute("target", "_blank"); -changeOrgButton.innerHTML = "Change org"; +// === Dropdown menu ========================================================================================== -docqLogoContainer.appendChild(changeOrgButton); +const orgDropdown = document.createElement("div"); +orgDropdown.setAttribute("id", "docq-org-dropdown"); +orgDropdown.setAttribute("style", "margin-top: 10px;"); +const selectLabel = document.createElement("label"); +selectLabel.setAttribute("for", "docq-org-dropdown-select"); +selectLabel.setAttribute("class", "docq-select-label"); +selectLabel.innerHTML = "Select org:"; +selectLabel.setAttribute("style", "margin-right: 10px;"); -const sideBar = findSideBar(); +const selectMenu = document.createElement("select"); +selectMenu.setAttribute("id", "docq-org-dropdown-select"); +selectMenu.setAttribute("onchange", "selectOrg(this.value)"); -// Add close script to parent -const closeScript = document.createElement("script"); -closeScript.innerHTML = ` - function docqClose() { - const sideBar = document.querySelectorAll('section[data-testid="stSidebar"]'); - if (sideBar) { - console.log('Closing sidebar') - sideBar[0].setAttribute("aria-expanded", "false"); - sideBar[0].setAttribute("aria-hidden", "true"); - sideBar[0].setAttribute("style", "display: none;"); - } +if (orgOptionsJson && !orgOptionsJson.match(matchParamNotSet)) { + const orgOptions = JSON.parse(orgOptionsJson); + orgOptions.forEach((org) => { + const option = createSelectOption(org, org); + if (org === selectedOrg) { + option.setAttribute("selected", "selected"); } - `; + selectMenu.appendChild(option); + }); +}; + +orgDropdown.appendChild(selectLabel); +orgDropdown.appendChild(selectMenu); + +if (!selectedOrg.match(matchParamNotSet) && !orgOptionsJson.match(matchParamNotSet)) { + docqLogoContainer.appendChild(orgDropdown); +} + + +const sideBar = findSideBar(); -parent.body.appendChild(closeScript); if (sideBar) { // Check if the logo already exists @@ -94,3 +131,22 @@ if (sideBar) { } sideBar.insertBefore(docqLogoContainer, sideBar.firstChild); } + + +// === Add scripts to parent document === +const selectOrgScript = document.createElement("script"); +selectOrgScript.setAttribute("type", "text/javascript"); +selectOrgScript.setAttribute("id", "docq-select-org-script"); +selectOrgScript.innerHTML = ` + function selectOrg(org) { + const orgParam = encodeURIComponent(org); + window.parent.location.href = \`?org=\${orgParam}\`; + } +`; + +const prevScript = parent.getElementById("docq-select-org-script"); +if (prevScript) { + prevScript.remove(); +} + +parent.body.appendChild(selectOrgScript); diff --git a/web/st_components/static_utils.py b/web/st_components/static_utils.py new file mode 100644 index 00000000..91584ac9 --- /dev/null +++ b/web/st_components/static_utils.py @@ -0,0 +1,11 @@ +"""Utility functions for static files.""" + +def load_file_variables(file_path: str, vars_: dict = None) -> str: + """Load file variables.""" + with open(file_path) as f: + file_str = f.read() + if vars_: + for key, value in vars_.items(): + if value is not None: + file_str = file_str.replace('{{' + key + '}}', value) + return file_str From 955783e7efb1bfa201b647336f1763f9dfeb146a Mon Sep 17 00:00:00 2001 From: Jashon Osala Date: Sun, 8 Oct 2023 14:29:45 +0300 Subject: [PATCH 05/16] chore: Update docstring for the sidebar header api. --- web/st_components/sidebar_header/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/st_components/sidebar_header/__init__.py b/web/st_components/sidebar_header/__init__.py index 6e02071e..4dce1df3 100644 --- a/web/st_components/sidebar_header/__init__.py +++ b/web/st_components/sidebar_header/__init__.py @@ -90,7 +90,7 @@ def render_sidebar(selected_org: str, org_options: list, logo_url: str = None) - Args: selected_org: The currently selected org. - org_options: json string containing available orgs. + org_options: A list containing available orgs for the drop down menu. logo_url: URL to logo. """ __side_bar_header_api.selected_org = selected_org From a010e9ef600363e0003083a0a6023fe79c97e1fa Mon Sep 17 00:00:00 2001 From: Jashon Osala Date: Mon, 9 Oct 2023 11:48:32 +0300 Subject: [PATCH 06/16] feat: Added header bar component + api. --- web/index.py | 20 +- web/st_components/page_header/__init__.py | 130 ++++++++- .../page_header/static/header.css | 229 +++++++++++++++ .../page_header/static/header.js | 268 ++++++++++++++++-- web/utils/layout.py | 23 +- 5 files changed, 616 insertions(+), 54 deletions(-) create mode 100644 web/st_components/page_header/static/header.css diff --git a/web/index.py b/web/index.py index 2e1455ff..1b745664 100644 --- a/web/index.py +++ b/web/index.py @@ -4,7 +4,7 @@ from st_pages import Page, Section, add_page_title, show_pages from utils.layout import init_with_pretty_error_ui, org_selection_ui, production_layout, public_access -from web.st_components.sidebar_header import sidebar +from web.st_components.sidebar_header import render_sidebar init_with_pretty_error_ui() @@ -63,11 +63,17 @@ st.markdown("Enjoy [Docq](https://docq.ai)!") -org_menu_options = [ - {"label": "Org 1", "value": "org1"}, - {"label": "Org 2", "value": "org2"}, - {"label": "Org 3", "value": "org3"}, - {"label": "Org 4", "value": "org4"} + +mock_org_options = [ + "org name 1", + "org name 2", + "org name 3", + "org name 4", ] -sidebar(org_menu_options) +mock_selected_org = "org name 2" + +render_sidebar( + selected_org=mock_selected_org, + org_options=mock_org_options, +) diff --git a/web/st_components/page_header/__init__.py b/web/st_components/page_header/__init__.py index 444f1dc3..ce79d65a 100644 --- a/web/st_components/page_header/__init__.py +++ b/web/st_components/page_header/__init__.py @@ -1,13 +1,127 @@ """Header bar.""" +import json +import os +from contextlib import contextmanager +from typing import Self + +import streamlit as st from streamlit.components.v1 import html -script = "" -with open("web/st_components/page_header/static/header.js") as f: - script = f.read() +from ..static_utils import load_file_variables + +parent_dir = os.path.dirname(os.path.abspath(__file__)) +script_path = os.path.join(parent_dir, "static", "header.js") +css_path = os.path.join(parent_dir, "static", "header.css") + + +class _PageHeaderAPI: + """Page header bar API.""" + + __menu_options_json: str = None + __menu_options_list: list = [] # [{"text": "Home", "key": "home"}] + __username: str = None + __avatar_src: str = None + __page_script: str = None + __page_style: str = None + + def __init__(self: Self,) -> None: + """Initialize the class.""" + self._load_script() + self._load_style() + + @property + def menu_options_json(self: Self,) -> str: + """Get the json string containing available menu options.""" + return self.__menu_options_json + + @property + def menu_options_list(self: Self,) -> dict: + """Get the dict containing available menu options.""" + return self.__menu_options_list + + @property + def username(self: Self,) -> str: + """Get the username.""" + return self.__username + + @property + def avatar_src(self: Self,) -> str: + """Get the avatar source.""" + return self.__avatar_src + + @property + def script(self: Self,) -> str: + """Get the script.""" + return self.__page_script + + @property + def style(self: Self,) -> str: + """Get the style.""" + return self.__page_style + + @menu_options_list.setter + def menu_options_list(self: Self, value: list) -> None: + """Set the dict containing available menu options.""" + self.__menu_options_list = value + self.__menu_options_json = json.dumps(value) + self._load_script() + + @username.setter + def username(self: Self, value: str) -> None: + """Set the username.""" + self.__username = value + self._load_script() + + @avatar_src.setter + def avatar_src(self: Self, value: str) -> None: + """Set the avatar source.""" + self.__avatar_src = value + self._load_script() + + def add_menu_option(self: Self, label: str, key: str, icon_html_: str = None) -> None: + """Add a menu option.""" + for entry in self.__menu_options_list: + if entry["text"] == label and entry["key"] == key: + return + self.__menu_options_list.append({"text": label, "key": key}) + self.__menu_options_json = json.dumps(self.__menu_options_list) + self._load_script() + self._load_style() + + def _load_script(self: Self,) -> None: + """Load the script.""" + script_args = { + "username": self.__username, + "avatar_src": self.__avatar_src, + "menu_items_json": self.__menu_options_json, + } + self.__page_script = load_file_variables(script_path, script_args) + + def _load_style(self: Self,) -> None: + """Load the style.""" + self.__page_style = load_file_variables(css_path) + + + +__page_header_api = _PageHeaderAPI() + +def render_header(username: str, avatar_src: str) -> None: + """Header bar. + + Args: + username (str): Username. + avatar_src (str): Avatar source. + """ + __page_header_api.username = username + __page_header_api.avatar_src = avatar_src + st.markdown(f"", unsafe_allow_html=True) + html(f"",height=0,) -def header(username: str, avatar_src: str, org: str) -> None: - """Header bar.""" - s = script.replace('{{avatar-src}}', avatar_src) - j = s.replace('{{username}}', username) - html(f"",height=0,) +@contextmanager +def menu_option(label: str, key: str = None) -> None: + """Add a menu option.""" + f_label = label.strip().replace(" ", "_").lower() + __button_key = st.button(label=f_label, key=key, type="primary") + __page_header_api.add_menu_option(label=label, key=f_label) + yield __button_key diff --git a/web/st_components/page_header/static/header.css b/web/st_components/page_header/static/header.css new file mode 100644 index 00000000..a1165209 --- /dev/null +++ b/web/st_components/page_header/static/header.css @@ -0,0 +1,229 @@ + +/* Color variables */ +:root { + --status-widget-color: #00bfa5; + --status-widget-background-color: #7d8181; +} + +/** Animate user menu */ + +@keyframes docq-user-menu-slide-in { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes docq-user-menu-slide-out { + from { + opacity: 1; + transform: translateY(0); + } + to { + opacity: 0; + transform: translateY(-10px); + } + +} + +@keyframes tooltip-slide-in { + from { + opacity: 0; + transform: translateX(10px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes tooltip-slide-out { + from { + opacity: 1; + transform: translateX(0); + } + to { + opacity: 0; + transform: translateX(10px); + } +} + + +div.appview-container div.row-widget.stButton button[kind="primary"] { + display: none !important; +} + +#docq-header-container { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 1rem; + height: 3rem; + background-color: transparent; + flex-direction: row; +} + +.docq-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0 1rem; + height: 3rem; + background-color: transparent; + flex-direction: row; + width: 30%; + gap: 1rem; +} + +#docq-header-left { + justify-content: flex-end; + margin-right: 3rem; +} + +#docq-header-center { + justify-content: center; +} + +#docq-header-right { + justify-content: flex-start; +} + +#docq-img-avatar { + width: 20px; + height: 20px; + border-radius: 50%; + cursor: pointer; +} + + +/* New Chat floating button */ +#docq-new-chat-button-container { + position: fixed; + bottom: 70px; + right: 30px; + width: 40px; + height: 40px; + display: flex; + justify-content: center; + align-items: center; + background-color: transparent; + z-index: 1000; +} + +#docq-new-chat-button { + bottom: 70px; + right: 80%; + width: 40px; + height: 40px; + border-radius: 50%; + background-color: #00bfa5; + color: white; + font-size: 40px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + z-index: 1000; +} + +#docq-new-chat-button:active { + border: none; +} + +#docq-new-chat-tooltip { + position: absolute; + top: -35px; + right: -26px; + background-color: var(--status-widget-background-color); + padding: 2px 10px; + border-radius: 5px; + width: 100px; + display: flex; + justify-content: center; + align-items: center; + visibility: hidden; + animation: tooltip-slide-out 0.2s ease-in-out; +} + +#docq-new-chat-button-container:hover #docq-new-chat-tooltip { + visibility: visible; + animation: tooltip-slide-in 0.2s ease-in-out; +} + +div[data-testid="stStatusWidget"] { + background-color: var(--status-widget-background-color) !important; +} + +.docq-user-menu { + position: absolute; + top: 3rem; + right: 1rem; + width: fit-content; + min-width: 200px; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.2); + display: none; + flex-direction: column; + justify-content: space-between; + align-items: center; + padding: 0.5rem 0; + z-index: 1000; + visibility: hidden; +} + +.docq-user-menu-active { + visibility: visible; + display: flex; +} + + +.docq-user-menu-item { + all: none; + width: 100%; + display: flex; + justify-content: flex-start; + align-items: center; + padding: 2px 0.5rem; + cursor: pointer; + border: none; + outline: none; + background-color: transparent; +} + +.docq-user-menu-item:active, .docq-user-menu-item:focus { + border: none; + outline: none; +} + +.docq-user-menu-item:focus { + opacity: 0.5; +} + + +.docq-user-menu-item:hover { + background-color: #00bfa5; +} + +.docq-user-menu-item-icon { + margin-right: 0.5rem; + width: 1.25rem; +} + +.docq-user-menu-profile { + display: flex; + justify-content: flex-start; + align-items: center; + flex-direction: row; + gap: 0.5rem; +} + +.docq-user-menu-divider { + width: 100%; + height: 1px; + background-color: #e31010; + margin: 0.5rem 0; +} diff --git a/web/st_components/page_header/static/header.js b/web/st_components/page_header/static/header.js index 21405e51..846f1b3a 100644 --- a/web/st_components/page_header/static/header.js +++ b/web/st_components/page_header/static/header.js @@ -1,25 +1,83 @@ +parent = window.parent.document || window.document; + +// Get params === These are to be set in the template by the method that loads this script +const username = "{{username}}"; +const avatarSrc = "{{avatar_src}}"; +const menuItemsJson = `{{menu_items_json}}`; // [{ "text": "Menu item text", "key": "menu-item-button-key", "icon": "menu-item-icon-html"}] + +const matchParamNotSet = /\{\{.*\}\}/; + +const defaultMenuItemIcon = ` ` + + +/** Utility functions */// ========================================================================================================================================================== + +/** + * Inserts a class to the svg element in the menu item html + * @param {string} menuItemHtml The html string of the menu item + * @param {string} iconClass Icon class to be added to the svg element (default: 'docq-user-menu-item-icon') + * @returns {string} The html string of the menu item with the icon class added to the svg element + */ +function insertUserMenuItemIconClass(menuItemHtml, iconClass = 'docq-user-menu-item-icon'){ + const parser = new DOMParser() + const doc = parser.parseFromString(menuItemHtml, 'text/html') + const svg = doc.querySelector('svg') + svg.setAttribute('class', iconClass) + return doc.body.innerHTML +} + +/** + * Creates a user menu item using the given text and icon html string + * @param {string} text The text to be displayed in the menu item + * @param {string} imgHtml + * @returns {HTMLButtonElement} The user menu item + */ +function createUserMenuItem(text, imgHtml = null){ + const item = parent.createElement('button') + item.setAttribute('class', 'docq-user-menu-item') + item.setAttribute('id', `docq-user-menu-item-${text.replace(' ', '-')}`) + if (imgHtml) { + const iconWithClass = insertUserMenuItemIconClass(imgHtml) + item.innerHTML = `${iconWithClass}${text}` + } else { + item.innerHTML = `${text}` + } + return item +} + +/** + * Creates a horizontal divider for the user menu + * @returns {HTMLDivElement} The horizontal divider + */ +function createHorizontalDivider(){ + const divider = parent.createElement('div') + divider.setAttribute('class', 'docq-user-menu-divider') + return divider +} + +// End utility functions ============================================================================================================================================================== // Container const docqContainer = document.createElement("div"); docqContainer.setAttribute("id", "docq-header-container"); -docqContainer.setAttribute( - "style", - `display: flex; justify-content: center - align-items: flex-end; position: sticky; - top: 0; z-index: 1000; background-color: transparent; left: 0; - flex-direction: column; padding: 10px; gap: 10px; - width: 50%; height: 50px; - ` -); + + +// Create header divs +const [left, center, right] = ["left", "center", "right"].map((id) => { + const div = document.createElement("div"); + div.setAttribute("id", `docq-header-${id}`); + div.setAttribute("class", `docq-header header-${id}`); + return div; +}); // Avatar const loadAvatar = () => { const avatar = document.createElement("img"); - avatar.setAttribute("src", "{{avatar-src}}"); - avatar.setAttribute("alt", "user avatar"); - avatar.setAttribute("style", "width: 25px; height: 25px; border-radius: 50%;"); - avatar.setAttribute("id", "docq-avatar"); + avatar.setAttribute("src", avatarSrc); + avatar.setAttribute("alt", "user-avatar"); + avatar.setAttribute("style", "width: 20px; height: 20px;"); + avatar.setAttribute("id", "docq-img-avatar"); avatar.setAttribute("async", "1"); return avatar; } @@ -27,32 +85,188 @@ const loadAvatar = () => { // Avatar container const avatarContainer = document.createElement("div"); avatarContainer.setAttribute("id", "docq-avatar-container"); -avatarContainer.setAttribute( - "style", - "position: absolute; right: 10px; top: 10px; background-color: transparent; border: none; outline: none; cursor: pointer;" -); -avatarContainer.setAttribute("onclick", "docqToggle()"); -avatarContainer.appendChild(loadAvatar()); -// User name +const avatar = loadAvatar(); +avatarContainer.appendChild(avatar); + +// User menu ======================================================================================================================================================================== +const userMenu = document.createElement("div"); +userMenu.setAttribute("id", "docq-user-menu"); +userMenu.setAttribute("class", "docq-user-menu"); + +// Usermenu items ========================================================================== + +// Profile ================================================================================= +const userProfile = document.createElement("div"); +userProfile.setAttribute("id", "docq-user-menu-profile"); +userProfile.setAttribute("class", "docq-user-menu-profile"); +userProfile.innerHTML = ``; + +// Logout =================================================================================== +const logoutImgHtml = ` ` +const logoutBtn = createUserMenuItem("Logout", logoutImgHtml) +logoutBtn.addEventListener("click", () => { + const btns = parent.querySelectorAll('button[kind="primary"]'); + const logoutBtn = Array.from(btns).find((btn) => btn.innerText === "Logout"); + if (logoutBtn) { + logoutBtn.click(); + } else { + console.log("Logout button not found", logoutBtn); + } +}) + +/** Help and Feedback section */ +// Help ===================================================================================== +const helpSvgHtml = ` ` +const helpBtn = createUserMenuItem("Help", helpSvgHtml) +helpBtn.addEventListener("click", () => { + window.open("https://docq.ai", "_blank"); +}); + +// Send feedback =========================================================================== +const feedbackSvgHtml = `` +const feedbackBtn = createUserMenuItem("Send feedback", feedbackSvgHtml) +feedbackBtn.addEventListener("click", () => { + window.open("https://docq.ai", "_blank"); +}); + +// Add items to user menu +userMenu.appendChild(userProfile); +userMenu.appendChild(createHorizontalDivider()) +userMenu.appendChild(logoutBtn) +// Add menu items from json +if (!matchParamNotSet.test(menuItemsJson)) { + const menuItems = JSON.parse(menuItemsJson) + menuItems.forEach(item => { + const icon = item?.icon || defaultMenuItemIcon + const menuItem = createUserMenuItem(item.text, icon) + menuItem.addEventListener('click', () => { + const btns = parent.querySelectorAll('button[kind="primary"]'); + const menuItemBtn = Array.from(btns).find((btn) => btn.innerText === item.key); + if (menuItemBtn) { + menuItemBtn.click(); + } else { + console.log(`Menu item button with key ${item.key} not found`, menuItemBtn); + } + }) + userMenu.appendChild(menuItem) + }) +} + +userMenu.appendChild(createHorizontalDivider()) +userMenu.appendChild(helpBtn); +userMenu.appendChild(feedbackBtn); + +// Add user menu to avatar container +avatarContainer.appendChild(userMenu); + +// User menu toggle +avatar.addEventListener("click", () => { + const userMenu = parent.getElementById("docq-user-menu"); + if (userMenu) { + console.log("User menu found", userMenu); + userMenu.classList.toggle("docq-user-menu-active"); + // Autofocus on the user menu + const userMenuItems = userMenu.querySelectorAll(".docq-user-menu-item"); + if (userMenuItems.length > 0) { + userMenuItems[0].focus(); + } + + // Close user menu on click outside + const closeUserMenu = (e) => { + if (!userMenu.contains(parent.activeElement)) { + userMenu.classList.remove("docq-user-menu-active"); + parent.removeEventListener("click", closeUserMenu); + } + }; + parent.addEventListener("click", closeUserMenu); + } else { + console.log("User menu not found", userMenu); + } +}); + +// User menu animation +const userMenuObserver = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.attributeName === "class") { + const userMenu = parent.getElementById("docq-user-menu"); + if (userMenu) { + if (userMenu.classList.contains("docq-user-menu-active")) { + userMenu.style.animation = "docq-user-menu-slide-in 0.2s ease-in-out"; + } else { + userMenu.style.animation = "docq-user-menu-slide-out 0.2s ease-in-out"; + } + } + } + }); +}); + +userMenuObserver.observe(userMenu, { attributes: true }); + +// End user menu ==================================================================================================================================================================== + + +/* User name */ const userName = document.createElement("span"); -userName.innerHTML = "{{username}}@{{org}}"; +userName.innerHTML = `${username}`; userName.setAttribute("id", "docq-user-name"); -userName.setAttribute("style", "margin-right: 20px; margin-left: 16px; width: 100px; text-align: right;"); + +if(!matchParamNotSet.test(username)) { + left.appendChild(userName); +} +if (!matchParamNotSet.test(avatarSrc)) { + left.appendChild(avatarContainer); +} -avatarContainer.appendChild(userName); +// Page title +const pageTitle = document.createElement("span"); -docqContainer.appendChild(avatarContainer); + + +// Insert docq left, center, right divs +[right, center, left].forEach((div) => docqContainer.appendChild(div)); + + +// New chat button container +const newChatButtonContainer = document.createElement("div"); +newChatButtonContainer.setAttribute("id", "docq-new-chat-button-container"); +newChatButtonContainer.setAttribute("class", "docq-new-chat-button-container"); + +// New chat button +const newChatButton = document.createElement("button"); +newChatButton.setAttribute("id", "docq-new-chat-button"); +newChatButton.setAttribute("class", "docq-new-chat-button"); +newChatButton.innerHTML = "+"; +newChatButton.addEventListener("click", () => { + const btns = parent.querySelectorAll('button[kind="secondary"]'); + const newChatBtn = Array.from(btns).find((btn) => btn.innerText.toLowerCase() === "new chat"); + if (newChatBtn) { + newChatBtn.click(); + } else { + console.log("New chat button not found", newChatBtn); + } +}); + + + +const newChatTooltip = document.createElement("span"); +newChatTooltip.setAttribute("id", "docq-new-chat-tooltip"); +newChatTooltip.setAttribute("class", "docq-new-chat-tooltip"); +newChatTooltip.innerHTML = "New chat"; + +newChatButtonContainer.appendChild(newChatTooltip); +newChatButtonContainer.appendChild(newChatButton); + +parent.body.appendChild(newChatButtonContainer); // Insert docq container in the DOM -stApp = window.parent.document.querySelector("header[data-testid='stHeader']"); +stApp = parent.querySelector("header[data-testid='stHeader']"); if (stApp) { - const prevDocqContainer = window.parent.document.getElementById("docq-header-container"); + const prevDocqContainer = parent.getElementById("docq-header-container"); if (prevDocqContainer) { prevDocqContainer.remove(); } stApp.insertBefore(docqContainer, stApp.firstChild); } - diff --git a/web/utils/layout.py b/web/utils/layout.py index 341bb863..d14baff5 100644 --- a/web/utils/layout.py +++ b/web/utils/layout.py @@ -22,7 +22,7 @@ from streamlit.components.v1 import html from streamlit.delta_generator import DeltaGenerator -from web.st_components.page_header import header +from web.st_components.page_header import menu_option, render_header from .constants import ALLOWED_DOC_EXTS, SessionKeyNameForAuth, SessionKeyNameForChat from .error_ui import _handle_error_state_ui @@ -260,7 +260,7 @@ def __login_form() -> None: def __logout_button() -> None: - if st.button("Logout"): + if st.button("Logout", type="primary"): handle_logout() st.experimental_rerun() @@ -599,14 +599,16 @@ def chat_ui(feature: FeatureKey) -> None: ) st.checkbox("Including your documents", value=True, key="chat_personal_space") - load_history, create_new_chat = st.columns([3, 1]) - with load_history: + _, chat_histoy, _ = st.columns([1,1,1]) + with chat_histoy: if st.button("Load chat history earlier"): query_chat_history(feature) - with create_new_chat: - if st.button("New chat"): - handle_create_new_chat(feature) - with st.container(): + if st.button("New chat", type="primary"): + handle_create_new_chat(feature) + + with menu_option("Chat Settings"): + print("\x1b[31mChat settings test\x1b[0m") + day = format_datetime(get_chat_session(feature.type_, SessionKeyNameForChat.CUTOFF)) st.markdown(f"#### {day}") @@ -991,7 +993,4 @@ def init_with_pretty_error_ui() -> None: def header_ui(name: str) -> None: """Header UI.""" avatar_src = handle_get_gravatar_url() - selected_org_id = get_selected_org_id() - orgs = handle_list_orgs() - selected_org = next((o for o in orgs if o[0] == selected_org_id), None) - header(username=name, avatar_src=avatar_src, org=selected_org[1] if selected_org else None) + render_header(username=name, avatar_src=avatar_src) From 0c3d502038586be3d27e36e276a2f65422ee3477 Mon Sep 17 00:00:00 2001 From: Jashon Osala Date: Mon, 9 Oct 2023 12:07:16 +0300 Subject: [PATCH 07/16] chore: Update page header component. --- web/st_components/page_header/__init__.py | 15 ++--------- .../page_header/static/header.js | 27 +++++++++++++++++-- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/web/st_components/page_header/__init__.py b/web/st_components/page_header/__init__.py index ce79d65a..2fba5e15 100644 --- a/web/st_components/page_header/__init__.py +++ b/web/st_components/page_header/__init__.py @@ -26,8 +26,8 @@ class _PageHeaderAPI: def __init__(self: Self,) -> None: """Initialize the class.""" + self.__page_style = load_file_variables(css_path, {}) self._load_script() - self._load_style() @property def menu_options_json(self: Self,) -> str: @@ -54,11 +54,6 @@ def script(self: Self,) -> str: """Get the script.""" return self.__page_script - @property - def style(self: Self,) -> str: - """Get the style.""" - return self.__page_style - @menu_options_list.setter def menu_options_list(self: Self, value: list) -> None: """Set the dict containing available menu options.""" @@ -86,7 +81,6 @@ def add_menu_option(self: Self, label: str, key: str, icon_html_: str = None) -> self.__menu_options_list.append({"text": label, "key": key}) self.__menu_options_json = json.dumps(self.__menu_options_list) self._load_script() - self._load_style() def _load_script(self: Self,) -> None: """Load the script.""" @@ -94,14 +88,10 @@ def _load_script(self: Self,) -> None: "username": self.__username, "avatar_src": self.__avatar_src, "menu_items_json": self.__menu_options_json, + "style_doc": self.__page_style, } self.__page_script = load_file_variables(script_path, script_args) - def _load_style(self: Self,) -> None: - """Load the style.""" - self.__page_style = load_file_variables(css_path) - - __page_header_api = _PageHeaderAPI() @@ -114,7 +104,6 @@ def render_header(username: str, avatar_src: str) -> None: """ __page_header_api.username = username __page_header_api.avatar_src = avatar_src - st.markdown(f"", unsafe_allow_html=True) html(f"",height=0,) diff --git a/web/st_components/page_header/static/header.js b/web/st_components/page_header/static/header.js index 846f1b3a..3e12d563 100644 --- a/web/st_components/page_header/static/header.js +++ b/web/st_components/page_header/static/header.js @@ -1,15 +1,38 @@ parent = window.parent.document || window.document; // Get params === These are to be set in the template by the method that loads this script -const username = "{{username}}"; -const avatarSrc = "{{avatar_src}}"; +const username = "{{username}}"; // User name to be displayed in the header +const avatarSrc = "{{avatar_src}}"; // Avatar image source const menuItemsJson = `{{menu_items_json}}`; // [{ "text": "Menu item text", "key": "menu-item-button-key", "icon": "menu-item-icon-html"}] +const styleDoc = `{{style_doc}}`; // CSS string to be added to the parent.document.head const matchParamNotSet = /\{\{.*\}\}/; const defaultMenuItemIcon = ` ` +// Add style to the parent document head +if (!matchParamNotSet.test(styleDoc)) { + const style = parent.createElement("style"); + style.setAttribute("id", "docq-header-style"); + // check if style tag already exists and verify if it is the same as the one to be added + const prevStyle = parent.getElementById("docq-header-style"); + if (prevStyle) { + if (prevStyle.innerHTML === styleDoc) { + console.log("Style already exists"); + } else { + console.log("Style exists but is different"); + prevStyle.remove(); + style.innerHTML = styleDoc; + parent.head.appendChild(style); + } + } else { + style.innerHTML = styleDoc; + parent.head.appendChild(style); + } +} + + /** Utility functions */// ========================================================================================================================================================== /** From c98292c0bcada1b9627d65680473428d1b315fe6 Mon Sep 17 00:00:00 2001 From: Jashon Osala Date: Mon, 9 Oct 2023 12:53:28 +0300 Subject: [PATCH 08/16] chore: Update st_components. --- web/index.py | 1 + .../page_header/static/header.js | 4 +--- web/st_components/sidebar_header/__init__.py | 15 ++++++------- .../sidebar_header/static/sidebar.js | 22 ++++++++++++++++++- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/web/index.py b/web/index.py index 1b745664..fa30941d 100644 --- a/web/index.py +++ b/web/index.py @@ -73,6 +73,7 @@ mock_selected_org = "org name 2" + render_sidebar( selected_org=mock_selected_org, org_options=mock_org_options, diff --git a/web/st_components/page_header/static/header.js b/web/st_components/page_header/static/header.js index 3e12d563..4ec5509e 100644 --- a/web/st_components/page_header/static/header.js +++ b/web/st_components/page_header/static/header.js @@ -22,9 +22,7 @@ if (!matchParamNotSet.test(styleDoc)) { console.log("Style already exists"); } else { console.log("Style exists but is different"); - prevStyle.remove(); - style.innerHTML = styleDoc; - parent.head.appendChild(style); + prevStyle.innerHTML = styleDoc; } } else { style.innerHTML = styleDoc; diff --git a/web/st_components/sidebar_header/__init__.py b/web/st_components/sidebar_header/__init__.py index 4dce1df3..15a7e6f1 100644 --- a/web/st_components/sidebar_header/__init__.py +++ b/web/st_components/sidebar_header/__init__.py @@ -1,4 +1,5 @@ """Nav UI example.""" +import base64 import json import os from typing import Self @@ -23,8 +24,8 @@ class _SideBarHeaderAPI: def __init__(self: Self,) -> None: """Initialize the class.""" + self.__header_style = load_file_variables(css_path, {}) self._load_script() - self._load_style() @property def selected_org(self: Self,) -> str: @@ -74,13 +75,10 @@ def _load_script(self: Self,) -> None: "selected_org": self.selected_org, "org_options_json": self.org_options_json, "logo_url": self.logo_url, + "style_doc": self.style, } self.__header_script = load_file_variables(script_path, params) - def _load_style(self: Self,) -> None: - params = {} - self.__header_style = load_file_variables(css_path, params) - __side_bar_header_api = _SideBarHeaderAPI() @@ -96,7 +94,6 @@ def render_sidebar(selected_org: str, org_options: list, logo_url: str = None) - __side_bar_header_api.selected_org = selected_org __side_bar_header_api.org_options_json = org_options __side_bar_header_api.logo_url = logo_url - st.markdown(f"", unsafe_allow_html=True) components.html(f""" // ST-SIDEBAR-SCRIPT-CONTAINER @@ -115,6 +112,8 @@ def update_org_options(org_options: list) -> None: __side_bar_header_api.org_options_json = org_options -def get_selected_org() -> str: +def get_selected_org_from_ui() -> str: """Get the current org.""" - return st.experimental_get_query_params().get("org", [None])[0] + org = st.experimental_get_query_params().get("org", [None])[0] + if org is not None: + return base64.b64decode(org).decode("utf-8") diff --git a/web/st_components/sidebar_header/static/sidebar.js b/web/st_components/sidebar_header/static/sidebar.js index cb64df5d..2640a236 100644 --- a/web/st_components/sidebar_header/static/sidebar.js +++ b/web/st_components/sidebar_header/static/sidebar.js @@ -3,6 +3,7 @@ parent = window.parent.document || window.document; // === Required params === const selectedOrg = "{{selected_org}}"; const orgOptionsJson = `{{org_options_json}}`; +const styleDoc = `{{style_doc}}`; // === Optional params === const logoUrl = "{{logo_url}}"; @@ -10,6 +11,25 @@ const logoUrl = "{{logo_url}}"; const matchParamNotSet = /\{\{.*\}\}/g; + +// === Style document ====================================================================================================================================================== +const styleDocElement = document.createElement("style"); +styleDocElement.setAttribute("id", "docq-sidebar-style-doc"); + +if (!matchParamNotSet.test(styleDoc)) { + styleDocElement.innerHTML = styleDoc; + const prevStyleDoc = parent.getElementById("docq-sidebar-style-doc"); + if (prevStyleDoc) { + if (prevStyleDoc.innerHTML !== styleDoc) { + prevStyleDoc.innerHTML = styleDoc; + } else { + console.log("Style doc already exists"); + } + } else { + parent.head.appendChild(styleDocElement); + } +} + // === Util functions ====================================================================================================================================================== const findSideBar = () => { const sideBar = parent.querySelectorAll('section[data-testid="stSidebar"]'); @@ -139,7 +159,7 @@ selectOrgScript.setAttribute("type", "text/javascript"); selectOrgScript.setAttribute("id", "docq-select-org-script"); selectOrgScript.innerHTML = ` function selectOrg(org) { - const orgParam = encodeURIComponent(org); + const orgParam = encodeURIComponent(btoa(org)); window.parent.location.href = \`?org=\${orgParam}\`; } `; From ea6ff762cb8ec1c1a3ae936205895004bc0f904e Mon Sep 17 00:00:00 2001 From: Jashon Osala Date: Tue, 10 Oct 2023 03:23:29 +0300 Subject: [PATCH 09/16] chore: Update st_components. --- web/admin_logs.py | 5 ++ web/admin_orgs.py | 5 +- web/admin_settings.py | 5 ++ web/admin_space_groups.py | 4 + web/admin_spaces.py | 4 + web/admin_user_groups.py | 5 +- web/admin_users.py | 6 +- web/index.py | 13 ++- web/personal_ask.py | 5 +- web/personal_chat.py | 6 +- web/personal_docs.py | 5 +- web/shared_ask.py | 5 +- web/shared_spaces.py | 4 + web/st_components/page_header/__init__.py | 77 ++++++++++++++++-- .../page_header/static/header.css | 21 +++-- .../page_header/static/header.js | 80 +++++++++++++------ web/st_components/sidebar_header/__init__.py | 20 +++-- .../sidebar_header/static/sidebar.css | 2 + .../sidebar_header/static/sidebar.js | 25 +++++- web/st_components/static_utils.py | 25 ++++++ web/utils/layout.py | 7 +- 21 files changed, 269 insertions(+), 60 deletions(-) diff --git a/web/admin_logs.py b/web/admin_logs.py index d6f0d719..b1c2dccd 100644 --- a/web/admin_logs.py +++ b/web/admin_logs.py @@ -1,10 +1,13 @@ """Page: Admin / View Logs.""" +import st_components.page_header as st_header import streamlit as st from docq.config import LogType from st_pages import add_page_title from utils.layout import auth_required, list_logs_ui +st_header._setup_page_script() + auth_required(requiring_admin=True) add_page_title() @@ -15,3 +18,5 @@ with tab: st.subheader(type_.value) list_logs_ui(type_) + +st_header.run_script() diff --git a/web/admin_orgs.py b/web/admin_orgs.py index d9cac123..b771539a 100644 --- a/web/admin_orgs.py +++ b/web/admin_orgs.py @@ -1,11 +1,14 @@ """Page: Admin / Manage Orgs.""" - +import st_components.page_header as st_header from st_pages import add_page_title from utils.layout import auth_required, create_org_ui, list_orgs_ui +st_header._setup_page_script() auth_required(requiring_admin=True) add_page_title() create_org_ui() list_orgs_ui() + +st_header.run_script() diff --git a/web/admin_settings.py b/web/admin_settings.py index 51392721..b1aee01b 100644 --- a/web/admin_settings.py +++ b/web/admin_settings.py @@ -1,10 +1,15 @@ """Page: Admin / Manage Settings.""" +import st_components.page_header as st_header from st_pages import add_page_title from utils.layout import auth_required, system_settings_ui +st_header._setup_page_script() + auth_required(requiring_admin=True) add_page_title() system_settings_ui() + +st_header.run_script() diff --git a/web/admin_space_groups.py b/web/admin_space_groups.py index 1412c3e4..75bbd5eb 100644 --- a/web/admin_space_groups.py +++ b/web/admin_space_groups.py @@ -1,11 +1,15 @@ """Page: Admin / Manage Space Groups.""" +import st_components.page_header as st_header from st_pages import add_page_title from utils.layout import auth_required, create_space_group_ui, list_space_groups_ui +st_header._setup_page_script() auth_required(requiring_admin=True) add_page_title() create_space_group_ui() list_space_groups_ui() + +st_header.run_script() diff --git a/web/admin_spaces.py b/web/admin_spaces.py index 91d6cf5a..0280118a 100644 --- a/web/admin_spaces.py +++ b/web/admin_spaces.py @@ -1,7 +1,9 @@ """Page: Admin / Manage Documents.""" +import st_components.page_header as st_header from st_pages import add_page_title from utils.layout import admin_docs_ui, auth_required, create_space_ui +st_header._setup_page_script() auth_required(requiring_admin=True) add_page_title() @@ -12,3 +14,5 @@ create_space_ui() admin_docs_ui(PARAM_NAME) + +st_header.run_script() diff --git a/web/admin_user_groups.py b/web/admin_user_groups.py index 021d2b07..466da8b8 100644 --- a/web/admin_user_groups.py +++ b/web/admin_user_groups.py @@ -1,11 +1,14 @@ """Page: Admin / Manage User Groups.""" - +import st_components.page_header as st_header from st_pages import add_page_title from utils.layout import auth_required, create_user_group_ui, list_user_groups_ui +st_header._setup_page_script() auth_required(requiring_admin=True) add_page_title() create_user_group_ui() list_user_groups_ui() + +st_header.run_script() diff --git a/web/admin_users.py b/web/admin_users.py index 5bbf6a43..6a4741b4 100644 --- a/web/admin_users.py +++ b/web/admin_users.py @@ -1,10 +1,10 @@ """Page: Admin / Manage Users.""" - -import logging +import st_components.page_header as st_header import streamlit as st from st_pages import add_page_title from utils.layout import auth_required, create_user_ui, list_users_ui, org_selection_ui +st_header._setup_page_script() auth_required(requiring_admin=True) add_page_title() @@ -14,3 +14,5 @@ create_user_ui() list_users_ui() + +st_header.run_script() diff --git a/web/index.py b/web/index.py index fa30941d..b9bb5737 100644 --- a/web/index.py +++ b/web/index.py @@ -1,6 +1,8 @@ """Page: Home (no auth required).""" - +import st_components.page_header as st_header import streamlit as st +from docq import setup +from st_components.sidebar_header import get_selected_org_from_ui, render_sidebar from st_pages import Page, Section, add_page_title, show_pages from utils.layout import init_with_pretty_error_ui, org_selection_ui, production_layout, public_access @@ -36,6 +38,8 @@ ] ) +st_header._setup_page_script() + public_access() add_page_title() @@ -78,3 +82,10 @@ selected_org=mock_selected_org, org_options=mock_org_options, ) + +selected_org = get_selected_org_from_ui() +if selected_org is not None: + print("\x1b[31m", "selected_org", selected_org, "\x1b[0m") + st.experimental_set_query_params() + +st_header.run_script() diff --git a/web/personal_ask.py b/web/personal_ask.py index d31f68cf..2ea6a2c7 100644 --- a/web/personal_ask.py +++ b/web/personal_ask.py @@ -1,11 +1,12 @@ """Page: Personal / Ask Your Documents.""" - +import st_components.page_header as st_header from docq.config import FeatureType from docq.domain import FeatureKey from st_pages import add_page_title from utils.layout import auth_required, chat_ui, feature_enabled, header_ui from utils.sessions import get_authenticated_user_id +st_header._setup_page_script() auth_required() feature_enabled(FeatureType.ASK_PERSONAL) @@ -15,3 +16,5 @@ feature = FeatureKey(FeatureType.ASK_PERSONAL, get_authenticated_user_id()) chat_ui(feature) + +st_header.run_script() diff --git a/web/personal_chat.py b/web/personal_chat.py index 416b7dcb..f5cdf0d5 100644 --- a/web/personal_chat.py +++ b/web/personal_chat.py @@ -1,11 +1,12 @@ """Page: Personal / General Chat.""" - +import st_components.page_header as st_header from docq.config import FeatureType from docq.domain import FeatureKey from st_pages import add_page_title from utils.layout import auth_required, chat_ui, feature_enabled from utils.sessions import get_authenticated_user_id +st_header._setup_page_script() auth_required() feature_enabled(FeatureType.CHAT_PRIVATE) @@ -15,3 +16,6 @@ feature = FeatureKey(FeatureType.CHAT_PRIVATE, get_authenticated_user_id()) chat_ui(feature) + + +st_header.run_script() diff --git a/web/personal_docs.py b/web/personal_docs.py index 5500f37b..59890b0c 100644 --- a/web/personal_docs.py +++ b/web/personal_docs.py @@ -1,11 +1,12 @@ """Page: Personal / Manage Your Documents.""" - +import st_components.page_header as st_header from docq.config import FeatureType, SpaceType from docq.domain import SpaceKey from st_pages import add_page_title from utils.layout import auth_required, documents_ui, feature_enabled from utils.sessions import get_authenticated_user_id, get_selected_org_id +st_header._setup_page_script() auth_required() feature_enabled(FeatureType.ASK_PERSONAL) @@ -15,3 +16,5 @@ space = SpaceKey(SpaceType.PERSONAL, get_authenticated_user_id(), get_selected_org_id()) documents_ui(space) + +st_header.run_script() diff --git a/web/shared_ask.py b/web/shared_ask.py index e92e2642..e7cfaf78 100644 --- a/web/shared_ask.py +++ b/web/shared_ask.py @@ -1,11 +1,12 @@ """Page: Shared / Ask Shared Documents.""" - +import st_components.page_header as st_header from docq.config import FeatureType from docq.domain import FeatureKey from st_pages import add_page_title from utils.layout import auth_required, chat_ui, feature_enabled from utils.sessions import get_authenticated_user_id +st_header._setup_page_script() auth_required() feature_enabled(FeatureType.ASK_SHARED) @@ -15,3 +16,5 @@ feature = FeatureKey(FeatureType.ASK_SHARED, get_authenticated_user_id()) chat_ui(feature) + +st_header.run_script() diff --git a/web/shared_spaces.py b/web/shared_spaces.py index 3447c90b..f77ada83 100644 --- a/web/shared_spaces.py +++ b/web/shared_spaces.py @@ -1,11 +1,13 @@ """Page: Shared / List Shared Spaces.""" +import st_components.page_header as st_header from docq.config import FeatureType from docq.domain import FeatureKey from st_pages import add_page_title from utils.layout import auth_required, feature_enabled, list_spaces_ui from utils.sessions import get_authenticated_user_id +st_header._setup_page_script() auth_required() feature_enabled(FeatureType.ASK_SHARED) @@ -15,3 +17,5 @@ feature = FeatureKey(FeatureType.ASK_SHARED, get_authenticated_user_id()) list_spaces_ui() + +st_header.run_script() diff --git a/web/st_components/page_header/__init__.py b/web/st_components/page_header/__init__.py index 2fba5e15..435793e8 100644 --- a/web/st_components/page_header/__init__.py +++ b/web/st_components/page_header/__init__.py @@ -1,18 +1,19 @@ """Header bar.""" import json import os -from contextlib import contextmanager from typing import Self import streamlit as st from streamlit.components.v1 import html -from ..static_utils import load_file_variables +from ..static_utils import get_current_page_info, load_file_variables parent_dir = os.path.dirname(os.path.abspath(__file__)) script_path = os.path.join(parent_dir, "static", "header.js") css_path = os.path.join(parent_dir, "static", "header.css") +api_key = "page_header_api{page_script_hash}_{page_name}" + class _PageHeaderAPI: """Page header bar API.""" @@ -23,6 +24,7 @@ class _PageHeaderAPI: __avatar_src: str = None __page_script: str = None __page_style: str = None + __fab_config: str = None def __init__(self: Self,) -> None: """Initialize the class.""" @@ -77,11 +79,18 @@ def add_menu_option(self: Self, label: str, key: str, icon_html_: str = None) -> """Add a menu option.""" for entry in self.__menu_options_list: if entry["text"] == label and entry["key"] == key: + self.__menu_options_json = json.dumps(self.__menu_options_list) + self._load_script() return self.__menu_options_list.append({"text": label, "key": key}) self.__menu_options_json = json.dumps(self.__menu_options_list) self._load_script() + def setup_fab(self: Self, tool_tip_label: str, key: str, icon: str = "+") -> None: + """Setup floating action button.""" + self.__fab_config = json.dumps({"label": tool_tip_label, "key": key, "icon": icon}) + self._load_script() + def _load_script(self: Self,) -> None: """Load the script.""" script_args = { @@ -89,11 +98,24 @@ def _load_script(self: Self,) -> None: "avatar_src": self.__avatar_src, "menu_items_json": self.__menu_options_json, "style_doc": self.__page_style, + "fab_config": self.__fab_config, } self.__page_script = load_file_variables(script_path, script_args) -__page_header_api = _PageHeaderAPI() +# Run this at the start of each page +def _setup_page_script() -> None: + """Setup page script.""" + script_caller_info = get_current_page_info() + print(f"\x1b[31mDebug-script-caller-info: {script_caller_info}\x1b[0m") + st.session_state[ + api_key.format( + page_script_hash=script_caller_info["page_script_hash"], + page_name=script_caller_info["page_name"] + ) + ] = _PageHeaderAPI() + theme = st.get_option("theme.primaryColor") + print(f"\x1b[31mDebug-theme: {theme}\x1b[0m") def render_header(username: str, avatar_src: str) -> None: """Header bar. @@ -102,15 +124,56 @@ def render_header(username: str, avatar_src: str) -> None: username (str): Username. avatar_src (str): Avatar source. """ + script_caller_info = get_current_page_info() + __page_header_api: _PageHeaderAPI = st.session_state[ + api_key.format( + page_script_hash=script_caller_info["page_script_hash"], + page_name=script_caller_info["page_name"] + ) + ] __page_header_api.username = username __page_header_api.avatar_src = avatar_src html(f"",height=0,) -@contextmanager -def menu_option(label: str, key: str = None) -> None: +def menu_option(label: str, key: str = None) -> bool: """Add a menu option.""" f_label = label.strip().replace(" ", "_").lower() - __button_key = st.button(label=f_label, key=key, type="primary") + script_caller_info = get_current_page_info() + __page_header_api: _PageHeaderAPI = st.session_state[ + api_key.format( + page_script_hash=script_caller_info["page_script_hash"], + page_name=script_caller_info["page_name"] + ) + ] __page_header_api.add_menu_option(label=label, key=f_label) - yield __button_key + return st.button(label=f_label, key=key, type="primary") + + +def floating_action_button(label: str, key: str = None, icon: str = None) -> bool: + """Add a floating action button.""" + f_label = label.strip().replace(" ", "_").lower() + script_caller_info = get_current_page_info() + __page_header_api: _PageHeaderAPI = st.session_state[ + api_key.format( + page_script_hash=script_caller_info["page_script_hash"], + page_name=script_caller_info["page_name"] + ) + ] + __page_header_api.setup_fab(tool_tip_label=label, key=f_label, icon=icon) + return st.button(label=f_label, key=key, type="primary") + + +def run_script() -> None: + """Run page header script. + + Run this at the end of each page to load all the defined components for the page. + """ + script_caller_info = get_current_page_info() + __page_header_api: _PageHeaderAPI = st.session_state[ + api_key.format( + page_script_hash=script_caller_info["page_script_hash"], + page_name=script_caller_info["page_name"] + ) + ] + html(f"",height=0,) diff --git a/web/st_components/page_header/static/header.css b/web/st_components/page_header/static/header.css index a1165209..0589c600 100644 --- a/web/st_components/page_header/static/header.css +++ b/web/st_components/page_header/static/header.css @@ -101,7 +101,7 @@ div.appview-container div.row-widget.stButton button[kind="primary"] { /* New Chat floating button */ -#docq-new-chat-button-container { +#docq-floating-action-button-container { position: fixed; bottom: 70px; right: 30px; @@ -114,7 +114,7 @@ div.appview-container div.row-widget.stButton button[kind="primary"] { z-index: 1000; } -#docq-new-chat-button { +#docq-floating-action-button { bottom: 70px; right: 80%; width: 40px; @@ -128,13 +128,20 @@ div.appview-container div.row-widget.stButton button[kind="primary"] { align-items: center; cursor: pointer; z-index: 1000; + border: none; + outline: none; } -#docq-new-chat-button:active { +#docq-floating-action-button:active, #docq-floating-action-button:focus { border: none; + outline: none; +} + +#docq-floating-action-button:active { + transform: translateY(4px); } -#docq-new-chat-tooltip { +#docq-fab-tooltip { position: absolute; top: -35px; right: -26px; @@ -149,7 +156,7 @@ div.appview-container div.row-widget.stButton button[kind="primary"] { animation: tooltip-slide-out 0.2s ease-in-out; } -#docq-new-chat-button-container:hover #docq-new-chat-tooltip { +#docq-floating-action-button-container:hover #docq-fab-tooltip { visibility: visible; animation: tooltip-slide-in 0.2s ease-in-out; } @@ -227,3 +234,7 @@ div[data-testid="stStatusWidget"] { background-color: #e31010; margin: 0.5rem 0; } + +.docq-iframe-container { + display: none !important; +} diff --git a/web/st_components/page_header/static/header.js b/web/st_components/page_header/static/header.js index 4ec5509e..0dae7690 100644 --- a/web/st_components/page_header/static/header.js +++ b/web/st_components/page_header/static/header.js @@ -1,10 +1,12 @@ +// PAGE-HEADER-SCRIPT === DO NOT REMOVE THIS COMMENT --- Used to identify this script in the page header parent = window.parent.document || window.document; // Get params === These are to be set in the template by the method that loads this script -const username = "{{username}}"; // User name to be displayed in the header -const avatarSrc = "{{avatar_src}}"; // Avatar image source +const username = `{{username}}`; // User name to be displayed in the header +const avatarSrc = `{{avatar_src}}`; // Avatar image source const menuItemsJson = `{{menu_items_json}}`; // [{ "text": "Menu item text", "key": "menu-item-button-key", "icon": "menu-item-icon-html"}] const styleDoc = `{{style_doc}}`; // CSS string to be added to the parent.document.head +const fab_config = `{{fab_config}}`; // { "icon": "fab-icon", "label": "tool-tip-text", "key": "fab-button-key" } const matchParamNotSet = /\{\{.*\}\}/; @@ -250,37 +252,53 @@ const pageTitle = document.createElement("span"); [right, center, left].forEach((div) => docqContainer.appendChild(div)); -// New chat button container -const newChatButtonContainer = document.createElement("div"); -newChatButtonContainer.setAttribute("id", "docq-new-chat-button-container"); -newChatButtonContainer.setAttribute("class", "docq-new-chat-button-container"); +// === Floating action button ==================================================================================================================================================== +const fabContainer = document.createElement("div"); +fabContainer.setAttribute("id", "docq-floating-action-button-container"); +fabContainer.setAttribute("class", "docq-floating-action-button-container"); // New chat button -const newChatButton = document.createElement("button"); -newChatButton.setAttribute("id", "docq-new-chat-button"); -newChatButton.setAttribute("class", "docq-new-chat-button"); -newChatButton.innerHTML = "+"; -newChatButton.addEventListener("click", () => { - const btns = parent.querySelectorAll('button[kind="secondary"]'); - const newChatBtn = Array.from(btns).find((btn) => btn.innerText.toLowerCase() === "new chat"); - if (newChatBtn) { - newChatBtn.click(); - } else { - console.log("New chat button not found", newChatBtn); - } -}); +function fabSetup (key, icon) { + const newChatButton = document.createElement("button"); + newChatButton.setAttribute("id", "docq-floating-action-button"); + newChatButton.setAttribute("class", "docq-floating-action-button"); + newChatButton.innerHTML = `${icon}`; + newChatButton.addEventListener("click", () => { + const btns = parent.querySelectorAll('button[kind="primary"]'); + const newChatBtn = Array.from(btns).find((btn) => btn.innerText.toLowerCase() === key.toLowerCase()); + if (newChatBtn) { + newChatBtn.click(); + } else { + console.log("New chat button not found", newChatBtn, key, icon); + } + }); + return newChatButton; +} +function tooltipSetup (label) { + const newChatTooltip = document.createElement("span"); + newChatTooltip.setAttribute("id", "docq-fab-tooltip"); + newChatTooltip.setAttribute("class", "docq-fab-tooltip"); + newChatTooltip.innerHTML = label; + return newChatTooltip; +} -const newChatTooltip = document.createElement("span"); -newChatTooltip.setAttribute("id", "docq-new-chat-tooltip"); -newChatTooltip.setAttribute("class", "docq-new-chat-tooltip"); -newChatTooltip.innerHTML = "New chat"; +previousFabButton = parent.getElementById("docq-floating-action-button"); +if (previousFabButton) { + previousFabButton.remove(); +} -newChatButtonContainer.appendChild(newChatTooltip); -newChatButtonContainer.appendChild(newChatButton); +if (!matchParamNotSet.test(fab_config)) { + const { icon, label, key } = JSON.parse(fab_config) + const newChatButton = fabSetup(key, icon) + const newChatTooltip = tooltipSetup(label) + fabContainer.appendChild(newChatTooltip); + fabContainer.appendChild(newChatButton); + parent.body.appendChild(fabContainer); +} -parent.body.appendChild(newChatButtonContainer); +// === END Floating action button ======================= // Insert docq container in the DOM stApp = parent.querySelector("header[data-testid='stHeader']"); @@ -291,3 +309,13 @@ if (stApp) { } stApp.insertBefore(docqContainer, stApp.firstChild); } + + +// === +const iframes = parent.querySelectorAll("iframe"); +iframes.forEach((iframe) => { + const srcdoc = iframe.getAttribute("srcdoc"); + if (srcdoc.includes("PAGE-HEADER-SCRIPT")) { + iframe.parentNode.setAttribute("class", "docq-iframe-container"); + } +}); diff --git a/web/st_components/sidebar_header/__init__.py b/web/st_components/sidebar_header/__init__.py index 15a7e6f1..be4f6bac 100644 --- a/web/st_components/sidebar_header/__init__.py +++ b/web/st_components/sidebar_header/__init__.py @@ -2,17 +2,20 @@ import base64 import json import os +from datetime import datetime from typing import Self +from urllib.parse import unquote import streamlit as st import streamlit.components.v1 as components -from ..static_utils import load_file_variables +from ..static_utils import get_current_page_info, load_file_variables parent_dir = os.path.dirname(os.path.abspath(__file__)) script_path = os.path.join(parent_dir, "static", "sidebar.js") css_path = os.path.join(parent_dir, "static", "sidebar.css") + class _SideBarHeaderAPI: """Custom API for the st_components package.""" @@ -82,7 +85,6 @@ def _load_script(self: Self,) -> None: __side_bar_header_api = _SideBarHeaderAPI() - def render_sidebar(selected_org: str, org_options: list, logo_url: str = None) -> None: """Docq sidebar header component. @@ -112,8 +114,14 @@ def update_org_options(org_options: list) -> None: __side_bar_header_api.org_options_json = org_options -def get_selected_org_from_ui() -> str: +def get_selected_org_from_ui() -> str | None: """Get the current org.""" - org = st.experimental_get_query_params().get("org", [None])[0] - if org is not None: - return base64.b64decode(org).decode("utf-8") + param = st.experimental_get_query_params().get("org", [None])[0] + if param is not None: + org, timestamp = base64.b64decode( + unquote(param) + ).decode("utf-8").split("::") + now_ = datetime.now() + if now_.timestamp() - float(timestamp) < 60: + return org + return None diff --git a/web/st_components/sidebar_header/static/sidebar.css b/web/st_components/sidebar_header/static/sidebar.css index 30a0ccd5..d4bf8152 100644 --- a/web/st_components/sidebar_header/static/sidebar.css +++ b/web/st_components/sidebar_header/static/sidebar.css @@ -27,3 +27,5 @@ section[data-testid="stSidebar"][aria-expanded="true"] button[kind="header"] { font-weight: 500; padding: 0 1rem; } + + diff --git a/web/st_components/sidebar_header/static/sidebar.js b/web/st_components/sidebar_header/static/sidebar.js index 2640a236..d4ff9e5c 100644 --- a/web/st_components/sidebar_header/static/sidebar.js +++ b/web/st_components/sidebar_header/static/sidebar.js @@ -1,12 +1,14 @@ +// SIDEBAR-HEADER-SCRIPT === DO NOT REMOVE THIS COMMENT --- Used to identify this script in the page header parent = window.parent.document || window.document; // === Required params === -const selectedOrg = "{{selected_org}}"; +const selectedOrg = `{{selected_org}}`; const orgOptionsJson = `{{org_options_json}}`; const styleDoc = `{{style_doc}}`; +const orgSelectorLabel = `{{org_selector_label}}`; // === Optional params === -const logoUrl = "{{logo_url}}"; +const logoUrl = `{{logo_url}}`; const matchParamNotSet = /\{\{.*\}\}/g; @@ -98,7 +100,7 @@ const logoSrc = logoUrl && !logoUrl.match(matchParamNotSet) ? logoUrl : "https:/ docqLogo.setAttribute("src", logoSrc); docqLogo.setAttribute("alt", "docq logo"); -docqLogo.setAttribute("style", "width: 50px; height: 50px;"); +docqLogo.setAttribute("style", "width: 25%;"); docqLogo.setAttribute("id", "docq-logo"); docqLogo.setAttribute("async", "1"); @@ -115,6 +117,10 @@ const selectLabel = document.createElement("label"); selectLabel.setAttribute("for", "docq-org-dropdown-select"); selectLabel.setAttribute("class", "docq-select-label"); selectLabel.innerHTML = "Select org:"; +if (!matchParamNotSet.test(orgSelectorLabel)) { + selectLabel.innerHTML = orgSelectorLabel; +} + selectLabel.setAttribute("style", "margin-right: 10px;"); const selectMenu = document.createElement("select"); @@ -159,7 +165,8 @@ selectOrgScript.setAttribute("type", "text/javascript"); selectOrgScript.setAttribute("id", "docq-select-org-script"); selectOrgScript.innerHTML = ` function selectOrg(org) { - const orgParam = encodeURIComponent(btoa(org)); + const timeStamp = new Date().getTime(); + const orgParam = encodeURIComponent(btoa(org + "::" + timeStamp)); window.parent.location.href = \`?org=\${orgParam}\`; } `; @@ -170,3 +177,13 @@ if (prevScript) { } parent.body.appendChild(selectOrgScript); + +// === +const iframes = parent.querySelectorAll("iframe"); +iframes.forEach((iframe) => { + const srcdoc = iframe.getAttribute("srcdoc"); + if (srcdoc.includes("SIDEBAR-HEADER-SCRIPT")) { + iframe.parentNode.setAttribute("class", "docq-iframe-container"); + } +}); +// === EOF ================================================================================================================================================================= diff --git a/web/st_components/static_utils.py b/web/st_components/static_utils.py index 91584ac9..8d95b13f 100644 --- a/web/st_components/static_utils.py +++ b/web/st_components/static_utils.py @@ -1,4 +1,16 @@ """Utility functions for static files.""" +from streamlit.source_util import get_pages + +try: + from streamlit.runtime.scriptrunner import get_script_run_ctx +except ImportError: + from streamlit.scriptrunner.script_run_context import ( + get_script_run_ctx, + ) + +import os +import sys + def load_file_variables(file_path: str, vars_: dict = None) -> str: """Load file variables.""" @@ -9,3 +21,16 @@ def load_file_variables(file_path: str, vars_: dict = None) -> str: if value is not None: file_str = file_str.replace('{{' + key + '}}', value) return file_str + + +def get_current_page_info() -> str: + """Get the current page name.""" + main_script_path = os.path.abspath(sys.argv[0]) + pages = get_pages("") + ctx = get_script_run_ctx() + if ctx is not None: + return pages.get( + ctx.page_script_hash, + (p for p in pages.values() if p["relative_page_hash"] == ctx.page_script_hash) + ) + return None diff --git a/web/utils/layout.py b/web/utils/layout.py index d14baff5..cf67ca49 100644 --- a/web/utils/layout.py +++ b/web/utils/layout.py @@ -22,7 +22,7 @@ from streamlit.components.v1 import html from streamlit.delta_generator import DeltaGenerator -from web.st_components.page_header import menu_option, render_header +from web.st_components.page_header import floating_action_button, menu_option, render_header from .constants import ALLOWED_DOC_EXTS, SessionKeyNameForAuth, SessionKeyNameForChat from .error_ui import _handle_error_state_ui @@ -603,10 +603,11 @@ def chat_ui(feature: FeatureKey) -> None: with chat_histoy: if st.button("Load chat history earlier"): query_chat_history(feature) - if st.button("New chat", type="primary"): + + if floating_action_button("New chat", icon="+"): handle_create_new_chat(feature) - with menu_option("Chat Settings"): + if menu_option("Chat Settings"): print("\x1b[31mChat settings test\x1b[0m") day = format_datetime(get_chat_session(feature.type_, SessionKeyNameForChat.CUTOFF)) From 63017984563b17f5863d206e57ab37143fd7253a Mon Sep 17 00:00:00 2001 From: Jashon Osala Date: Tue, 10 Oct 2023 07:27:53 +0300 Subject: [PATCH 10/16] chore: Update sidebar and header components. --- web/admin_logs.py | 7 +- web/admin_orgs.py | 7 +- web/admin_settings.py | 7 +- web/admin_space_groups.py | 13 +- web/admin_spaces.py | 7 +- web/admin_user_groups.py | 7 +- web/admin_users.py | 11 +- web/embed.py | 3 +- web/index.py | 35 ++---- web/personal_ask.py | 7 +- web/personal_chat.py | 7 +- web/personal_docs.py | 7 +- web/shared_ask.py | 7 +- web/shared_spaces.py | 7 +- web/st_components/page_header/__init__.py | 119 ++++++++++-------- .../page_header/static/header.js | 42 ++++--- web/st_components/sidebar_header/__init__.py | 63 +++++++--- .../sidebar_header/static/sidebar.js | 3 +- web/utils/handlers.py | 20 ++- web/utils/layout.py | 88 ++++++++----- 20 files changed, 267 insertions(+), 200 deletions(-) diff --git a/web/admin_logs.py b/web/admin_logs.py index b1c2dccd..88471b8b 100644 --- a/web/admin_logs.py +++ b/web/admin_logs.py @@ -1,12 +1,11 @@ """Page: Admin / View Logs.""" -import st_components.page_header as st_header import streamlit as st from docq.config import LogType from st_pages import add_page_title -from utils.layout import auth_required, list_logs_ui +from utils.layout import auth_required, list_logs_ui, run_page_scripts, setup_page_scripts -st_header._setup_page_script() +setup_page_scripts() auth_required(requiring_admin=True) @@ -19,4 +18,4 @@ st.subheader(type_.value) list_logs_ui(type_) -st_header.run_script() +run_page_scripts() diff --git a/web/admin_orgs.py b/web/admin_orgs.py index b771539a..9ad734e0 100644 --- a/web/admin_orgs.py +++ b/web/admin_orgs.py @@ -1,9 +1,8 @@ """Page: Admin / Manage Orgs.""" -import st_components.page_header as st_header from st_pages import add_page_title -from utils.layout import auth_required, create_org_ui, list_orgs_ui +from utils.layout import auth_required, create_org_ui, list_orgs_ui, run_page_scripts, setup_page_scripts -st_header._setup_page_script() +setup_page_scripts() auth_required(requiring_admin=True) add_page_title() @@ -11,4 +10,4 @@ create_org_ui() list_orgs_ui() -st_header.run_script() +run_page_scripts() diff --git a/web/admin_settings.py b/web/admin_settings.py index b1aee01b..cbb9e350 100644 --- a/web/admin_settings.py +++ b/web/admin_settings.py @@ -1,10 +1,9 @@ """Page: Admin / Manage Settings.""" -import st_components.page_header as st_header from st_pages import add_page_title -from utils.layout import auth_required, system_settings_ui +from utils.layout import auth_required, run_page_scripts, setup_page_scripts, system_settings_ui -st_header._setup_page_script() +setup_page_scripts() auth_required(requiring_admin=True) @@ -12,4 +11,4 @@ system_settings_ui() -st_header.run_script() +run_page_scripts() diff --git a/web/admin_space_groups.py b/web/admin_space_groups.py index 75bbd5eb..5e059341 100644 --- a/web/admin_space_groups.py +++ b/web/admin_space_groups.py @@ -1,10 +1,15 @@ """Page: Admin / Manage Space Groups.""" -import st_components.page_header as st_header from st_pages import add_page_title -from utils.layout import auth_required, create_space_group_ui, list_space_groups_ui +from utils.layout import ( + auth_required, + create_space_group_ui, + list_space_groups_ui, + run_page_scripts, + setup_page_scripts, +) -st_header._setup_page_script() +setup_page_scripts() auth_required(requiring_admin=True) add_page_title() @@ -12,4 +17,4 @@ create_space_group_ui() list_space_groups_ui() -st_header.run_script() +run_page_scripts() diff --git a/web/admin_spaces.py b/web/admin_spaces.py index 0280118a..3af7333f 100644 --- a/web/admin_spaces.py +++ b/web/admin_spaces.py @@ -1,9 +1,8 @@ """Page: Admin / Manage Documents.""" -import st_components.page_header as st_header from st_pages import add_page_title -from utils.layout import admin_docs_ui, auth_required, create_space_ui +from utils.layout import admin_docs_ui, auth_required, create_space_ui, run_page_scripts, setup_page_scripts -st_header._setup_page_script() +setup_page_scripts() auth_required(requiring_admin=True) add_page_title() @@ -15,4 +14,4 @@ admin_docs_ui(PARAM_NAME) -st_header.run_script() +run_page_scripts() diff --git a/web/admin_user_groups.py b/web/admin_user_groups.py index 466da8b8..ec769d23 100644 --- a/web/admin_user_groups.py +++ b/web/admin_user_groups.py @@ -1,9 +1,8 @@ """Page: Admin / Manage User Groups.""" -import st_components.page_header as st_header from st_pages import add_page_title -from utils.layout import auth_required, create_user_group_ui, list_user_groups_ui +from utils.layout import auth_required, create_user_group_ui, list_user_groups_ui, run_page_scripts, setup_page_scripts -st_header._setup_page_script() +setup_page_scripts() auth_required(requiring_admin=True) add_page_title() @@ -11,4 +10,4 @@ create_user_group_ui() list_user_groups_ui() -st_header.run_script() +run_page_scripts() diff --git a/web/admin_users.py b/web/admin_users.py index 6a4741b4..69386cf3 100644 --- a/web/admin_users.py +++ b/web/admin_users.py @@ -1,18 +1,13 @@ """Page: Admin / Manage Users.""" -import st_components.page_header as st_header -import streamlit as st from st_pages import add_page_title -from utils.layout import auth_required, create_user_ui, list_users_ui, org_selection_ui +from utils.layout import auth_required, create_user_ui, list_users_ui, run_page_scripts, setup_page_scripts -st_header._setup_page_script() +setup_page_scripts() auth_required(requiring_admin=True) add_page_title() -with st.sidebar: - org_selection_ui() - create_user_ui() list_users_ui() -st_header.run_script() +run_page_scripts() diff --git a/web/embed.py b/web/embed.py index a5b6b6db..9f4e54c1 100644 --- a/web/embed.py +++ b/web/embed.py @@ -1,9 +1,10 @@ """Docq widget embed page.""" from docq.config import FeatureType from docq.domain import FeatureKey -from utils.layout import chat_ui, public_session_setup, public_space_enabled +from utils.layout import chat_ui, public_session_setup, public_space_enabled, setup_page_scripts from utils.sessions import get_public_session_id +setup_page_scripts() public_session_setup() public_space_enabled(FeatureType.ASK_PUBLIC) diff --git a/web/index.py b/web/index.py index b9bb5737..8193dc5f 100644 --- a/web/index.py +++ b/web/index.py @@ -1,12 +1,12 @@ """Page: Home (no auth required).""" -import st_components.page_header as st_header import streamlit as st -from docq import setup -from st_components.sidebar_header import get_selected_org_from_ui, render_sidebar from st_pages import Page, Section, add_page_title, show_pages -from utils.layout import init_with_pretty_error_ui, org_selection_ui, production_layout, public_access - -from web.st_components.sidebar_header import render_sidebar +from utils.layout import ( + init_with_pretty_error_ui, + production_layout, + public_access, + setup_page_scripts, +) init_with_pretty_error_ui() @@ -38,8 +38,6 @@ ] ) -st_header._setup_page_script() - public_access() add_page_title() @@ -68,24 +66,5 @@ st.markdown("Enjoy [Docq](https://docq.ai)!") -mock_org_options = [ - "org name 1", - "org name 2", - "org name 3", - "org name 4", -] - -mock_selected_org = "org name 2" - - -render_sidebar( - selected_org=mock_selected_org, - org_options=mock_org_options, -) - -selected_org = get_selected_org_from_ui() -if selected_org is not None: - print("\x1b[31m", "selected_org", selected_org, "\x1b[0m") - st.experimental_set_query_params() -st_header.run_script() +setup_page_scripts() diff --git a/web/personal_ask.py b/web/personal_ask.py index 2ea6a2c7..4035623c 100644 --- a/web/personal_ask.py +++ b/web/personal_ask.py @@ -1,12 +1,11 @@ """Page: Personal / Ask Your Documents.""" -import st_components.page_header as st_header from docq.config import FeatureType from docq.domain import FeatureKey from st_pages import add_page_title -from utils.layout import auth_required, chat_ui, feature_enabled, header_ui +from utils.layout import auth_required, chat_ui, feature_enabled, run_page_scripts, setup_page_scripts from utils.sessions import get_authenticated_user_id -st_header._setup_page_script() +setup_page_scripts() auth_required() feature_enabled(FeatureType.ASK_PERSONAL) @@ -17,4 +16,4 @@ chat_ui(feature) -st_header.run_script() +run_page_scripts() diff --git a/web/personal_chat.py b/web/personal_chat.py index f5cdf0d5..eb26fad4 100644 --- a/web/personal_chat.py +++ b/web/personal_chat.py @@ -1,12 +1,11 @@ """Page: Personal / General Chat.""" -import st_components.page_header as st_header from docq.config import FeatureType from docq.domain import FeatureKey from st_pages import add_page_title -from utils.layout import auth_required, chat_ui, feature_enabled +from utils.layout import auth_required, chat_ui, feature_enabled, run_page_scripts, setup_page_scripts from utils.sessions import get_authenticated_user_id -st_header._setup_page_script() +setup_page_scripts() auth_required() feature_enabled(FeatureType.CHAT_PRIVATE) @@ -18,4 +17,4 @@ chat_ui(feature) -st_header.run_script() +run_page_scripts() diff --git a/web/personal_docs.py b/web/personal_docs.py index 59890b0c..c626ff62 100644 --- a/web/personal_docs.py +++ b/web/personal_docs.py @@ -1,12 +1,11 @@ """Page: Personal / Manage Your Documents.""" -import st_components.page_header as st_header from docq.config import FeatureType, SpaceType from docq.domain import SpaceKey from st_pages import add_page_title -from utils.layout import auth_required, documents_ui, feature_enabled +from utils.layout import auth_required, documents_ui, feature_enabled, run_page_scripts, setup_page_scripts from utils.sessions import get_authenticated_user_id, get_selected_org_id -st_header._setup_page_script() +setup_page_scripts() auth_required() feature_enabled(FeatureType.ASK_PERSONAL) @@ -17,4 +16,4 @@ documents_ui(space) -st_header.run_script() +run_page_scripts() diff --git a/web/shared_ask.py b/web/shared_ask.py index e7cfaf78..69940787 100644 --- a/web/shared_ask.py +++ b/web/shared_ask.py @@ -1,12 +1,11 @@ """Page: Shared / Ask Shared Documents.""" -import st_components.page_header as st_header from docq.config import FeatureType from docq.domain import FeatureKey from st_pages import add_page_title -from utils.layout import auth_required, chat_ui, feature_enabled +from utils.layout import auth_required, chat_ui, feature_enabled, run_page_scripts, setup_page_scripts from utils.sessions import get_authenticated_user_id -st_header._setup_page_script() +setup_page_scripts() auth_required() feature_enabled(FeatureType.ASK_SHARED) @@ -17,4 +16,4 @@ chat_ui(feature) -st_header.run_script() +run_page_scripts() diff --git a/web/shared_spaces.py b/web/shared_spaces.py index f77ada83..5e4e3aef 100644 --- a/web/shared_spaces.py +++ b/web/shared_spaces.py @@ -1,13 +1,12 @@ """Page: Shared / List Shared Spaces.""" -import st_components.page_header as st_header from docq.config import FeatureType from docq.domain import FeatureKey from st_pages import add_page_title -from utils.layout import auth_required, feature_enabled, list_spaces_ui +from utils.layout import auth_required, feature_enabled, list_spaces_ui, run_page_scripts, setup_page_scripts from utils.sessions import get_authenticated_user_id -st_header._setup_page_script() +setup_page_scripts() auth_required() feature_enabled(FeatureType.ASK_SHARED) @@ -18,4 +17,4 @@ list_spaces_ui() -st_header.run_script() +run_page_scripts() diff --git a/web/st_components/page_header/__init__.py b/web/st_components/page_header/__init__.py index 435793e8..2e2d0c44 100644 --- a/web/st_components/page_header/__init__.py +++ b/web/st_components/page_header/__init__.py @@ -1,5 +1,6 @@ """Header bar.""" import json +import logging as log import os from typing import Self @@ -25,6 +26,7 @@ class _PageHeaderAPI: __page_script: str = None __page_style: str = None __fab_config: str = None + __auth_state: str = None def __init__(self: Self,) -> None: """Initialize the class.""" @@ -56,6 +58,20 @@ def script(self: Self,) -> str: """Get the script.""" return self.__page_script + @property + def auth_state(self: Self,) -> str: + """Get the auth state.""" + return self.__auth_state + + @auth_state.setter + def auth_state(self: Self, value: bool) -> None: + """Set the auth state.""" + if value: + self.__auth_state = "authenticated" + else: + self.__auth_state = "unauthenticated" + self._load_script() + @menu_options_list.setter def menu_options_list(self: Self, value: list) -> None: """Set the dict containing available menu options.""" @@ -99,81 +115,78 @@ def _load_script(self: Self,) -> None: "menu_items_json": self.__menu_options_json, "style_doc": self.__page_style, "fab_config": self.__fab_config, + "auth_state": self.__auth_state, } self.__page_script = load_file_variables(script_path, script_args) # Run this at the start of each page -def _setup_page_script() -> None: +def _setup_page_script(auth_state: bool) -> None: """Setup page script.""" script_caller_info = get_current_page_info() - print(f"\x1b[31mDebug-script-caller-info: {script_caller_info}\x1b[0m") - st.session_state[ - api_key.format( - page_script_hash=script_caller_info["page_script_hash"], - page_name=script_caller_info["page_name"] - ) - ] = _PageHeaderAPI() - theme = st.get_option("theme.primaryColor") - print(f"\x1b[31mDebug-theme: {theme}\x1b[0m") - -def render_header(username: str, avatar_src: str) -> None: - """Header bar. - - Args: - username (str): Username. - avatar_src (str): Avatar source. - """ - script_caller_info = get_current_page_info() - __page_header_api: _PageHeaderAPI = st.session_state[ - api_key.format( - page_script_hash=script_caller_info["page_script_hash"], - page_name=script_caller_info["page_name"] - ) - ] - __page_header_api.username = username - __page_header_api.avatar_src = avatar_src - html(f"",height=0,) + try: + _key = api_key.format( + page_script_hash=script_caller_info["page_script_hash"], + page_name=script_caller_info["page_name"] + ) + __page_header_api = _PageHeaderAPI() + __page_header_api.auth_state = auth_state + st.session_state[_key] = __page_header_api + html(f"",height=0,) + except Exception as e: + log.error("Page header not initialized properly. error: %s", e) def menu_option(label: str, key: str = None) -> bool: """Add a menu option.""" f_label = label.strip().replace(" ", "_").lower() script_caller_info = get_current_page_info() - __page_header_api: _PageHeaderAPI = st.session_state[ - api_key.format( - page_script_hash=script_caller_info["page_script_hash"], - page_name=script_caller_info["page_name"] - ) - ] - __page_header_api.add_menu_option(label=label, key=f_label) - return st.button(label=f_label, key=key, type="primary") + try: + _key = api_key.format( + page_script_hash=script_caller_info["page_script_hash"], + page_name=script_caller_info["page_name"] + ) + __page_header_api: _PageHeaderAPI = st.session_state[_key] + __page_header_api.add_menu_option(label=label, key=f_label) + return st.button(label=f_label, key=key, type="primary") + except KeyError as e: + log.error("Page header not initialized. Please run `setup_page_script` first. error: %s", e) def floating_action_button(label: str, key: str = None, icon: str = None) -> bool: """Add a floating action button.""" f_label = label.strip().replace(" ", "_").lower() script_caller_info = get_current_page_info() - __page_header_api: _PageHeaderAPI = st.session_state[ - api_key.format( - page_script_hash=script_caller_info["page_script_hash"], - page_name=script_caller_info["page_name"] - ) - ] - __page_header_api.setup_fab(tool_tip_label=label, key=f_label, icon=icon) - return st.button(label=f_label, key=key, type="primary") + try: + _key = api_key.format( + page_script_hash=script_caller_info["page_script_hash"], + page_name=script_caller_info["page_name"] + ) + __page_header_api: _PageHeaderAPI = st.session_state[_key] + __page_header_api.setup_fab(tool_tip_label=label, key=f_label, icon=icon) + return st.button(label=f_label, key=key, type="primary") + except KeyError as e: + log.error("Page header not initialized. Please run `setup_page_script` first. error: %s", e) -def run_script() -> None: - """Run page header script. +def run_script(auth_state: bool, username: str = None, avatar_src: str = None) -> None: + """Render the header bar. - Run this at the end of each page to load all the defined components for the page. + Args: + auth_state (bool): Authentication state. + username (str): Username. + avatar_src (str): Avatar source. """ script_caller_info = get_current_page_info() - __page_header_api: _PageHeaderAPI = st.session_state[ - api_key.format( - page_script_hash=script_caller_info["page_script_hash"], - page_name=script_caller_info["page_name"] + try: + _key = api_key.format( + page_script_hash=script_caller_info["page_script_hash"], + page_name=script_caller_info["page_name"] ) - ] - html(f"",height=0,) + __page_header_api: _PageHeaderAPI = st.session_state[_key] + __page_header_api.auth_state = auth_state + __page_header_api.username = username + __page_header_api.avatar_src = avatar_src + html(f"",height=0,) + except KeyError as e: + log.error("Page header not initialized. Please run `setup_page_script` first. error: %s", e) diff --git a/web/st_components/page_header/static/header.js b/web/st_components/page_header/static/header.js index 0dae7690..d373fb17 100644 --- a/web/st_components/page_header/static/header.js +++ b/web/st_components/page_header/static/header.js @@ -7,6 +7,7 @@ const avatarSrc = `{{avatar_src}}`; // Avatar image source const menuItemsJson = `{{menu_items_json}}`; // [{ "text": "Menu item text", "key": "menu-item-button-key", "icon": "menu-item-icon-html"}] const styleDoc = `{{style_doc}}`; // CSS string to be added to the parent.document.head const fab_config = `{{fab_config}}`; // { "icon": "fab-icon", "label": "tool-tip-text", "key": "fab-button-key" } +const authState = `{{auth_state}}`; // "authenticated" or "unauthenticated" const matchParamNotSet = /\{\{.*\}\}/; @@ -19,13 +20,8 @@ if (!matchParamNotSet.test(styleDoc)) { style.setAttribute("id", "docq-header-style"); // check if style tag already exists and verify if it is the same as the one to be added const prevStyle = parent.getElementById("docq-header-style"); - if (prevStyle) { - if (prevStyle.innerHTML === styleDoc) { - console.log("Style already exists"); - } else { - console.log("Style exists but is different"); - prevStyle.innerHTML = styleDoc; - } + if (prevStyle && prevStyle.innerHTML !== styleDoc) { + prevStyle.innerHTML = styleDoc; } else { style.innerHTML = styleDoc; parent.head.appendChild(style); @@ -97,7 +93,8 @@ const [left, center, right] = ["left", "center", "right"].map((id) => { // Avatar const loadAvatar = () => { const avatar = document.createElement("img"); - avatar.setAttribute("src", avatarSrc); + const src = matchParamNotSet.test(avatarSrc) ? "https://www.gravatar.com/avatar/00" : avatarSrc; + avatar.setAttribute("src", src); avatar.setAttribute("alt", "user-avatar"); avatar.setAttribute("style", "width: 20px; height: 20px;"); avatar.setAttribute("id", "docq-img-avatar"); @@ -111,7 +108,10 @@ avatarContainer.setAttribute("id", "docq-avatar-container"); const avatar = loadAvatar(); -avatarContainer.appendChild(avatar); + +if (authState === "authenticated" && !matchParamNotSet.test(avatarSrc)) { + avatarContainer.appendChild(avatar); +} // User menu ======================================================================================================================================================================== const userMenu = document.createElement("div"); @@ -182,13 +182,15 @@ userMenu.appendChild(helpBtn); userMenu.appendChild(feedbackBtn); // Add user menu to avatar container -avatarContainer.appendChild(userMenu); +console.log("authState", authState); +if (authState === "authenticated") { + avatarContainer.appendChild(userMenu); +} // User menu toggle avatar.addEventListener("click", () => { const userMenu = parent.getElementById("docq-user-menu"); if (userMenu) { - console.log("User menu found", userMenu); userMenu.classList.toggle("docq-user-menu-active"); // Autofocus on the user menu const userMenuItems = userMenu.querySelectorAll(".docq-user-menu-item"); @@ -227,6 +229,16 @@ const userMenuObserver = new MutationObserver((mutations) => { userMenuObserver.observe(userMenu, { attributes: true }); +// Close user menu on clicking its child elements +userMenu.addEventListener("click", (e) => { + if (e.target !== userMenu) { + const userMenu = parent.getElementById("docq-user-menu"); + if (userMenu) { + userMenu.classList.remove("docq-user-menu-active"); + } + } +}); + // End user menu ==================================================================================================================================================================== @@ -235,10 +247,10 @@ const userName = document.createElement("span"); userName.innerHTML = `${username}`; userName.setAttribute("id", "docq-user-name"); -if(!matchParamNotSet.test(username)) { +if(!matchParamNotSet.test(username) && authState === "authenticated") { left.appendChild(userName); } -if (!matchParamNotSet.test(avatarSrc)) { +if (!matchParamNotSet.test(avatarSrc) && authState === "authenticated") { left.appendChild(avatarContainer); } @@ -295,7 +307,9 @@ if (!matchParamNotSet.test(fab_config)) { const newChatTooltip = tooltipSetup(label) fabContainer.appendChild(newChatTooltip); fabContainer.appendChild(newChatButton); - parent.body.appendChild(fabContainer); + if(authState === "authenticated") { + parent.body.appendChild(fabContainer); + } } // === END Floating action button ======================= diff --git a/web/st_components/sidebar_header/__init__.py b/web/st_components/sidebar_header/__init__.py index be4f6bac..a41b4dae 100644 --- a/web/st_components/sidebar_header/__init__.py +++ b/web/st_components/sidebar_header/__init__.py @@ -9,13 +9,12 @@ import streamlit as st import streamlit.components.v1 as components -from ..static_utils import get_current_page_info, load_file_variables +from ..static_utils import load_file_variables parent_dir = os.path.dirname(os.path.abspath(__file__)) script_path = os.path.join(parent_dir, "static", "sidebar.js") css_path = os.path.join(parent_dir, "static", "sidebar.css") - class _SideBarHeaderAPI: """Custom API for the st_components package.""" @@ -24,6 +23,7 @@ class _SideBarHeaderAPI: __logo_url: str = None __header_script: str = None __header_style: str = None + __auth_state: str = "unauthenticated" def __init__(self: Self,) -> None: """Initialize the class.""" @@ -42,12 +42,12 @@ def selected_org(self: Self, value: str) -> None: self._load_script() @property - def org_options_json(self: Self,) -> str: + def org_options_list(self: Self,) -> str: """Get the json string containing available orgs.""" return self.__org_options_json - @org_options_json.setter - def org_options_json(self: Self, value: dict) -> None: + @org_options_list.setter + def org_options_list(self: Self, value: list) -> None: """Set the json string containing available orgs.""" self.__org_options_json = json.dumps(value) self._load_script() @@ -73,29 +73,42 @@ def style(self: Self,) -> str: """Get the style.""" return self.__header_style + @property + def auth_state(self: Self,) -> str: + """Get the auth state.""" + return self.__auth_state + + @auth_state.setter + def auth_state(self: Self, value: bool) -> None: + """Set the auth state.""" + auth_state = "authenticated" if value else "unauthenticated" + if auth_state == "unauthenticated" and self.__auth_state != auth_state: + self.reset_user_details() + self.__auth_state = auth_state + self._load_script() + + def reset_user_details(self: Self,) -> None: + """Reset the instance.""" + self.__selected_org = None + self.__org_options_json = None + self.__auth_state = "unauthenticated" + def _load_script(self: Self,) -> None: params = { "selected_org": self.selected_org, - "org_options_json": self.org_options_json, + "org_options_json": self.org_options_list, "logo_url": self.logo_url, "style_doc": self.style, + "auth_state": self.auth_state, } self.__header_script = load_file_variables(script_path, params) __side_bar_header_api = _SideBarHeaderAPI() -def render_sidebar(selected_org: str, org_options: list, logo_url: str = None) -> None: - """Docq sidebar header component. - - Args: - selected_org: The currently selected org. - org_options: A list containing available orgs for the drop down menu. - logo_url: URL to logo. - """ - __side_bar_header_api.selected_org = selected_org - __side_bar_header_api.org_options_json = org_options - __side_bar_header_api.logo_url = logo_url +def _setup_page_script(auth_state: bool) -> None: + """Setup the page script.""" + __side_bar_header_api.auth_state = auth_state components.html(f""" // ST-SIDEBAR-SCRIPT-CONTAINER @@ -103,7 +116,6 @@ def render_sidebar(selected_org: str, org_options: list, logo_url: str = None) - height=0 ) - def set_selected_org(selected_org: str) -> None: """Set the current org.""" __side_bar_header_api.selected_org = selected_org @@ -111,7 +123,7 @@ def set_selected_org(selected_org: str) -> None: def update_org_options(org_options: list) -> None: """Update the org options.""" - __side_bar_header_api.org_options_json = org_options + __side_bar_header_api.org_options_list = org_options def get_selected_org_from_ui() -> str | None: @@ -125,3 +137,16 @@ def get_selected_org_from_ui() -> str | None: if now_.timestamp() - float(timestamp) < 60: return org return None + + +def run_script(auth_state: bool, selected_org: str = None, org_options: list = None) -> None: + """Run the script.""" + __side_bar_header_api.selected_org = selected_org + __side_bar_header_api.org_options_list = org_options + __side_bar_header_api.auth_state = auth_state + components.html(f""" + // ST-SIDEBAR-SCRIPT-CONTAINER + + """, + height=0 + ) diff --git a/web/st_components/sidebar_header/static/sidebar.js b/web/st_components/sidebar_header/static/sidebar.js index d4ff9e5c..c85ff252 100644 --- a/web/st_components/sidebar_header/static/sidebar.js +++ b/web/st_components/sidebar_header/static/sidebar.js @@ -6,6 +6,7 @@ const selectedOrg = `{{selected_org}}`; const orgOptionsJson = `{{org_options_json}}`; const styleDoc = `{{style_doc}}`; const orgSelectorLabel = `{{org_selector_label}}`; +const authState = `{{auth_state}}`; // === Optional params === const logoUrl = `{{logo_url}}`; @@ -141,7 +142,7 @@ if (orgOptionsJson && !orgOptionsJson.match(matchParamNotSet)) { orgDropdown.appendChild(selectLabel); orgDropdown.appendChild(selectMenu); -if (!selectedOrg.match(matchParamNotSet) && !orgOptionsJson.match(matchParamNotSet)) { +if (!selectedOrg.match(matchParamNotSet) && !orgOptionsJson.match(matchParamNotSet) && authState === "authenticated") { docqLogoContainer.appendChild(orgDropdown); } diff --git a/web/utils/handlers.py b/web/utils/handlers.py index 49c5d277..b48ed6c9 100644 --- a/web/utils/handlers.py +++ b/web/utils/handlers.py @@ -8,6 +8,7 @@ from datetime import datetime from typing import Any, List, Optional, Tuple +import st_components.sidebar_header as st_sidebar import streamlit as st from docq import ( config, @@ -339,11 +340,16 @@ def handle_list_orgs(name_match: str = None) -> List[Tuple]: return manage_organisations.list_organisations(name_match=name_match, user_id=current_user_id) -def handle_org_selection_change(org_id: int) -> None: +def handle_org_selection_change() -> None: """Handle org selection change.""" - set_selected_org_id(org_id) + org_selection = st_sidebar.get_selected_org_from_ui() + if org_selection is not None: + orgs_list = handle_list_orgs(org_selection) + new_org_id = orgs_list[0][0] + set_selected_org_id(new_org_id) - set_if_current_user_is_selected_org_admin(org_id) + set_if_current_user_is_selected_org_admin(new_org_id) + st.experimental_set_query_params() def handle_create_space_group() -> int: @@ -720,3 +726,11 @@ def handle_public_session() -> None: space_group_id=-1, public_session_id=-1, ) + + +def get_auth_state() -> tuple[bool, str | None]: + """Check the auth state.""" + auth_session = get_auth_session() + state = auth_session and not auth_session.get(SessionKeyNameForAuth.ANONYMOUS.name) + name = auth_session.get(SessionKeyNameForAuth.NAME.name) + return state, name diff --git a/web/utils/layout.py b/web/utils/layout.py index cf67ca49..0aef2091 100644 --- a/web/utils/layout.py +++ b/web/utils/layout.py @@ -3,6 +3,8 @@ import logging as log from typing import List, Tuple +import st_components.page_header as st_header +import st_components.sidebar_header as st_sidebar import streamlit as st from docq import setup from docq.access_control.main import SpaceAccessType @@ -22,13 +24,12 @@ from streamlit.components.v1 import html from streamlit.delta_generator import DeltaGenerator -from web.st_components.page_header import floating_action_button, menu_option, render_header - from .constants import ALLOWED_DOC_EXTS, SessionKeyNameForAuth, SessionKeyNameForChat from .error_ui import _handle_error_state_ui from .formatters import format_archived, format_datetime, format_filesize, format_timestamp from .handlers import ( _set_session_state_configs, + get_auth_state, get_enabled_features, get_max_number_of_documents, get_shared_space, @@ -317,7 +318,6 @@ def auth_required(show_login_form: bool = True, requiring_admin: bool = False, s if requiring_admin: __not_authorised() return False - header_ui(auth.get(SessionKeyNameForAuth.NAME.name, "")) return True else: log.debug("auth_required(): No valid auth session found. User needs to re-authenticate.") @@ -604,10 +604,10 @@ def chat_ui(feature: FeatureKey) -> None: if st.button("Load chat history earlier"): query_chat_history(feature) - if floating_action_button("New chat", icon="+"): + if st_header.floating_action_button("New chat", icon="+"): handle_create_new_chat(feature) - if menu_option("Chat Settings"): + if st_header.menu_option("Chat Settings"): print("\x1b[31mChat settings test\x1b[0m") day = format_datetime(get_chat_session(feature.type_, SessionKeyNameForChat.CUTOFF)) @@ -957,28 +957,28 @@ def admin_docs_ui(q_param: str = None) -> None: _editor_view(q_param) -def org_selection_ui() -> None: - """Render organisation selection UI.""" - try: - current_org_id = get_selected_org_id() - except KeyError: - current_org_id = None - if current_org_id: - orgs = handle_list_orgs() +# def org_selection_ui() -> None: +# """Render organisation selection UI.""" +# try: +# current_org_id = get_selected_org_id() +# except KeyError: +# current_org_id = None +# if current_org_id: +# orgs = handle_list_orgs() - index__ = next((i for i, s in enumerate(orgs) if s[0] == current_org_id), -1) +# index__ = next((i for i, s in enumerate(orgs) if s[0] == current_org_id), -1) - log.debug("org_selection_ui index: %s ", index__) - log.debug("org_selection_ui() orgs: %s", orgs) - selected = st.selectbox( - "Organisation", - orgs, - format_func=lambda x: x[1], - label_visibility="collapsed", - index=index__, - ) - if selected: - handle_org_selection_change(selected[0]) +# log.debug("org_selection_ui index: %s ", index__) +# log.debug("org_selection_ui() orgs: %s", orgs) +# selected = st.selectbox( +# "Organisation", +# orgs, +# format_func=lambda x: x[1], +# label_visibility="collapsed", +# index=index__, +# ) +# if selected: +# handle_org_selection_change(selected[0]) def init_with_pretty_error_ui() -> None: @@ -991,7 +991,37 @@ def init_with_pretty_error_ui() -> None: st.stop() -def header_ui(name: str) -> None: - """Header UI.""" - avatar_src = handle_get_gravatar_url() - render_header(username=name, avatar_src=avatar_src) +def setup_page_scripts() -> None: + """Setup page scripts. + + Called at the begining of each page. + """ + auth_state = get_auth_state() + st_header._setup_page_script(auth_state) + st_sidebar._setup_page_script(auth_state) + + +def run_page_scripts() -> None: + """Run page scripts. + + Called at the end of each page. + """ + auth_state, name = get_auth_state() + + if auth_state: + handle_org_selection_change() + selected_org_id, org_list = get_selected_org_id(), handle_list_orgs() + selected_org_name = next((x[1] for x in org_list if x[0] == selected_org_id), None) + print(f"\x1b31mDebug selected org name {selected_org_name}\x1b[0m") + st_sidebar.run_script( + auth_state=auth_state, + org_options=[ x[1] for x in org_list ], + selected_org=selected_org_name, + ) + + avatar_src = handle_get_gravatar_url() + st_header.run_script(auth_state, name, avatar_src) + else: + st_sidebar.run_script(auth_state) + st_header.run_script(auth_state) + From 1e7225d2ecb9329e70af559cb4c9437f743cd735 Mon Sep 17 00:00:00 2001 From: Jashon Osala Date: Tue, 10 Oct 2023 09:55:36 +0300 Subject: [PATCH 11/16] chore: Update st_components utils. --- web/st_components/static_utils.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/web/st_components/static_utils.py b/web/st_components/static_utils.py index 8d95b13f..182e015b 100644 --- a/web/st_components/static_utils.py +++ b/web/st_components/static_utils.py @@ -8,9 +8,6 @@ get_script_run_ctx, ) -import os -import sys - def load_file_variables(file_path: str, vars_: dict = None) -> str: """Load file variables.""" @@ -25,7 +22,6 @@ def load_file_variables(file_path: str, vars_: dict = None) -> str: def get_current_page_info() -> str: """Get the current page name.""" - main_script_path = os.path.abspath(sys.argv[0]) pages = get_pages("") ctx = get_script_run_ctx() if ctx is not None: From 98b1e7b6125f74696175b3ca728c2e62b51b19c1 Mon Sep 17 00:00:00 2001 From: Jashon Osala Date: Sat, 14 Oct 2023 01:22:48 +0300 Subject: [PATCH 12/16] chore: Add cleanup script for the sidebar and header. --- web/st_components/page_header/__init__.py | 13 ++++-- .../page_header/static/header.js | 42 +++++++++---------- web/st_components/sidebar_header/__init__.py | 28 +++++++++---- .../sidebar_header/static/sidebar.js | 28 +++++-------- web/utils/layout.py | 5 +-- 5 files changed, 64 insertions(+), 52 deletions(-) diff --git a/web/st_components/page_header/__init__.py b/web/st_components/page_header/__init__.py index 2e2d0c44..4eb011bf 100644 --- a/web/st_components/page_header/__init__.py +++ b/web/st_components/page_header/__init__.py @@ -132,12 +132,12 @@ def _setup_page_script(auth_state: bool) -> None: __page_header_api = _PageHeaderAPI() __page_header_api.auth_state = auth_state st.session_state[_key] = __page_header_api - html(f"",height=0,) + # html(f"",height=0,) except Exception as e: log.error("Page header not initialized properly. error: %s", e) -def menu_option(label: str, key: str = None) -> bool: +def _menu_option(label: str, key: str = None) -> bool: """Add a menu option.""" f_label = label.strip().replace(" ", "_").lower() script_caller_info = get_current_page_info() @@ -153,7 +153,7 @@ def menu_option(label: str, key: str = None) -> bool: log.error("Page header not initialized. Please run `setup_page_script` first. error: %s", e) -def floating_action_button(label: str, key: str = None, icon: str = None) -> bool: +def _floating_action_button(label: str, key: str = None, icon: str = None) -> bool: """Add a floating action button.""" f_label = label.strip().replace(" ", "_").lower() script_caller_info = get_current_page_info() @@ -169,7 +169,7 @@ def floating_action_button(label: str, key: str = None, icon: str = None) -> boo log.error("Page header not initialized. Please run `setup_page_script` first. error: %s", e) -def run_script(auth_state: bool, username: str = None, avatar_src: str = None) -> None: +def _run_script(auth_state: bool, username: str = None, avatar_src: str = None) -> None: """Render the header bar. Args: @@ -190,3 +190,8 @@ def run_script(auth_state: bool, username: str = None, avatar_src: str = None) - html(f"",height=0,) except KeyError as e: log.error("Page header not initialized. Please run `setup_page_script` first. error: %s", e) + + +floating_action_button = _floating_action_button +menu_option = _menu_option +run_script = _run_script diff --git a/web/st_components/page_header/static/header.js b/web/st_components/page_header/static/header.js index d373fb17..5017afa1 100644 --- a/web/st_components/page_header/static/header.js +++ b/web/st_components/page_header/static/header.js @@ -1,11 +1,11 @@ // PAGE-HEADER-SCRIPT === DO NOT REMOVE THIS COMMENT --- Used to identify this script in the page header -parent = window.parent.document || window.document; +const __parent = window.parent.document || window.document; // Get params === These are to be set in the template by the method that loads this script const username = `{{username}}`; // User name to be displayed in the header const avatarSrc = `{{avatar_src}}`; // Avatar image source const menuItemsJson = `{{menu_items_json}}`; // [{ "text": "Menu item text", "key": "menu-item-button-key", "icon": "menu-item-icon-html"}] -const styleDoc = `{{style_doc}}`; // CSS string to be added to the parent.document.head +const styleDoc = `{{style_doc}}`; // CSS string to be added to the __parent.document.head const fab_config = `{{fab_config}}`; // { "icon": "fab-icon", "label": "tool-tip-text", "key": "fab-button-key" } const authState = `{{auth_state}}`; // "authenticated" or "unauthenticated" @@ -16,15 +16,15 @@ const defaultMenuItemIcon = `` } /** * Creates a user menu item using the given text and icon html string * @param {string} text The text to be displayed in the menu item - * @param {string} imgHtml + * @param {string} name * @returns {HTMLButtonElement} The user menu item */ -function createUserMenuItem(text, imgHtml = null){ +function createUserMenuItem(text, name = null){ const item = __parent.createElement('button') item.setAttribute('class', 'docq-user-menu-item') item.setAttribute('id', `docq-user-menu-item-${text.replace(' ', '-')}`) - if (imgHtml) { - const iconWithClass = insertUserMenuItemIconClass(imgHtml) - item.innerHTML = `${iconWithClass}${text}` + if (name) { + item.innerHTML = `${createFAIcon(name)} ${text}` } else { item.innerHTML = `${text}` } @@ -127,8 +132,7 @@ userProfile.setAttribute("class", "docq-user-menu-profile"); userProfile.innerHTML = ``; // Logout =================================================================================== -const logoutImgHtml = ` ` -const logoutBtn = createUserMenuItem("Logout", logoutImgHtml) +const logoutBtn = createUserMenuItem("Logout", 'logout') logoutBtn.addEventListener("click", () => { const btns = __parent.querySelectorAll('button[kind="primary"]'); const logoutBtn = Array.from(btns).find((btn) => btn.innerText === "Logout"); @@ -141,15 +145,13 @@ logoutBtn.addEventListener("click", () => { /** Help and Feedback section */ // Help ===================================================================================== -const helpSvgHtml = ` ` -const helpBtn = createUserMenuItem("Help", helpSvgHtml) +const helpBtn = createUserMenuItem("Help", 'help') helpBtn.addEventListener("click", () => { window.open("https://docq.ai", "_blank"); }); // Send feedback =========================================================================== -const feedbackSvgHtml = `` -const feedbackBtn = createUserMenuItem("Send feedback", feedbackSvgHtml) +const feedbackBtn = createUserMenuItem("Send feedback", 'feedback') feedbackBtn.addEventListener("click", () => { window.open("https://docq.ai", "_blank"); }); @@ -162,7 +164,7 @@ userMenu.appendChild(logoutBtn) if (!matchParamNotSet.test(menuItemsJson)) { const menuItems = JSON.parse(menuItemsJson) menuItems.forEach(item => { - const icon = item?.icon || defaultMenuItemIcon + const icon = item.icon || "square" // default icon const menuItem = createUserMenuItem(item.text, icon) menuItem.addEventListener('click', () => { const btns = __parent.querySelectorAll('button[kind="primary"]'); @@ -182,7 +184,6 @@ userMenu.appendChild(helpBtn); userMenu.appendChild(feedbackBtn); // Add user menu to avatar container -console.log("authState", authState); if (authState === "authenticated") { avatarContainer.appendChild(userMenu); } diff --git a/web/st_components/sidebar_header/__init__.py b/web/st_components/sidebar_header/__init__.py index 97c3a8f3..d436f51f 100644 --- a/web/st_components/sidebar_header/__init__.py +++ b/web/st_components/sidebar_header/__init__.py @@ -24,6 +24,11 @@ class _SideBarHeaderAPI: __header_script: str = None __header_style: str = None __auth_state: str = "unauthenticated" + __active_elements: list = [ + "docq-org-dropdown", + "docq-header-container", + "docq-floating-action-button", + ] def __init__(self: Self,) -> None: """Initialize the class.""" @@ -57,6 +62,11 @@ def logo_url(self: Self,) -> str: """Get the URL to logo.""" return self.__logo_url + @property + def active_elements(self: Self,) -> list: + """Get the active elements.""" + return json.dumps(self.__active_elements) + @logo_url.setter def logo_url(self: Self, value: str) -> None: """Set the URL to logo.""" @@ -151,16 +161,18 @@ def _run_script(auth_state: bool, selected_org: str = None, org_options: list = def _cleanup_script() -> None: """Cleanup the script.""" __side_bar_header_api.reset_user_details() - components.html(""" - // ST-SIDEBAR-SCRIPT-CONTAINER + components.html(f""" """, height=0 ) + + run_script = _run_script diff --git a/web/st_components/sidebar_header/static/sidebar.css b/web/st_components/sidebar_header/static/sidebar.css index d4bf8152..655dbd43 100644 --- a/web/st_components/sidebar_header/static/sidebar.css +++ b/web/st_components/sidebar_header/static/sidebar.css @@ -1,3 +1,7 @@ +section[data-testid="stSidebar"] div[data-testid="stSidebarNav"] { + min-height: 800px !important; +} + section[data-testid="stSidebar"] ul { padding-top: 1rem !important; } diff --git a/web/st_components/sidebar_header/static/sidebar.js b/web/st_components/sidebar_header/static/sidebar.js index d5c40d27..30348c3b 100644 --- a/web/st_components/sidebar_header/static/sidebar.js +++ b/web/st_components/sidebar_header/static/sidebar.js @@ -147,10 +147,10 @@ const sideBar = findSideBar(); if (sideBar) { // Check if the logo already exists const docqLogo = __parent.getElementById("docq-logo-container"); - if (docqLogo) { - docqLogo.remove(); + if (!docqLogo || docqLogo.innerHTML !== docqLogoContainer.innerHTML) { + if(docqLogo) docqLogo.remove(); + sideBar.insertBefore(docqLogoContainer, sideBar.firstChild); } - sideBar.insertBefore(docqLogoContainer, sideBar.firstChild); } diff --git a/web/utils/layout.py b/web/utils/layout.py index bb696041..807e4d69 100644 --- a/web/utils/layout.py +++ b/web/utils/layout.py @@ -608,7 +608,7 @@ def chat_ui(feature: FeatureKey) -> None: if st_header.floating_action_button("New chat", icon="+"): handle_create_new_chat(feature) - if st_header.menu_option("Chat Settings"): + if st_header.menu_option("Chat Settings", "settings"): print("\x1b[31mChat settings test\x1b[0m") day = format_datetime(get_chat_session(feature.type_, SessionKeyNameForChat.CUTOFF)) @@ -981,7 +981,6 @@ def admin_docs_ui(q_param: str = None) -> None: # if selected: # handle_org_selection_change(selected[0]) - def init_with_pretty_error_ui() -> None: """UI to run setup and prevent showing errors to the user.""" try: diff --git a/web/utils/sessions.py b/web/utils/sessions.py index aee05b7d..3f405d00 100644 --- a/web/utils/sessions.py +++ b/web/utils/sessions.py @@ -125,6 +125,9 @@ def get_selected_org_id() -> int | None: def set_selected_org_id(org_id: int) -> None: """Set the selected org_id context.""" _set_session_value(org_id, SessionKeySubName.AUTH, SessionKeyNameForAuth.SELECTED_ORG_ID.name) + set_cache_auth_session( + st.session_state[SESSION_KEY_NAME_DOCQ][SessionKeySubName.AUTH.name][SessionKeyNameForAuth.ID.name] + ) def get_username() -> str | None: From 9415858a8a0c52c5626c8f3309252e935adf8756 Mon Sep 17 00:00:00 2001 From: Jashon Osala Date: Sat, 14 Oct 2023 05:50:41 +0300 Subject: [PATCH 14/16] chore: Update sidebar selector component. --- web/utils/handlers.py | 8 ++++---- web/utils/layout.py | 23 ----------------------- web/utils/sessions.py | 8 ++++++-- 3 files changed, 10 insertions(+), 29 deletions(-) diff --git a/web/utils/handlers.py b/web/utils/handlers.py index b48ed6c9..0cec93de 100644 --- a/web/utils/handlers.py +++ b/web/utils/handlers.py @@ -345,10 +345,10 @@ def handle_org_selection_change() -> None: org_selection = st_sidebar.get_selected_org_from_ui() if org_selection is not None: orgs_list = handle_list_orgs(org_selection) - new_org_id = orgs_list[0][0] - set_selected_org_id(new_org_id) - - set_if_current_user_is_selected_org_admin(new_org_id) + new_org_id = next((x[0] for x in orgs_list if x[1] == org_selection), None) + if new_org_id: + set_selected_org_id(new_org_id) + set_if_current_user_is_selected_org_admin(new_org_id) st.experimental_set_query_params() diff --git a/web/utils/layout.py b/web/utils/layout.py index 807e4d69..8236d883 100644 --- a/web/utils/layout.py +++ b/web/utils/layout.py @@ -958,29 +958,6 @@ def admin_docs_ui(q_param: str = None) -> None: _editor_view(q_param) -# def org_selection_ui() -> None: -# """Render organisation selection UI.""" -# try: -# current_org_id = get_selected_org_id() -# except KeyError: -# current_org_id = None -# if current_org_id: -# orgs = handle_list_orgs() - -# index__ = next((i for i, s in enumerate(orgs) if s[0] == current_org_id), -1) - -# log.debug("org_selection_ui index: %s ", index__) -# log.debug("org_selection_ui() orgs: %s", orgs) -# selected = st.selectbox( -# "Organisation", -# orgs, -# format_func=lambda x: x[1], -# label_visibility="collapsed", -# index=index__, -# ) -# if selected: -# handle_org_selection_change(selected[0]) - def init_with_pretty_error_ui() -> None: """UI to run setup and prevent showing errors to the user.""" try: diff --git a/web/utils/sessions.py b/web/utils/sessions.py index 3f405d00..d76a3f2b 100644 --- a/web/utils/sessions.py +++ b/web/utils/sessions.py @@ -29,7 +29,11 @@ def _init_session_state() -> None: def session_state_exists() -> bool: """Check if any session state exists.""" - return SESSION_KEY_NAME_DOCQ in st.session_state + docq_initialized = SESSION_KEY_NAME_DOCQ in st.session_state + auth_initialized = SessionKeySubName.AUTH.name in st.session_state.get(SESSION_KEY_NAME_DOCQ, {}) + if docq_initialized and auth_initialized: + return bool(get_auth_session()) + return False def reset_session_state() -> None: @@ -126,7 +130,7 @@ def set_selected_org_id(org_id: int) -> None: """Set the selected org_id context.""" _set_session_value(org_id, SessionKeySubName.AUTH, SessionKeyNameForAuth.SELECTED_ORG_ID.name) set_cache_auth_session( - st.session_state[SESSION_KEY_NAME_DOCQ][SessionKeySubName.AUTH.name][SessionKeyNameForAuth.ID.name] + st.session_state[SESSION_KEY_NAME_DOCQ][SessionKeySubName.AUTH.name] ) From 8220f306b6fba962c64252699751667361d4c616 Mon Sep 17 00:00:00 2001 From: Jashon Osala Date: Mon, 23 Oct 2023 14:55:44 +0300 Subject: [PATCH 15/16] chore: Remove org DDL selector from sidebar header. --- web/st_components/sidebar_header/__init__.py | 166 +----------------- .../sidebar_header/static/sidebar.js | 104 ++--------- web/utils/handlers.py | 22 +-- web/utils/layout.py | 80 ++++----- web/utils/sessions.py | 9 +- 5 files changed, 67 insertions(+), 314 deletions(-) diff --git a/web/st_components/sidebar_header/__init__.py b/web/st_components/sidebar_header/__init__.py index d436f51f..fe3a82f0 100644 --- a/web/st_components/sidebar_header/__init__.py +++ b/web/st_components/sidebar_header/__init__.py @@ -1,12 +1,6 @@ """Nav UI example.""" -import base64 -import json import os -from datetime import datetime -from typing import Self -from urllib.parse import unquote -import streamlit as st import streamlit.components.v1 as components from ..static_utils import load_file_variables @@ -15,164 +9,22 @@ script_path = os.path.join(parent_dir, "static", "sidebar.js") css_path = os.path.join(parent_dir, "static", "sidebar.css") -class _SideBarHeaderAPI: - """Custom API for the st_components package.""" - __selected_org: str = None - __org_options_json: str = None - __logo_url: str = None - __header_script: str = None - __header_style: str = None - __auth_state: str = "unauthenticated" - __active_elements: list = [ - "docq-org-dropdown", - "docq-header-container", - "docq-floating-action-button", - ] +def _get_script(logo_url: str = None) -> str: + """Get the script.""" + style = load_file_variables(css_path, {}) + return load_file_variables(script_path, { + "logo_url": logo_url, + "style_doc": style, + }) - def __init__(self: Self,) -> None: - """Initialize the class.""" - self.__header_style = load_file_variables(css_path, {}) - self._load_script() - @property - def selected_org(self: Self,) -> str: - """Get the currently selected org.""" - return self.__selected_org - @selected_org.setter - def selected_org(self: Self, value: str) -> None: - """Set the currently selected org.""" - self.__selected_org = value - self._load_script() - - @property - def org_options_list(self: Self,) -> str: - """Get the json string containing available orgs.""" - return self.__org_options_json - - @org_options_list.setter - def org_options_list(self: Self, value: list) -> None: - """Set the json string containing available orgs.""" - self.__org_options_json = json.dumps(value) - self._load_script() - - @property - def logo_url(self: Self,) -> str: - """Get the URL to logo.""" - return self.__logo_url - - @property - def active_elements(self: Self,) -> list: - """Get the active elements.""" - return json.dumps(self.__active_elements) - - @logo_url.setter - def logo_url(self: Self, value: str) -> None: - """Set the URL to logo.""" - self.__logo_url = value - self._load_script() - - @property - def script(self: Self,) -> str: - """Get the script.""" - return self.__header_script - - @property - def style(self: Self,) -> str: - """Get the style.""" - return self.__header_style - - @property - def auth_state(self: Self,) -> str: - """Get the auth state.""" - return self.__auth_state - - @auth_state.setter - def auth_state(self: Self, value: bool) -> None: - """Set the auth state.""" - auth_state = "authenticated" if value else "unauthenticated" - if auth_state == "unauthenticated" and self.__auth_state != auth_state: - self.reset_user_details() - self.__auth_state = auth_state - self._load_script() - - def reset_user_details(self: Self,) -> None: - """Reset the instance.""" - self.__selected_org = None - self.__org_options_json = None - self.__auth_state = "unauthenticated" - - def _load_script(self: Self,) -> None: - params = { - "selected_org": self.selected_org, - "org_options_json": self.org_options_list, - "logo_url": self.logo_url, - "style_doc": self.style, - "auth_state": self.auth_state, - } - self.__header_script = load_file_variables(script_path, params) - - -__side_bar_header_api = _SideBarHeaderAPI() - - -def _setup_page_script(auth_state: bool) -> None: - """Setup the page script.""" - __side_bar_header_api.auth_state = auth_state - - -def set_selected_org(selected_org: str) -> None: - """Set the current org.""" - __side_bar_header_api.selected_org = selected_org - - -def update_org_options(org_options: list) -> None: - """Update the org options.""" - __side_bar_header_api.org_options_list = org_options - - -def get_selected_org_from_ui() -> str | None: - """Get the current org.""" - param = st.experimental_get_query_params().get("org", [None])[0] - if param is not None: - org, timestamp = base64.b64decode( - unquote(param) - ).decode("utf-8").split("::") - now_ = datetime.now() - if now_.timestamp() - float(timestamp) < 60: - return org - return None - - -def _run_script(auth_state: bool, selected_org: str = None, org_options: list = None) -> None: +def run_script(logo_url: str = None) -> None: """Run the script.""" - __side_bar_header_api.selected_org = selected_org - __side_bar_header_api.org_options_list = org_options - __side_bar_header_api.auth_state = auth_state components.html(f""" // ST-SIDEBAR-SCRIPT-CONTAINER - - """, - height=0 - ) - - -def _cleanup_script() -> None: - """Cleanup the script.""" - __side_bar_header_api.reset_user_details() - components.html(f""" - + """, height=0 ) - - - -run_script = _run_script diff --git a/web/st_components/sidebar_header/static/sidebar.js b/web/st_components/sidebar_header/static/sidebar.js index 30348c3b..e56e83da 100644 --- a/web/st_components/sidebar_header/static/sidebar.js +++ b/web/st_components/sidebar_header/static/sidebar.js @@ -2,11 +2,7 @@ const __parent = window.parent.document || window.document; // === Required params === -const selectedOrg = `{{selected_org}}`; -const orgOptionsJson = `{{org_options_json}}`; const styleDoc = `{{style_doc}}`; -const orgSelectorLabel = `{{org_selector_label}}`; -const authState = `{{auth_state}}`; // === Optional params === const logoUrl = `{{logo_url}}`; @@ -15,7 +11,7 @@ const logoUrl = `{{logo_url}}`; const matchParamNotSet = /\{\{.*\}\}/g; -// === Style document ====================================================================================================================================================== +// === Style document ======================================= const styleDocElement = document.createElement("style"); styleDocElement.setAttribute("id", "docq-sidebar-style-doc"); @@ -29,7 +25,7 @@ if (!matchParamNotSet.test(styleDoc)) { } } -// === Util functions ====================================================================================================================================================== +// === Util functions ================================ const findSideBar = () => { const sideBar = __parent.querySelectorAll('section[data-testid="stSidebar"]'); if (sideBar) { @@ -37,25 +33,10 @@ const findSideBar = () => { } return null; }; - -/** - * Create dropdown option - * @param {string} value dropdown option value - * @param {string} text dropdown option text - * @returns {HTMLOptionElement} HTML option element - */ -function createSelectOption (value, text) { - const option = document.createElement("option"); - option.setAttribute("value", value); - option.setAttribute("class", "docq-select-option") - option.innerHTML = text; - return option; -} - -// === End util functions ============================== +// === End util functions === -// === Container for the logo ============================================================================================================================================= +// === Container for the logo ======================== const docqLogoContainer = document.createElement("div"); docqLogoContainer.setAttribute("class", "docq-logo-container"); docqLogoContainer.setAttribute("id", "docq-logo-container"); @@ -64,7 +45,7 @@ docqLogoContainer.setAttribute( "display: flex; justify-content: center; align-items: center; width: 100%; position: sticky; top: 0; z-index: 1000; background-color: transparent; flex-direction: column; padding: 10px;" ); -// === Close button ========================================================== +// === Close button ================================== const closeIcon = ``; const closeButton = document.createElement("button"); closeButton.setAttribute("id", "docq-close-button"); @@ -88,58 +69,26 @@ closeButton.addEventListener("click", () => { docqLogoContainer.appendChild(closeButton); -// === Logo ================================================================================================= +// === Logo ================================================ const docqLogo = document.createElement("img"); const logoSrc = logoUrl && !logoUrl.match(matchParamNotSet) ? logoUrl : "https://github.com/docqai/docq/blob/main/docs/assets/logo.jpg?raw=true" +const docqLogoLink = document.createElement("a"); +docqLogoLink.setAttribute("href", "/"); +docqLogoLink.setAttribute("target", "_self"); +docqLogoLink.setAttribute("style", "text-decoration: none; width: 25% !important;"); +docqLogoLink.setAttribute("id", "docq-logo-link"); + docqLogo.setAttribute("src", logoSrc); docqLogo.setAttribute("alt", "docq logo"); -docqLogo.setAttribute("style", "width: 25%;"); +docqLogo.setAttribute("style", "width: 100%;"); docqLogo.setAttribute("id", "docq-logo"); docqLogo.setAttribute("async", "1"); -docqLogoContainer.appendChild(docqLogo); - - -// === Dropdown menu ========================================================================================== - -const orgDropdown = document.createElement("div"); -orgDropdown.setAttribute("id", "docq-org-dropdown"); -orgDropdown.setAttribute("style", "margin-top: 10px;"); - -const selectLabel = document.createElement("label"); -selectLabel.setAttribute("for", "docq-org-dropdown-select"); -selectLabel.setAttribute("class", "docq-select-label"); -selectLabel.innerHTML = "Select org:"; -if (!matchParamNotSet.test(orgSelectorLabel)) { - selectLabel.innerHTML = orgSelectorLabel; -} - -selectLabel.setAttribute("style", "margin-right: 10px;"); - -const selectMenu = document.createElement("select"); -selectMenu.setAttribute("id", "docq-org-dropdown-select"); -selectMenu.setAttribute("onchange", "selectOrg(this.value)"); - -if (orgOptionsJson && !orgOptionsJson.match(matchParamNotSet)) { - const orgOptions = JSON.parse(orgOptionsJson); - orgOptions.forEach((org) => { - const option = createSelectOption(org, org); - if (org === selectedOrg) { - option.setAttribute("selected", "selected"); - } - selectMenu.appendChild(option); - }); -}; - -orgDropdown.appendChild(selectLabel); -orgDropdown.appendChild(selectMenu); - -if (!selectedOrg.match(matchParamNotSet) && !orgOptionsJson.match(matchParamNotSet) && authState === "authenticated") { - docqLogoContainer.appendChild(orgDropdown); -} +docqLogoLink.appendChild(docqLogo); +docqLogoContainer.appendChild(docqLogoLink); const sideBar = findSideBar(); @@ -153,27 +102,6 @@ if (sideBar) { } } - -// === Add scripts to parent document === -const selectOrgScript = document.createElement("script"); -selectOrgScript.setAttribute("type", "text/javascript"); -selectOrgScript.setAttribute("id", "docq-select-org-script"); -selectOrgScript.innerHTML = ` - function selectOrg(org) { - const timeStamp = new Date().getTime(); - const orgParam = encodeURIComponent(btoa(org + "::" + timeStamp)); - window.parent.location.href = \`?org=\${orgParam}\`; - } -`; - -const prevScript =__parent.getElementById("docq-select-org-script"); -if (prevScript) { - prevScript.remove(); -} - -__parent.body.appendChild(selectOrgScript); - -// === const iframes =__parent.querySelectorAll("iframe"); iframes.forEach((iframe) => { const srcdoc = iframe.getAttribute("srcdoc"); @@ -181,4 +109,4 @@ iframes.forEach((iframe) => { iframe.parentNode.setAttribute("class", "docq-iframe-container"); } }); -// === EOF ================================================================================================================================================================= +// === EOF === diff --git a/web/utils/handlers.py b/web/utils/handlers.py index 0cec93de..49c5d277 100644 --- a/web/utils/handlers.py +++ b/web/utils/handlers.py @@ -8,7 +8,6 @@ from datetime import datetime from typing import Any, List, Optional, Tuple -import st_components.sidebar_header as st_sidebar import streamlit as st from docq import ( config, @@ -340,16 +339,11 @@ def handle_list_orgs(name_match: str = None) -> List[Tuple]: return manage_organisations.list_organisations(name_match=name_match, user_id=current_user_id) -def handle_org_selection_change() -> None: +def handle_org_selection_change(org_id: int) -> None: """Handle org selection change.""" - org_selection = st_sidebar.get_selected_org_from_ui() - if org_selection is not None: - orgs_list = handle_list_orgs(org_selection) - new_org_id = next((x[0] for x in orgs_list if x[1] == org_selection), None) - if new_org_id: - set_selected_org_id(new_org_id) - set_if_current_user_is_selected_org_admin(new_org_id) - st.experimental_set_query_params() + set_selected_org_id(org_id) + + set_if_current_user_is_selected_org_admin(org_id) def handle_create_space_group() -> int: @@ -726,11 +720,3 @@ def handle_public_session() -> None: space_group_id=-1, public_session_id=-1, ) - - -def get_auth_state() -> tuple[bool, str | None]: - """Check the auth state.""" - auth_session = get_auth_session() - state = auth_session and not auth_session.get(SessionKeyNameForAuth.ANONYMOUS.name) - name = auth_session.get(SessionKeyNameForAuth.NAME.name) - return state, name diff --git a/web/utils/layout.py b/web/utils/layout.py index 8236d883..9515a2e2 100644 --- a/web/utils/layout.py +++ b/web/utils/layout.py @@ -3,8 +3,7 @@ import logging as log from typing import List, Tuple -import st_components.page_header as st_header -import st_components.sidebar_header as st_sidebar +import st_components.sidebar_header as st_header import streamlit as st from docq import setup from docq.access_control.main import SpaceAccessType @@ -29,7 +28,6 @@ from .formatters import format_archived, format_datetime, format_filesize, format_timestamp from .handlers import ( _set_session_state_configs, - get_auth_state, get_enabled_features, get_max_number_of_documents, get_shared_space, @@ -261,9 +259,8 @@ def __login_form() -> None: def __logout_button() -> None: - if st.button("Logout", type="primary"): + if st.button("Logout"): handle_logout() - st_sidebar._cleanup_script() st.experimental_rerun() @@ -319,6 +316,7 @@ def auth_required(show_login_form: bool = True, requiring_admin: bool = False, s if requiring_admin: __not_authorised() return False + return True else: log.debug("auth_required(): No valid auth session found. User needs to re-authenticate.") @@ -600,17 +598,14 @@ def chat_ui(feature: FeatureKey) -> None: ) st.checkbox("Including your documents", value=True, key="chat_personal_space") - _, chat_histoy, _ = st.columns([1,1,1]) - with chat_histoy: + load_history, create_new_chat = st.columns([3, 1]) + with load_history: if st.button("Load chat history earlier"): query_chat_history(feature) - - if st_header.floating_action_button("New chat", icon="+"): - handle_create_new_chat(feature) - - if st_header.menu_option("Chat Settings", "settings"): - print("\x1b[31mChat settings test\x1b[0m") - + with create_new_chat: + if st.button("New chat"): + handle_create_new_chat(feature) + with st.container(): day = format_datetime(get_chat_session(feature.type_, SessionKeyNameForChat.CUTOFF)) st.markdown(f"#### {day}") @@ -958,6 +953,30 @@ def admin_docs_ui(q_param: str = None) -> None: _editor_view(q_param) +def org_selection_ui() -> None: + """Render organisation selection UI.""" + try: + current_org_id = get_selected_org_id() + except KeyError: + current_org_id = None + if current_org_id: + orgs = handle_list_orgs() + + index__ = next((i for i, s in enumerate(orgs) if s[0] == current_org_id), -1) + + log.debug("org_selection_ui index: %s ", index__) + log.debug("org_selection_ui() orgs: %s", orgs) + selected = st.selectbox( + "Organisation", + orgs, + format_func=lambda x: x[1], + label_visibility="collapsed", + index=index__, + ) + if selected: + handle_org_selection_change(selected[0]) + + def init_with_pretty_error_ui() -> None: """UI to run setup and prevent showing errors to the user.""" try: @@ -967,36 +986,11 @@ def init_with_pretty_error_ui() -> None: log.fatal("Error: setup.init() failed with %s", e) st.stop() - def setup_page_scripts() -> None: - """Setup page scripts. - - Called at the begining of each page. - """ - auth_state = get_auth_state() - st_header._setup_page_script(auth_state) - st_sidebar._setup_page_script(auth_state) + """Setup page scripts.""" + pass def run_page_scripts() -> None: - """Run page scripts. - - Called at the end of each page. - """ - auth_state, name = get_auth_state() - - if auth_state: - handle_org_selection_change() - selected_org_id, org_list = get_selected_org_id(), handle_list_orgs() - st_sidebar.run_script( - auth_state=auth_state, - org_options=[ x[1] for x in org_list ], - selected_org=next((x[1] for x in org_list if x[0] == selected_org_id), None), - ) - - avatar_src = handle_get_gravatar_url() - st_header.run_script(auth_state, name, avatar_src) - else: - st_sidebar.run_script(auth_state) - st_header.run_script(auth_state) - + """Run page scripts.""" + st_header.run_script() diff --git a/web/utils/sessions.py b/web/utils/sessions.py index d76a3f2b..aee05b7d 100644 --- a/web/utils/sessions.py +++ b/web/utils/sessions.py @@ -29,11 +29,7 @@ def _init_session_state() -> None: def session_state_exists() -> bool: """Check if any session state exists.""" - docq_initialized = SESSION_KEY_NAME_DOCQ in st.session_state - auth_initialized = SessionKeySubName.AUTH.name in st.session_state.get(SESSION_KEY_NAME_DOCQ, {}) - if docq_initialized and auth_initialized: - return bool(get_auth_session()) - return False + return SESSION_KEY_NAME_DOCQ in st.session_state def reset_session_state() -> None: @@ -129,9 +125,6 @@ def get_selected_org_id() -> int | None: def set_selected_org_id(org_id: int) -> None: """Set the selected org_id context.""" _set_session_value(org_id, SessionKeySubName.AUTH, SessionKeyNameForAuth.SELECTED_ORG_ID.name) - set_cache_auth_session( - st.session_state[SESSION_KEY_NAME_DOCQ][SessionKeySubName.AUTH.name] - ) def get_username() -> str | None: From d6b7918144ae1b19693c695c2c0276cc4e25e718 Mon Sep 17 00:00:00 2001 From: Jashon Osala Date: Mon, 23 Oct 2023 16:50:07 +0300 Subject: [PATCH 16/16] chore: Make user profile static. --- web/st_components/page_header/__init__.py | 215 +++-------------- .../page_header/static/header.js | 221 +----------------- web/utils/handlers.py | 8 + web/utils/layout.py | 18 +- 4 files changed, 55 insertions(+), 407 deletions(-) diff --git a/web/st_components/page_header/__init__.py b/web/st_components/page_header/__init__.py index 44699f11..2bbde33d 100644 --- a/web/st_components/page_header/__init__.py +++ b/web/st_components/page_header/__init__.py @@ -1,199 +1,42 @@ """Header bar.""" -import json -import logging as log import os -from typing import Self -import streamlit as st from streamlit.components.v1 import html -from ..static_utils import get_current_page_info, load_file_variables +from ..static_utils import load_file_variables parent_dir = os.path.dirname(os.path.abspath(__file__)) script_path = os.path.join(parent_dir, "static", "header.js") css_path = os.path.join(parent_dir, "static", "header.css") -api_key = "page_header_api{page_script_hash}_{page_name}" - - -class _PageHeaderAPI: - """Page header bar API.""" - - __menu_options_json: str = None - __menu_options_list: list = [] # [{"text": "Home", "key": "home"}] - __username: str = None - __avatar_src: str = None - __page_script: str = None - __page_style: str = None - __fab_config: str = None - __auth_state: str = None - - def __init__(self: Self,) -> None: - """Initialize the class.""" - self.__page_style = load_file_variables(css_path, {}) - self._load_script() - - @property - def menu_options_json(self: Self,) -> str: - """Get the json string containing available menu options.""" - return self.__menu_options_json - - @property - def menu_options_list(self: Self,) -> dict: - """Get the dict containing available menu options.""" - return self.__menu_options_list - - @property - def username(self: Self,) -> str: - """Get the username.""" - return self.__username - - @property - def avatar_src(self: Self,) -> str: - """Get the avatar source.""" - return self.__avatar_src - - @property - def script(self: Self,) -> str: - """Get the script.""" - return self.__page_script - - @property - def auth_state(self: Self,) -> str: - """Get the auth state.""" - return self.__auth_state - - @auth_state.setter - def auth_state(self: Self, value: bool) -> None: - """Set the auth state.""" - if value: - self.__auth_state = "authenticated" - else: - self.__auth_state = "unauthenticated" - self._load_script() - - @menu_options_list.setter - def menu_options_list(self: Self, value: list) -> None: - """Set the dict containing available menu options.""" - self.__menu_options_list = value - self.__menu_options_json = json.dumps(value) - self._load_script() - - @username.setter - def username(self: Self, value: str) -> None: - """Set the username.""" - self.__username = value - self._load_script() - - @avatar_src.setter - def avatar_src(self: Self, value: str) -> None: - """Set the avatar source.""" - self.__avatar_src = value - self._load_script() - - - def add_menu_option(self: Self, label: str, key: str, icon: str = None) -> None: - """Add a menu option.""" - for entry in self.__menu_options_list: - if entry["text"] == label and entry["key"] == key: - self.__menu_options_json = json.dumps(self.__menu_options_list) - self._load_script() - return - self.__menu_options_list.append({"text": label, "key": key, "icon": icon}) - self.__menu_options_json = json.dumps(self.__menu_options_list) - self._load_script() - - - def setup_fab(self: Self, tool_tip_label: str, key: str, icon: str = "+") -> None: - """Setup floating action button.""" - self.__fab_config = json.dumps({"label": tool_tip_label, "key": key, "icon": icon}) - self._load_script() - - def _load_script(self: Self,) -> None: - """Load the script.""" - script_args = { - "username": self.__username, - "avatar_src": self.__avatar_src, - "menu_items_json": self.__menu_options_json, - "style_doc": self.__page_style, - "fab_config": self.__fab_config, - "auth_state": self.__auth_state, - } - self.__page_script = load_file_variables(script_path, script_args) - - # Run this at the start of each page def _setup_page_script(auth_state: bool) -> None: """Setup page script.""" - script_caller_info = get_current_page_info() - try: - _key = api_key.format( - page_script_hash=script_caller_info["page_script_hash"], - page_name=script_caller_info["page_name"] - ) - __page_header_api = _PageHeaderAPI() - __page_header_api.auth_state = auth_state - st.session_state[_key] = __page_header_api - # html(f"",height=0,) - except Exception as e: - log.error("Page header not initialized properly. error: %s", e) - - -def _menu_option(label: str, key: str = None, icon: str = None) -> bool: - """Add a menu option.""" - f_label = label.strip().replace(" ", "_").lower() - script_caller_info = get_current_page_info() - try: - _key = api_key.format( - page_script_hash=script_caller_info["page_script_hash"], - page_name=script_caller_info["page_name"] - ) - __page_header_api: _PageHeaderAPI = st.session_state[_key] - __page_header_api.add_menu_option(label=label, key=f_label, icon=icon) - return st.button(label=f_label, key=key, type="primary") - except KeyError as e: - log.error("Page header not initialized. Please run `setup_page_script` first. error: %s", e) - - -def _floating_action_button(label: str, key: str = None, icon: str = None) -> bool: - """Add a floating action button.""" - f_label = label.strip().replace(" ", "_").lower() - script_caller_info = get_current_page_info() - try: - _key = api_key.format( - page_script_hash=script_caller_info["page_script_hash"], - page_name=script_caller_info["page_name"] - ) - __page_header_api: _PageHeaderAPI = st.session_state[_key] - __page_header_api.setup_fab(tool_tip_label=label, key=f_label, icon=icon) - return st.button(label=f_label, key=key, type="primary") - except KeyError as e: - log.error("Page header not initialized. Please run `setup_page_script` first. error: %s", e) - - -def _run_script(auth_state: bool, username: str = None, avatar_src: str = None) -> None: - """Render the header bar. - - Args: - auth_state (bool): Authentication state. - username (str): Username. - avatar_src (str): Avatar source. - """ - script_caller_info = get_current_page_info() - try: - _key = api_key.format( - page_script_hash=script_caller_info["page_script_hash"], - page_name=script_caller_info["page_name"] - ) - __page_header_api: _PageHeaderAPI = st.session_state[_key] - __page_header_api.auth_state = auth_state - __page_header_api.username = username - __page_header_api.avatar_src = avatar_src - html(f"",height=0,) - except KeyError as e: - log.error("Page header not initialized. Please run `setup_page_script` first. error: %s", e) - - -floating_action_button = _floating_action_button -menu_option = _menu_option -run_script = _run_script + auth_ = "true" if auth_state else "false" + html( + f""" + + """, + height=0 + ) + + +def run_script(auth_state: bool, username: str, avatar_src: str, selected_org: str) -> None: + """Render the header bar.""" + style = load_file_variables(css_path, {}) + script_args = { + "username": username, + "avatar_src": avatar_src, + "style_doc": style, + "auth_state": "authenticated" if auth_state else "unauthenticated", + "selected_org": selected_org, + } + html(f"", height=0) diff --git a/web/st_components/page_header/static/header.js b/web/st_components/page_header/static/header.js index 3774685d..325cb7f8 100644 --- a/web/st_components/page_header/static/header.js +++ b/web/st_components/page_header/static/header.js @@ -3,24 +3,13 @@ const __parent = window.parent.document || window.document; // Get params === These are to be set in the template by the method that loads this script const username = `{{username}}`; // User name to be displayed in the header +const selectedOrg = `{{selected_org}}`; // Selected org name const avatarSrc = `{{avatar_src}}`; // Avatar image source -const menuItemsJson = `{{menu_items_json}}`; // [{ "text": "Menu item text", "key": "menu-item-button-key", "icon": "menu-item-icon-html"}] const styleDoc = `{{style_doc}}`; // CSS string to be added to the __parent.document.head -const fab_config = `{{fab_config}}`; // { "icon": "fab-icon", "label": "tool-tip-text", "key": "fab-button-key" } const authState = `{{auth_state}}`; // "authenticated" or "unauthenticated" const matchParamNotSet = /\{\{.*\}\}/; -// Add font awesome icons -const _link = document.createElement('link') -_link.setAttribute('rel', 'stylesheet') -_link.setAttribute('id', 'docq-fa-icon-link') -_link.setAttribute('href', 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css') -const prevIconLink = __parent.getElementById('docq-fa-icon-link') -if (!prevIconLink) { - __parent.head.appendChild(_link) -} - // Add style to the parent document head @@ -37,37 +26,7 @@ if (!matchParamNotSet.test(styleDoc)) { } } -const iconsMap = { "logout": "sign-out", "help": "question-circle", - "feedback": "commenting-o", "settings": "cog", "profile": "user-circle-o", - "new chat": "commenting-o", "new ticket": "ticket", "new task": "tasks", -} - -/** Utility functions */// ========================================================================================================================================================== - -function createFAIcon(name) { - if (name in iconsMap) { - name = iconsMap[name] - } - return `` -} - -/** - * Creates a user menu item using the given text and icon html string - * @param {string} text The text to be displayed in the menu item - * @param {string} name - * @returns {HTMLButtonElement} The user menu item - */ -function createUserMenuItem(text, name = null){ - const item = __parent.createElement('button') - item.setAttribute('class', 'docq-user-menu-item') - item.setAttribute('id', `docq-user-menu-item-${text.replace(' ', '-')}`) - if (name) { - item.innerHTML = `${createFAIcon(name)} ${text}` - } else { - item.innerHTML = `${text}` - } - return item -} +// === Utility functions // === /** * Creates a horizontal divider for the user menu @@ -118,134 +77,10 @@ if (authState === "authenticated" && !matchParamNotSet.test(avatarSrc)) { avatarContainer.appendChild(avatar); } -// User menu ======================================================================================================================================================================== -const userMenu = document.createElement("div"); -userMenu.setAttribute("id", "docq-user-menu"); -userMenu.setAttribute("class", "docq-user-menu"); - -// Usermenu items ========================================================================== - -// Profile ================================================================================= -const userProfile = document.createElement("div"); -userProfile.setAttribute("id", "docq-user-menu-profile"); -userProfile.setAttribute("class", "docq-user-menu-profile"); -userProfile.innerHTML = ``; - -// Logout =================================================================================== -const logoutBtn = createUserMenuItem("Logout", 'logout') -logoutBtn.addEventListener("click", () => { - const btns = __parent.querySelectorAll('button[kind="primary"]'); - const logoutBtn = Array.from(btns).find((btn) => btn.innerText === "Logout"); - if (logoutBtn) { - logoutBtn.click(); - } else { - console.log("Logout button not found", logoutBtn); - } -}) - -/** Help and Feedback section */ -// Help ===================================================================================== -const helpBtn = createUserMenuItem("Help", 'help') -helpBtn.addEventListener("click", () => { - window.open("https://docq.ai", "_blank"); -}); - -// Send feedback =========================================================================== -const feedbackBtn = createUserMenuItem("Send feedback", 'feedback') -feedbackBtn.addEventListener("click", () => { - window.open("https://docq.ai", "_blank"); -}); - -// Add items to user menu -userMenu.appendChild(userProfile); -userMenu.appendChild(createHorizontalDivider()) -userMenu.appendChild(logoutBtn) -// Add menu items from json -if (!matchParamNotSet.test(menuItemsJson)) { - const menuItems = JSON.parse(menuItemsJson) - menuItems.forEach(item => { - const icon = item.icon || "square" // default icon - const menuItem = createUserMenuItem(item.text, icon) - menuItem.addEventListener('click', () => { - const btns = __parent.querySelectorAll('button[kind="primary"]'); - const menuItemBtn = Array.from(btns).find((btn) => btn.innerText === item.key); - if (menuItemBtn) { - menuItemBtn.click(); - } else { - console.log(`Menu item button with key ${item.key} not found`, menuItemBtn); - } - }) - userMenu.appendChild(menuItem) - }) -} - -userMenu.appendChild(createHorizontalDivider()) -userMenu.appendChild(helpBtn); -userMenu.appendChild(feedbackBtn); - -// Add user menu to avatar container -if (authState === "authenticated") { - avatarContainer.appendChild(userMenu); -} - -// User menu toggle -avatar.addEventListener("click", () => { - const userMenu = __parent.getElementById("docq-user-menu"); - if (userMenu) { - userMenu.classList.toggle("docq-user-menu-active"); - // Autofocus on the user menu - const userMenuItems = userMenu.querySelectorAll(".docq-user-menu-item"); - if (userMenuItems.length > 0) { - userMenuItems[0].focus(); - } - - // Close user menu on click outside - const closeUserMenu = (e) => { - if (!userMenu.contains(__parent.activeElement)) { - userMenu.classList.remove("docq-user-menu-active"); - __parent.removeEventListener("click", closeUserMenu); - } - }; - __parent.addEventListener("click", closeUserMenu); - } else { - console.log("User menu not found", userMenu); - } -}); - -// User menu animation -const userMenuObserver = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.attributeName === "class") { - const userMenu = __parent.getElementById("docq-user-menu"); - if (userMenu) { - if (userMenu.classList.contains("docq-user-menu-active")) { - userMenu.style.animation = "docq-user-menu-slide-in 0.2s ease-in-out"; - } else { - userMenu.style.animation = "docq-user-menu-slide-out 0.2s ease-in-out"; - } - } - } - }); -}); - -userMenuObserver.observe(userMenu, { attributes: true }); - -// Close user menu on clicking its child elements -userMenu.addEventListener("click", (e) => { - if (e.target !== userMenu) { - const userMenu = __parent.getElementById("docq-user-menu"); - if (userMenu) { - userMenu.classList.remove("docq-user-menu-active"); - } - } -}); - -// End user menu ==================================================================================================================================================================== - /* User name */ const userName = document.createElement("span"); -userName.innerHTML = `${username}`; +userName.innerHTML = `${username}@${selectedOrg}`; userName.setAttribute("id", "docq-user-name"); if(!matchParamNotSet.test(username) && authState === "authenticated") { @@ -265,56 +100,6 @@ const pageTitle = document.createElement("span"); [right, center, left].forEach((div) => docqContainer.appendChild(div)); -// === Floating action button ==================================================================================================================================================== -const fabContainer = document.createElement("div"); -fabContainer.setAttribute("id", "docq-floating-action-button-container"); -fabContainer.setAttribute("class", "docq-floating-action-button-container"); - -// New chat button -function fabSetup (key, icon) { - const newChatButton = document.createElement("button"); - newChatButton.setAttribute("id", "docq-floating-action-button"); - newChatButton.setAttribute("class", "docq-floating-action-button"); - newChatButton.innerHTML = `${icon}`; - newChatButton.addEventListener("click", () => { - const btns = __parent.querySelectorAll('button[kind="primary"]'); - const newChatBtn = Array.from(btns).find((btn) => btn.innerText.toLowerCase() === key.toLowerCase()); - if (newChatBtn) { - newChatBtn.click(); - } else { - console.log("New chat button not found", newChatBtn, key, icon); - } - }); - return newChatButton; -} - - -function tooltipSetup (label) { - const newChatTooltip = document.createElement("span"); - newChatTooltip.setAttribute("id", "docq-fab-tooltip"); - newChatTooltip.setAttribute("class", "docq-fab-tooltip"); - newChatTooltip.innerHTML = label; - return newChatTooltip; -} - -previousFabButton = __parent.getElementById("docq-floating-action-button"); -if (previousFabButton) { - previousFabButton.remove(); -} - -if (!matchParamNotSet.test(fab_config)) { - const { icon, label, key } = JSON.parse(fab_config) - const newChatButton = fabSetup(key, icon) - const newChatTooltip = tooltipSetup(label) - fabContainer.appendChild(newChatTooltip); - fabContainer.appendChild(newChatButton); - if(authState === "authenticated") { - __parent.body.appendChild(fabContainer); - } -} - -// === END Floating action button ======================= - // Insert docq container in the DOM stApp = __parent.querySelector("header[data-testid='stHeader']"); if (stApp) { diff --git a/web/utils/handlers.py b/web/utils/handlers.py index 49c5d277..41f40b66 100644 --- a/web/utils/handlers.py +++ b/web/utils/handlers.py @@ -720,3 +720,11 @@ def handle_public_session() -> None: space_group_id=-1, public_session_id=-1, ) + + +def get_auth_state() -> tuple[bool, str | None]: + """Check the auth state.""" + auth_session = get_auth_session() + state = auth_session and not auth_session.get(SessionKeyNameForAuth.ANONYMOUS.name) + name = auth_session.get(SessionKeyNameForAuth.NAME.name) + return state, name diff --git a/web/utils/layout.py b/web/utils/layout.py index 9515a2e2..1ecad4dd 100644 --- a/web/utils/layout.py +++ b/web/utils/layout.py @@ -3,7 +3,8 @@ import logging as log from typing import List, Tuple -import st_components.sidebar_header as st_header +import st_components.page_header as st_header +import st_components.sidebar_header as st_sidebar import streamlit as st from docq import setup from docq.access_control.main import SpaceAccessType @@ -28,6 +29,7 @@ from .formatters import format_archived, format_datetime, format_filesize, format_timestamp from .handlers import ( _set_session_state_configs, + get_auth_state, get_enabled_features, get_max_number_of_documents, get_shared_space, @@ -986,11 +988,21 @@ def init_with_pretty_error_ui() -> None: log.fatal("Error: setup.init() failed with %s", e) st.stop() + def setup_page_scripts() -> None: """Setup page scripts.""" - pass + auth_state, _ = get_auth_state() + st_header._setup_page_script(auth_state) def run_page_scripts() -> None: """Run page scripts.""" - st_header.run_script() + selected_org_id, org_list = get_selected_org_id(), handle_list_orgs() + auth_state, name = get_auth_state() + st_sidebar.run_script() + st_header.run_script( + auth_state=auth_state, + selected_org=next((x[1] for x in org_list if x[0] == selected_org_id), None), + username=name, + avatar_src=handle_get_gravatar_url(), + )