diff --git a/src/entry.ts b/src/entry.ts index 95ef31e..e884a64 100644 --- a/src/entry.ts +++ b/src/entry.ts @@ -4,26 +4,70 @@ declare global { const stlite: any; } +interface AppState { + fullscreen: boolean, + theme: 'light' | 'dark' | 'system', + code: string, + config: { + canFixError: boolean + }, + ancestorOrigin: string, +} + // Page is loaded directly if (!import.meta.env.VITE_DEBUG && window === window.parent) window.location.href = 'https://iambee.ai/'; const ALLOWED_ORIGINS = (import.meta.env.VITE_ALLOWED_FRAME_ANCESTORS ?? '').split(' ').filter(Boolean); (() => { - let state = { code: 'async def main():\n pass', config: {} }; - let currentTheme = 'light'; + let state: AppState = { + code: import.meta.env.VITE_DEBUG ? 'import streamlit as st\nasync def main():\n st.write("APP LOADED!")': 'async def main():\n pass', + config: { + canFixError: false + }, + theme: 'light', + fullscreen: false, + ancestorOrigin: ALLOWED_ORIGINS[0], + }; + let app: any; - function mountApp(theme: 'light' | 'dark' = 'light') { + async function updateState(stateChange: Partial) { + const oldState = { ...state }; + + // validate ancestorOrigin + if(!ALLOWED_ORIGINS.includes(stateChange.ancestorOrigin)) stateChange.ancestorOrigin = undefined; + + // update state + state = { ...state, ...stateChange }; + + // set fullscreen + document.body.classList.toggle('fullscreen', state.fullscreen); + + // change theme + if(oldState.theme !== state.theme) { + document.body.classList.toggle('cds--g90', state.theme === 'dark'); + document.body.classList.toggle('cds--white', state.theme === 'light'); + app?.unmount(); + mountApp(); + } + + // update code & config + if (oldState.code !== state.code || JSON.stringify(oldState.config) !== JSON.stringify(state.config)) { + await app.writeFile('app.py', state.code); + await app.writeFile('config.json', JSON.stringify(state.config)); + await app.writeFile('trigger.py', 'import run; await run.run(); # ' + Math.random()); + } + } + + function mountApp() { app = stlite.mount( { requirements: ['pydantic'], entrypoint: 'trigger.py', files: { 'trigger.py': 'import run; await run.run()', - 'app.py': import.meta.env.VITE_DEBUG - ? 'import streamlit as st\nasync def main():\n st.write("APP LOADED!")' - : state.code, + 'app.py': state.code, 'config.json': JSON.stringify(state.config), 'run.py': runPy, }, @@ -31,7 +75,7 @@ const ALLOWED_ORIGINS = (import.meta.env.VITE_ALLOWED_FRAME_ANCESTORS ?? '').spl 'client.toolbarMode': 'minimal', 'server.runOnSave': true, 'theme.primaryColor': '#0f62fe', - ...(theme === 'light' ? { + ...(state.theme === 'light' ? { 'theme.base': 'light', 'theme.backgroundColor': '#ffffff', 'theme.secondaryBackgroundColor': '#ffffff', @@ -48,14 +92,14 @@ const ALLOWED_ORIGINS = (import.meta.env.VITE_ALLOWED_FRAME_ANCESTORS ?? '').spl app.kernel._worker.addEventListener('message', (event: MessageEvent) => { const data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data; switch (data.type) { - case 'bee:reportError': - ALLOWED_ORIGINS.forEach((origin: string) => - parent.postMessage({ type: data.type, errorText: data?.errorText }, origin), - ); + case 'bee:ready': + parent.postMessage({ type: 'bee:ready' }, state.ancestorOrigin); return; + case 'bee:request': - ALLOWED_ORIGINS.forEach((origin: string) => parent.postMessage(data, origin)); + parent.postMessage(data, state.ancestorOrigin); return; + default: return; } @@ -75,35 +119,15 @@ const ALLOWED_ORIGINS = (import.meta.env.VITE_ALLOWED_FRAME_ANCESTORS ?? '').spl return; } - const { classList } = document.body; - switch (data.type) { - case 'bee:setFullscreen': - classList.toggle('fullscreen', data.value); - + case 'bee:response': + app.kernel._worker.postMessage(data); return; - case 'bee:updateTheme': - classList.remove(data.theme === 'light' ? 'cds--g90' : 'cds--white'); - classList.add(data.theme === 'light' ? 'cds--white' : 'cds--g90'); - if(data.theme !== currentTheme) { - currentTheme = data.theme; - app?.unmount(); - mountApp(data.theme); - } + case 'bee:updateState': + updateState(data.stateChange); return; - case 'bee:updateCode': - const newState = { code: data.code, config: data.config } - if (JSON.stringify(state) === JSON.stringify(newState)) return; - await app.writeFile('app.py', data.code); - await app.writeFile('config.json', JSON.stringify(data.config ?? {})); - await app.writeFile('trigger.py', 'import run; await run.run(); # ' + Math.random()); - state = newState; - return; - case 'bee:response': - app.kernel._worker.postMessage(data); - return; default: return; } diff --git a/src/python/run.py b/src/python/run.py index b36f379..fafaa0a 100644 --- a/src/python/run.py +++ b/src/python/run.py @@ -158,9 +158,9 @@ def error_fragment(error_text): st.write("🛠️ The error is being fixed...") else: st.write("🤯 An error occurred while executing the app.") - if CONFIG.get("can_fix_error"): + if CONFIG.get("canFixError"): if st.button("Try to fix this error", icon="🛠️", type="primary"): - js.postMessage(json.dumps({"type": "bee:reportError", "errorText": error_text})) + request("fix_error", { "errorText": error_text }) st.session_state._bee_fixing_error = True st.rerun(scope="fragment") st.expander("Error details").code(error_text, language=None) @@ -232,6 +232,7 @@ def fix_markdown(body): async def run(): + js.postMessage(json.dumps({ "type": "bee:ready" })) try: code = pathlib.Path("app.py").read_text() if st.session_state.get("_bee_last_code") != code: