diff --git a/Dockerfile b/Dockerfile index 1ad98b5..7143ec3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -137,6 +137,9 @@ RUN chmod +x /app/entrypoint.sh # Patch Analytics RUN mamba run -n streamlit-env python hooks/hook-analytics.py +# Set Online Deployment +RUN jq '.online_deployment = true' settings.json > tmp.json && mv tmp.json settings.json + # Download latest OpenMS App executable for Windows from Github actions workflow. RUN WORKFLOW_ID=$(curl -s "https://api.github.com/repos/$GITHUB_USER/$GITHUB_REPO/actions/workflows" | jq -r '.workflows[] | select(.name == "Build executable for Windows") | .id') \ && SUCCESSFUL_RUNS=$(curl -s "https://api.github.com/repos/$GITHUB_USER/$GITHUB_REPO/actions/runs?workflow_id=$WORKFLOW_ID&status=success" | jq -r '.workflow_runs[0].id') \ diff --git a/Dockerfile_simple b/Dockerfile_simple index 1393d2b..af9793c 100644 --- a/Dockerfile_simple +++ b/Dockerfile_simple @@ -59,6 +59,7 @@ COPY content/ /app/content COPY docs/ /app/docs COPY example-data/ /app/example-data COPY gdpr_consent/ /app/gdpr_consent +COPY hooks/ /app/hooks COPY src/ /app/src COPY app.py /app/app.py COPY settings.json /app/settings.json @@ -79,6 +80,12 @@ RUN echo "mamba run --no-capture-output -n streamlit-env streamlit run app.py" > # make the script executable RUN chmod +x /app/entrypoint.sh +# Patch Analytics +RUN mamba run -n streamlit-env python hooks/hook-analytics.py + +# Set Online Deployment +RUN jq '.online_deployment = true' settings.json > tmp.json && mv tmp.json settings.json + # Download latest OpenMS App executable for Windows from Github actions workflow. RUN WORKFLOW_ID=$(curl -s "https://api.github.com/repos/$GITHUB_USER/$GITHUB_REPO/actions/workflows" | jq -r '.workflows[] | select(.name == "Build executable for Windows") | .id') \ && SUCCESSFUL_RUNS=$(curl -s "https://api.github.com/repos/$GITHUB_USER/$GITHUB_REPO/actions/runs?workflow_id=$WORKFLOW_ID&status=success" | jq -r '.workflow_runs[0].id') \ diff --git a/gdpr_consent/dist/bundle.js b/gdpr_consent/dist/bundle.js index ceb4b7d..2d2d814 100644 --- a/gdpr_consent/dist/bundle.js +++ b/gdpr_consent/dist/bundle.js @@ -235,7 +235,7 @@ eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpac /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; -eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var streamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! streamlit-component-lib */ \"./node_modules/streamlit-component-lib/dist/index.js\");\nvar __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n};\nvar __generator = (undefined && undefined.__generator) || function (thisArg, body) {\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\n function verb(n) { return function (v) { return step([n, v]); }; }\n function step(op) {\n if (f) throw new TypeError(\"Generator is already executing.\");\n while (g && (g = 0, op[0] && (_ = 0)), _) try {\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\n if (y = 0, t) op = [op[0] & 2, t.value];\n switch (op[0]) {\n case 0: case 1: t = op; break;\n case 4: _.label++; return { value: op[1], done: false };\n case 5: _.label++; y = op[1]; op = [0]; continue;\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\n default:\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\n if (t[2]) _.ops.pop();\n _.trys.pop(); continue;\n }\n op = body.call(thisArg, _);\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\n }\n};\n\n// Defines the configuration for Klaro\nvar klaroConfig = {\n mustConsent: true,\n acceptAll: true,\n services: [\n {\n // In GTM, you should define a custom event trigger named `klaro-google-analytics-accepted` which should trigger the Google Analytics integration.\n name: 'google-analytics',\n cookies: [\n /^_ga(_.*)?/ // we delete the Google Analytics cookies if the user declines its use\n ],\n purposes: ['analytics'],\n onAccept: onAcceptCallback,\n onDecline: onDeclineCallback,\n },\n ]\n};\n// This will make klaroConfig globally accessible\nwindow.klaroConfig = klaroConfig;\n// Function to safely access the Klaro manager\nfunction getKlaroManager() {\n var _a;\n return ((_a = window.klaro) === null || _a === void 0 ? void 0 : _a.getManager) ? window.klaro.getManager() : null;\n}\n// Waits until Klaro Manager is available\nfunction waitForKlaroManager() {\n return __awaiter(this, arguments, void 0, function (maxWaitTime, interval) {\n var startTime, klaroManager;\n if (maxWaitTime === void 0) { maxWaitTime = 5000; }\n if (interval === void 0) { interval = 100; }\n return __generator(this, function (_a) {\n switch (_a.label) {\n case 0:\n startTime = Date.now();\n _a.label = 1;\n case 1:\n if (!(Date.now() - startTime < maxWaitTime)) return [3 /*break*/, 3];\n klaroManager = getKlaroManager();\n if (klaroManager) {\n return [2 /*return*/, klaroManager];\n }\n return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, interval); })];\n case 2:\n _a.sent();\n return [3 /*break*/, 1];\n case 3: throw new Error(\"Klaro manager did not become available within the allowed time.\");\n }\n });\n });\n}\n// Helper function to handle unknown errors\nfunction handleError(error) {\n if (error instanceof Error) {\n console.error(\"Error:\", error.message);\n }\n else {\n console.error(\"Unknown error:\", error);\n }\n}\n// Tracking was accepted\nfunction onAcceptCallback() {\n return __awaiter(this, void 0, void 0, function () {\n var manager, error_1;\n return __generator(this, function (_a) {\n switch (_a.label) {\n case 0:\n _a.trys.push([0, 2, , 3]);\n return [4 /*yield*/, waitForKlaroManager()];\n case 1:\n manager = _a.sent();\n if (manager.confirmed) {\n streamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__.Streamlit.setComponentValue(true);\n }\n return [3 /*break*/, 3];\n case 2:\n error_1 = _a.sent();\n handleError(error_1);\n return [3 /*break*/, 3];\n case 3: return [2 /*return*/];\n }\n });\n });\n}\n// Tracking was declined\nfunction onDeclineCallback() {\n return __awaiter(this, void 0, void 0, function () {\n var manager, error_2;\n return __generator(this, function (_a) {\n switch (_a.label) {\n case 0:\n _a.trys.push([0, 2, , 3]);\n return [4 /*yield*/, waitForKlaroManager()];\n case 1:\n manager = _a.sent();\n if (manager.confirmed) {\n streamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__.Streamlit.setComponentValue(false);\n }\n return [3 /*break*/, 3];\n case 2:\n error_2 = _a.sent();\n handleError(error_2);\n return [3 /*break*/, 3];\n case 3: return [2 /*return*/];\n }\n });\n });\n}\n// Stores if the component has been rendered before\nvar rendered = false;\nfunction onRender(event) {\n // Klaro does not work if embedded multiple times\n if (rendered) {\n return;\n }\n rendered = true;\n // Create a new script element\n var script = document.createElement('script');\n // Set the necessary attributes\n script.defer = true;\n script.type = 'application/javascript';\n script.src = 'https://cdn.kiprotect.com/klaro/v0.7/klaro.js';\n // Set the klaro config\n script.setAttribute('data-config', 'klaroConfig');\n // Append the script to the head or body\n document.head.appendChild(script);\n}\n// Attach our `onRender` handler to Streamlit's render event.\nstreamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__.Streamlit.events.addEventListener(streamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__.Streamlit.RENDER_EVENT, onRender);\n// Tell Streamlit we're ready to start receiving data. We won't get our\n// first RENDER_EVENT until we call this function.\nstreamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__.Streamlit.setComponentReady();\n// Finally, tell Streamlit to update the initial height.\nstreamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__.Streamlit.setFrameHeight(1000);\n\n\n//# sourceURL=webpack://gdpr_consent/./src/main.ts?"); +eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var streamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! streamlit-component-lib */ \"./node_modules/streamlit-component-lib/dist/index.js\");\nvar __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\n return new (P || (P = Promise))(function (resolve, reject) {\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\n step((generator = generator.apply(thisArg, _arguments || [])).next());\n });\n};\nvar __generator = (undefined && undefined.__generator) || function (thisArg, body) {\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\n function verb(n) { return function (v) { return step([n, v]); }; }\n function step(op) {\n if (f) throw new TypeError(\"Generator is already executing.\");\n while (g && (g = 0, op[0] && (_ = 0)), _) try {\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\n if (y = 0, t) op = [op[0] & 2, t.value];\n switch (op[0]) {\n case 0: case 1: t = op; break;\n case 4: _.label++; return { value: op[1], done: false };\n case 5: _.label++; y = op[1]; op = [0]; continue;\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\n default:\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\n if (t[2]) _.ops.pop();\n _.trys.pop(); continue;\n }\n op = body.call(thisArg, _);\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\n }\n};\n\n// Defines the configuration for Klaro\nvar klaroConfig = {\n mustConsent: true,\n acceptAll: true,\n services: []\n};\n// This will make klaroConfig globally accessible\nwindow.klaroConfig = klaroConfig;\n// Function to safely access the Klaro manager\nfunction getKlaroManager() {\n var _a;\n return ((_a = window.klaro) === null || _a === void 0 ? void 0 : _a.getManager) ? window.klaro.getManager() : null;\n}\n// Waits until Klaro Manager is available\nfunction waitForKlaroManager() {\n return __awaiter(this, arguments, void 0, function (maxWaitTime, interval) {\n var startTime, klaroManager;\n if (maxWaitTime === void 0) { maxWaitTime = 5000; }\n if (interval === void 0) { interval = 100; }\n return __generator(this, function (_a) {\n switch (_a.label) {\n case 0:\n startTime = Date.now();\n _a.label = 1;\n case 1:\n if (!(Date.now() - startTime < maxWaitTime)) return [3 /*break*/, 3];\n klaroManager = getKlaroManager();\n if (klaroManager) {\n return [2 /*return*/, klaroManager];\n }\n return [4 /*yield*/, new Promise(function (resolve) { return setTimeout(resolve, interval); })];\n case 2:\n _a.sent();\n return [3 /*break*/, 1];\n case 3: throw new Error(\"Klaro manager did not become available within the allowed time.\");\n }\n });\n });\n}\n// Helper function to handle unknown errors\nfunction handleError(error) {\n if (error instanceof Error) {\n console.error(\"Error:\", error.message);\n }\n else {\n console.error(\"Unknown error:\", error);\n }\n}\n// Tracking was accepted\nfunction callback() {\n return __awaiter(this, void 0, void 0, function () {\n var manager, return_vals, _i, _a, service, error_1;\n return __generator(this, function (_b) {\n switch (_b.label) {\n case 0:\n _b.trys.push([0, 2, , 3]);\n return [4 /*yield*/, waitForKlaroManager()];\n case 1:\n manager = _b.sent();\n if (manager.confirmed) {\n return_vals = {};\n for (_i = 0, _a = klaroConfig.services; _i < _a.length; _i++) {\n service = _a[_i];\n return_vals[service.name] = manager.getConsent(service.name);\n }\n streamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__.Streamlit.setComponentValue(return_vals);\n }\n return [3 /*break*/, 3];\n case 2:\n error_1 = _b.sent();\n handleError(error_1);\n return [3 /*break*/, 3];\n case 3: return [2 /*return*/];\n }\n });\n });\n}\n// Stores if the component has been rendered before\nvar rendered = false;\nfunction onRender(event) {\n // Klaro does not work if embedded multiple times\n if (rendered) {\n return;\n }\n rendered = true;\n var data = event.detail;\n if (data.args['google_analytics']) {\n klaroConfig.services.push({\n name: 'google-analytics',\n cookies: [\n /^_ga(_.*)?/ // we delete the Google Analytics cookies if the user declines its use\n ],\n purposes: ['analytics'],\n onAccept: callback,\n onDecline: callback,\n });\n }\n if (data.args['piwik_pro']) {\n klaroConfig.services.push({\n name: 'piwik-pro',\n purposes: ['analytics'],\n onAccept: callback,\n onDecline: callback,\n });\n }\n // Create a new script element\n var script = document.createElement('script');\n // Set the necessary attributes\n script.defer = true;\n script.type = 'application/javascript';\n script.src = 'https://cdn.kiprotect.com/klaro/v0.7/klaro.js';\n // Set the klaro config\n script.setAttribute('data-config', 'klaroConfig');\n // Append the script to the head or body\n document.head.appendChild(script);\n}\n// Attach our `onRender` handler to Streamlit's render event.\nstreamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__.Streamlit.events.addEventListener(streamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__.Streamlit.RENDER_EVENT, onRender);\n// Tell Streamlit we're ready to start receiving data. We won't get our\n// first RENDER_EVENT until we call this function.\nstreamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__.Streamlit.setComponentReady();\n// Finally, tell Streamlit to update the initial height.\nstreamlit_component_lib__WEBPACK_IMPORTED_MODULE_0__.Streamlit.setFrameHeight(1000);\n\n\n//# sourceURL=webpack://gdpr_consent/./src/main.ts?"); /***/ }), diff --git a/gdpr_consent/src/main.ts b/gdpr_consent/src/main.ts index bbb5ef5..f7219ff 100644 --- a/gdpr_consent/src/main.ts +++ b/gdpr_consent/src/main.ts @@ -1,21 +1,23 @@ import { Streamlit, RenderData } from "streamlit-component-lib" +// Define service +type Service = { + name: string; + purposes: string[]; + onAccept: () => Promise; + onDecline: () => Promise; + cookies?: (string | RegExp)[]; +}; + // Defines the configuration for Klaro -const klaroConfig = { +let klaroConfig: { + mustConsent: boolean; + acceptAll: boolean; + services: Service[]; +} = { mustConsent: true, acceptAll: true, - services: [ - { - // In GTM, you should define a custom event trigger named `klaro-google-analytics-accepted` which should trigger the Google Analytics integration. - name: 'google-analytics', - cookies: [ - /^_ga(_.*)?/ // we delete the Google Analytics cookies if the user declines its use - ], - purposes: ['analytics'], - onAccept: onAcceptCallback, - onDecline: onDeclineCallback, - }, - ] + services: [] }; // This will make klaroConfig globally accessible @@ -62,23 +64,15 @@ function handleError(error: unknown): void { } // Tracking was accepted -async function onAcceptCallback(): Promise { +async function callback(): Promise { try { const manager = await waitForKlaroManager() if (manager.confirmed) { - Streamlit.setComponentValue(true) - } - } catch (error) { - handleError(error) - } -} - -// Tracking was declined -async function onDeclineCallback(): Promise { - try { - const manager = await waitForKlaroManager() - if (manager.confirmed) { - Streamlit.setComponentValue(false) + let return_vals : Record = {} + for (const service of klaroConfig.services) { + return_vals[service.name] = manager.getConsent(service.name) + } + Streamlit.setComponentValue(return_vals) } } catch (error) { handleError(error) @@ -95,6 +89,32 @@ function onRender(event: Event): void { } rendered = true + const data = (event as CustomEvent).detail + + if (data.args['google_analytics']) { + klaroConfig.services.push( + { + name: 'google-analytics', + cookies: [ + /^_ga(_.*)?/ // we delete the Google Analytics cookies if the user declines its use + ], + purposes: ['analytics'], + onAccept: callback, + onDecline: callback, + } + ) + } + if (data.args['piwik_pro']) { + klaroConfig.services.push( + { + name: 'piwik-pro', + purposes: ['analytics'], + onAccept: callback, + onDecline: callback, + } + ) + } + // Create a new script element var script = document.createElement('script') @@ -108,6 +128,7 @@ function onRender(event: Event): void { // Append the script to the head or body document.head.appendChild(script) + } // Attach our `onRender` handler to Streamlit's render event. diff --git a/hooks/hook-analytics.py b/hooks/hook-analytics.py index c8758b0..6b8b2da 100644 --- a/hooks/hook-analytics.py +++ b/hooks/hook-analytics.py @@ -41,20 +41,44 @@ def google_analytics_body(gtm_tag): """ +def piwik_pro_body(piwik_tag): + return f""" + + """ + if __name__ == '__main__': - index_path = os.path.join(os.path.dirname(st.__file__), 'static', 'index.html') - settings_path = os.path.join(os.path.dirname(__file__), '..', 'settings.json') + # Load configuration + settings_path = os.path.join(os.path.dirname(__file__), '..', 'settings.json') with open(settings_path, 'r') as f: settings = json.load(f) - gtm_tag = settings['google_analytics']['tag'] + # Load index.html + index_path = os.path.join(os.path.dirname(st.__file__), 'static', 'index.html') with open(index_path, 'r') as f: index = f.read() - index = patch_head(index, google_analytics_head(gtm_tag)) - index = patch_body(index, google_analytics_body(gtm_tag)) + # Configure google analytics + if settings['analytics']['google-analytics']['enabled']: + gtm_tag = settings['analytics']['google-analytics']['tag'] + index = patch_head(index, google_analytics_head(gtm_tag)) + index = patch_body(index, google_analytics_body(gtm_tag)) + + # Configure piwik pro + if settings['analytics']['piwik-pro']['enabled']: + piwik_tag = settings['analytics']['piwik-pro']['tag'] + index = patch_body(index, piwik_pro_body(piwik_tag)) + # Save index.html with open(index_path, 'w') as f: f.write(index) \ No newline at end of file diff --git a/settings.json b/settings.json index 2e2714e..ed1cc82 100644 --- a/settings.json +++ b/settings.json @@ -1,7 +1,13 @@ { - "google_analytics" : { - "enabled" : true, - "tag" : "GTM-NRXGDF5H" + "analytics": { + "google-analytics": { + "enabled": false, + "tag": "" + }, + "piwik-pro": { + "enabled": true, + "tag": "57690c44-d635-43b0-ab43-f8bd3064ca06" + } }, - "online_deployment": true -} + "online_deployment": false +} \ No newline at end of file diff --git a/src/common/captcha_.py b/src/common/captcha_.py index 62e5226..5788d08 100644 --- a/src/common/captcha_.py +++ b/src/common/captcha_.py @@ -200,10 +200,14 @@ def captcha_control(): if "controllo" not in st.session_state or st.session_state["controllo"] == False: # Check if consent for tracking was given - if (st.session_state.settings['google_analytics']['enabled']) and (st.session_state.tracking_consent is None): + ga = st.session_state.settings['analytics']['google-analytics']['enabled'] + pp = st.session_state.settings['analytics']['piwik-pro']['enabled'] + if (ga or pp) and (st.session_state.tracking_consent is None): with st.spinner(): # Ask for consent - st.session_state.tracking_consent = consent_component() + st.session_state.tracking_consent = consent_component( + google_analytics=ga, piwik_pro=pp + ) if st.session_state.tracking_consent is None: # No response by user yet st.stop() diff --git a/src/common/common.py b/src/common/common.py index aeed7ba..b3e3d24 100644 --- a/src/common/common.py +++ b/src/common/common.py @@ -134,26 +134,54 @@ def page_setup(page: str = "") -> dict[str, Any]: st.logo("assets/pyopenms_transparent_background.png") # Create google analytics if consent was given - if "tracking_consent" not in st.session_state: - st.session_state.tracking_consent = None - if (st.session_state.settings["google_analytics"]["enabled"]) and ( - st.session_state.tracking_consent == True + if ( + ("tracking_consent" not in st.session_state) + or (st.session_state.tracking_consent is None) + or (not st.session_state.settings['online_deployment']) ): - html( - """ - - - - - - """, - width=1, - height=1, - ) + st.session_state.tracking_consent = None + else: + if (st.session_state.settings["analytics"]["google-analytics"]["enabled"]) and ( + st.session_state.tracking_consent["google-analytics"] == True + ): + html( + """ + + + + + + """, + width=1, + height=1, + ) + if (st.session_state.settings["analytics"]["piwik-pro"]["enabled"]) and ( + st.session_state.tracking_consent["piwik-pro"] == True + ): + html( + """ + + + + + + """, + width=1, + height=1, + ) # Determine the workspace for the current session if ("workspace" not in st.session_state) or (