Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(apps): protocol #37

Merged
merged 6 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 60 additions & 36 deletions src/entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,78 @@ 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<AppState>) {
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,
},
streamlitConfig: {
'client.toolbarMode': 'minimal',
'server.runOnSave': true,
'theme.primaryColor': '#0f62fe',
...(theme === 'light' ? {
...(state.theme === 'light' ? {
'theme.base': 'light',
'theme.backgroundColor': '#ffffff',
'theme.secondaryBackgroundColor': '#ffffff',
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand Down
5 changes: 3 additions & 2 deletions src/python/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
Loading