| from enum import Enum | |
| from pathlib import Path | |
| class DemoType(Enum): | |
| GRADIO = 1 | |
| STREAMLIT = 2 | |
| gradio_lite_html_template = Path('templates/gradio-lite/gradio-lite-template.html').read_text() | |
| stlite_html_template = Path('templates/stlite/stlite-template.html').read_text() | |
| gradio_lite_snippet_template = Path('templates/gradio-lite/gradio-lite-snippet-template.html').read_text() | |
| stlite_snippet_template = Path('templates/stlite/stlite-snippet-template.html').read_text() | |
| def starting_app_code(demo_type: DemoType) -> str: | |
| if demo_type == DemoType.GRADIO: | |
| return Path('templates/gradio-lite/gradio_lite_starting_code.py').read_text().replace('`', r'\`') | |
| elif demo_type == DemoType.STREAMLIT: | |
| return Path('templates/stlite/stlite_starting_code.py').read_text().replace('`', r'\`') | |
| raise NotImplementedError(f'{demo_type} is not a supported demo type') | |
| def load_js(demo_type: DemoType) -> str: | |
| if demo_type == DemoType.GRADIO: | |
| return f"""() => {{ | |
| if (window.gradioLiteLoaded) {{ | |
| return | |
| }} | |
| // Get the query string from the URL | |
| const queryString = window.location.search; | |
| // Use a function to parse the query string into an object | |
| function parseQueryString(queryString) {{ | |
| const params = {{}}; | |
| const queryStringWithoutQuestionMark = queryString.substring(1); // Remove the leading question mark | |
| const keyValuePairs = queryStringWithoutQuestionMark.split('&'); | |
| keyValuePairs.forEach(keyValue => {{ | |
| const [key, value] = keyValue.split('='); | |
| if (value) {{ | |
| params[key] = decodeURIComponent(value.replace(/\+/g, ' ')); | |
| }} | |
| }}); | |
| return params; | |
| }} | |
| // Parse the query string into an object | |
| const queryParams = parseQueryString(queryString); | |
| // Access individual parameters | |
| const typeValue = queryParams.type; | |
| let codeValue = null; | |
| let requirementsValue = null; | |
| if (typeValue === 'gradio') {{ | |
| codeValue = queryParams.code; | |
| requirementsValue = queryParams.requirements; | |
| }} | |
| const htmlString = '<iframe id="gradio-iframe" width="100%" height="512px" src="about:blank"></iframe>'; | |
| const parser = new DOMParser(); | |
| const doc = parser.parseFromString(htmlString, 'text/html'); | |
| const iframe = doc.getElementById('gradio-iframe'); | |
| const div = document.getElementById('gradioDemoDiv'); | |
| div.appendChild(iframe); | |
| let template = `{gradio_lite_html_template.replace('STARTING_CODE', starting_app_code(demo_type))}`; | |
| if (codeValue) {{ | |
| template = `{gradio_lite_html_template}`.replace('STARTING_CODE', codeValue.replaceAll(String.fromCharCode(92), String.fromCharCode(92) + String.fromCharCode(92)).replaceAll('`', String.fromCharCode(92) + '`')); | |
| }} | |
| template = template.replace('STARTING_REQUIREMENTS', requirementsValue || ''); | |
| const frame = document.getElementById('gradio-iframe'); | |
| frame.contentWindow.document.open('text/html', 'replace'); | |
| frame.contentWindow.document.write(template); | |
| frame.contentWindow.document.close(); | |
| window.gradioLiteLoaded = true; | |
| }}""" | |
| elif demo_type == DemoType.STREAMLIT: | |
| return f"""() => {{ | |
| if (window.stliteLoaded) {{ | |
| return | |
| }} | |
| // Get the query string from the URL | |
| const queryString = window.location.search; | |
| // Use a function to parse the query string into an object | |
| function parseQueryString(queryString) {{ | |
| const params = {{}}; | |
| const queryStringWithoutQuestionMark = queryString.substring(1); // Remove the leading question mark | |
| const keyValuePairs = queryStringWithoutQuestionMark.split('&'); | |
| keyValuePairs.forEach(keyValue => {{ | |
| const [key, value] = keyValue.split('='); | |
| if (value) {{ | |
| params[key] = decodeURIComponent(value.replace(/\+/g, ' ')); | |
| }} | |
| }}); | |
| return params; | |
| }} | |
| // Parse the query string into an object | |
| const queryParams = parseQueryString(queryString); | |
| // Access individual parameters | |
| const typeValue = queryParams.type; | |
| let codeValue = null; | |
| let requirementsValue = null; | |
| if (typeValue === 'streamlit') {{ | |
| codeValue = queryParams.code; | |
| requirementsValue = queryParams.requirements; | |
| }} | |
| const htmlString = '<iframe id="stlite-iframe" width="100%" height="512px" src="about:blank"></iframe>'; | |
| const parser = new DOMParser(); | |
| const doc = parser.parseFromString(htmlString, 'text/html'); | |
| const iframe = doc.getElementById('stlite-iframe'); | |
| const div = document.getElementById('stliteDemoDiv'); | |
| div.appendChild(iframe); | |
| let template = `{stlite_html_template.replace('STARTING_CODE', starting_app_code(demo_type))}`; | |
| if (codeValue) {{ | |
| template = `{stlite_html_template}`.replace('STARTING_CODE', codeValue.replaceAll(String.fromCharCode(92), String.fromCharCode(92) + String.fromCharCode(92)).replaceAll('`', String.fromCharCode(92) + '`')); | |
| }} | |
| const formattedRequirements = (requirementsValue || '').split('\\n').filter(x => x && !x.startsWith('#')).map(x => x.trim()); | |
| template = template.replace('STARTING_REQUIREMENTS', formattedRequirements.map(x => `"${{x}}"`).join(', ') || ''); | |
| const frame = document.getElementById('stlite-iframe'); | |
| frame.contentWindow.document.open(); | |
| frame.contentWindow.document.write(template); | |
| frame.contentWindow.document.close(); | |
| window.stliteLoaded = true; | |
| }}""" | |
| raise NotImplementedError(f'{demo_type} is not a supported demo type') | |
| def update_iframe_js(demo_type: DemoType) -> str: | |
| if demo_type == DemoType.GRADIO: | |
| return f"""async (code, requirements, lastError, codeHistory, codeHistoryIndex) => {{ | |
| const formattedRequirements = requirements.split('\\n').filter(x => x && !x.startsWith('#')).map(x => x.trim()); | |
| let errorResult = null; | |
| const attemptedRequirements = new Set(); | |
| const installedRequirements = []; | |
| async function update() {{ | |
| // Remove existing stylesheet so it will be reloaded; | |
| // see https://github.com/gradio-app/gradio/blob/200237d73c169f39514465efc163db756969d3ac/js/app/src/lite/css.ts#L41 | |
| const demoFrameWindow = document.getElementById('gradio-iframe').contentWindow; | |
| const oldStyle = demoFrameWindow.document.querySelector("head style"); | |
| oldStyle.remove(); | |
| const appController = demoFrameWindow.window.appController; | |
| const newCode = code + ` # Update tag ${{Math.random()}}`; | |
| try {{ | |
| await appController.install(formattedRequirements); | |
| await appController.run_code(newCode); | |
| }} | |
| catch (e) {{ | |
| // Replace old style if code error prevented new style from loading. | |
| const newStyle = demoFrameWindow.document.querySelector("head style"); | |
| if (!newStyle) {{ | |
| demoFrameWindow.document.head.appendChild(oldStyle); | |
| }} | |
| // If the error is caused by a missing module try once to install it and update again. | |
| if (e.toString().includes('ModuleNotFoundError')) {{ | |
| try {{ | |
| const guessedModuleName = e.toString().split("'")[1].replaceAll('_', '-'); | |
| if (attemptedRequirements.has(guessedModuleName)) {{ | |
| throw Error(`Could not install pyodide module ${{guessedModuleName}}`); | |
| }} | |
| console.log(`Attempting to install missing pyodide module "${{guessedModuleName}}"`); | |
| attemptedRequirements.add(guessedModuleName); | |
| await appController.install([guessedModuleName]); | |
| installedRequirements.push(guessedModuleName); | |
| return await update(); | |
| }} | |
| catch (err) {{ | |
| console.log(err); | |
| }} | |
| }} | |
| // Hide app so the error traceback is visible. | |
| // First div in main is the error traceback, second is the app. | |
| const appBody = demoFrameWindow.document.querySelectorAll("div.main > div")[1]; | |
| appBody.style.visibility = "hidden"; | |
| errorResult = e.toString(); | |
| const allRequirements = formattedRequirements.concat(installedRequirements); | |
| return [code, allRequirements, errorResult, codeHistory, codeHistoryIndex]; | |
| }} | |
| }}; | |
| await update(); | |
| const allRequirements = formattedRequirements.concat(installedRequirements); | |
| // Update URL query params to include the current demo code state | |
| const currentUrl = new URL(window.location.href); | |
| currentUrl.searchParams.set('type', 'gradio'); | |
| if (requirements) {{ | |
| currentUrl.searchParams.set('requirements', allRequirements.join('\\n')); | |
| }} | |
| if (code) {{ | |
| currentUrl.searchParams.set('code', code); | |
| }} | |
| // Replace the current URL with the updated one | |
| history.replaceState({{}}, '', currentUrl.href); | |
| return [code, allRequirements, errorResult, codeHistory, codeHistoryIndex]; | |
| }}""" | |
| elif demo_type == DemoType.STREAMLIT: | |
| return f"""async (code, requirements, lastError, codeHistory, codeHistoryIndex) => {{ | |
| const formattedRequirements = (requirements || '').split('\\n').filter(x => x && !x.startsWith('#')).map(x => x.trim()); | |
| let errorResult = null; | |
| const attemptedRequirements = new Set(); | |
| const installedRequirements = []; | |
| async function update() {{ | |
| const appController = document.getElementById('stlite-iframe').contentWindow.window.appController; | |
| try {{ | |
| if (formattedRequirements) {{ | |
| await appController.install(formattedRequirements); | |
| }} | |
| const newCode = code + ` # Update tag ${{Math.random()}}`; | |
| const entrypointFile = "streamlit_app.py"; | |
| // As code rerun happens inside streamlit this won't throw an error for self-healing imports. | |
| await appController.writeFile(entrypointFile, newCode); | |
| // So instead wait 500 milliseconds to see if the streamlit error banner appeared with an error. | |
| // TODO: Consider a way to make this not rely on streamlit refresh timing; otherwise user can just re-update and this will trigger. | |
| await new Promise(r => setTimeout(r, 500)); | |
| const messageDiv = document.getElementById('stlite-iframe').contentWindow.document.querySelector('.message'); | |
| if (messageDiv) {{ | |
| throw Error(messageDiv.innerHTML); | |
| }} | |
| }} | |
| catch (e) {{ | |
| // If the error is caused by a missing module try once to install it and update again. | |
| if (e.toString().includes('ModuleNotFoundError')) {{ | |
| try {{ | |
| const guessedModuleName = e.toString().split("'")[1].replaceAll('_', '-'); | |
| if (attemptedRequirements.has(guessedModuleName)) {{ | |
| throw Error(`Could not install pyodide module ${{guessedModuleName}}`); | |
| }} | |
| console.log(`Attempting to install missing pyodide module "${{guessedModuleName}}"`); | |
| attemptedRequirements.add(guessedModuleName); | |
| await appController.install([guessedModuleName]); | |
| installedRequirements.push(guessedModuleName); | |
| return await update(); | |
| }} | |
| catch (err) {{ | |
| console.log(err); | |
| }} | |
| }} | |
| errorResult = e.toString(); | |
| const allRequirements = formattedRequirements.concat(installedRequirements); | |
| return [code, allRequirements, errorResult, codeHistory, codeHistoryIndex]; | |
| }} | |
| }}; | |
| await update(); | |
| const allRequirements = formattedRequirements.concat(installedRequirements); | |
| // Update URL query params to include the current demo code state | |
| const currentUrl = new URL(window.location.href); | |
| currentUrl.searchParams.set('type', 'streamlit'); | |
| if (requirements) {{ | |
| currentUrl.searchParams.set('requirements', allRequirements.join('\\n')); | |
| }} | |
| if (code) {{ | |
| currentUrl.searchParams.set('code', code); | |
| }} | |
| // Replace the current URL with the updated one | |
| history.replaceState({{}}, '', currentUrl.href); | |
| return [code, allRequirements, errorResult, codeHistory, codeHistoryIndex]; | |
| }}""" | |
| raise NotImplementedError(f'{demo_type} is not a supported demo type') | |
| def copy_share_link_js(demo_type: DemoType) -> str: | |
| if demo_type == DemoType.GRADIO: | |
| return f"""async (code, requirements) => {{ | |
| const url = new URL(window.location.href); | |
| url.searchParams.set('type', 'gradio'); | |
| url.searchParams.set('requirements', requirements); | |
| url.searchParams.set('code', code); | |
| // TODO: Figure out why link doesn't load as expected in Spaces. | |
| const shareLink = url.toString().replace('gstaff-kitewind.hf.space', 'huggingface.co/spaces/gstaff/KiteWind'); | |
| await navigator.clipboard.writeText(shareLink); | |
| return [code, requirements]; | |
| }}""" | |
| if demo_type == DemoType.STREAMLIT: | |
| return f"""async (code, requirements) => {{ | |
| const url = new URL(window.location.href); | |
| url.searchParams.set('type', 'streamlit'); | |
| url.searchParams.set('requirements', requirements); | |
| url.searchParams.set('code', code); | |
| // TODO: Figure out why link doesn't load as expected in Spaces. | |
| const shareLink = url.toString().replace('gstaff-kitewind.hf.space', 'huggingface.co/spaces/gstaff/KiteWind'); | |
| await navigator.clipboard.writeText(shareLink); | |
| return [code, requirements]; | |
| }}""" | |
| raise NotImplementedError(f'{demo_type} is not a supported demo type') | |
| def copy_snippet_js(demo_type: DemoType) -> str: | |
| if demo_type == DemoType.GRADIO: | |
| return f"""async (code, requirements) => {{ | |
| const escapedCode = code.replaceAll(String.fromCharCode(92), String.fromCharCode(92) + String.fromCharCode(92) + String.fromCharCode(92) + String.fromCharCode(92)).replaceAll('`', String.fromCharCode(92) + '`'); | |
| const template = `{gradio_lite_snippet_template}`; | |
| // Step 1: Generate the HTML content | |
| const completedTemplate = template.replace('STARTING_CODE', escapedCode).replace('STARTING_REQUIREMENTS', requirements); | |
| const snippet = completedTemplate; | |
| await navigator.clipboard.writeText(snippet); | |
| return [code, requirements]; | |
| }}""" | |
| elif demo_type == DemoType.STREAMLIT: | |
| return f"""async (code, requirements) => {{ | |
| const escapedCode = code.replaceAll(String.fromCharCode(92), String.fromCharCode(92) + String.fromCharCode(92) + String.fromCharCode(92)).replaceAll('`', String.fromCharCode(92) + '`'); | |
| const template = `{stlite_snippet_template}`; | |
| // Step 1: Generate the HTML content | |
| const formattedRequirements = (requirements || '').split('\\n').filter(x => x && !x.startsWith('#')).map(x => x.trim()); | |
| const completedTemplate = template.replace('STARTING_CODE', code).replace('STARTING_REQUIREMENTS', formattedRequirements.map(x => `"${{x}}"`).join(', ') || ''); | |
| const snippet = completedTemplate; | |
| await navigator.clipboard.writeText(snippet); | |
| return [code, requirements]; | |
| }}""" | |
| raise NotImplementedError(f'{demo_type} is not a supported demo type') | |
| def download_code_js(demo_type: DemoType) -> str: | |
| if demo_type == demo_type.GRADIO: | |
| return f"""(code, requirements) => {{ | |
| const escapedCode = code.replaceAll(String.fromCharCode(92), String.fromCharCode(92) + String.fromCharCode(92)).replaceAll('`', String.fromCharCode(92) + '`'); | |
| // Step 1: Generate the HTML content | |
| const completedTemplate = `{gradio_lite_html_template}`.replace('STARTING_CODE', escapedCode).replace('STARTING_REQUIREMENTS', requirements); | |
| // Step 2: Create a Blob from the HTML content | |
| const blob = new Blob([completedTemplate], {{ type: "text/html" }}); | |
| // Step 3: Create a URL for the Blob | |
| const url = URL.createObjectURL(blob); | |
| // Step 4: Create a download link | |
| const downloadLink = document.createElement("a"); | |
| downloadLink.href = url; | |
| downloadLink.download = "gradio-lite-app.html"; // Specify the filename for the download | |
| // Step 5: Trigger a click event on the download link | |
| downloadLink.click(); | |
| // Clean up by revoking the URL | |
| URL.revokeObjectURL(url); | |
| }}""" | |
| elif demo_type == demo_type.STREAMLIT: | |
| return f"""(code, requirements) => {{ | |
| const escapedCode = code.replaceAll(String.fromCharCode(92), String.fromCharCode(92) + String.fromCharCode(92)).replaceAll('`', String.fromCharCode(92) + '`'); | |
| // Step 1: Generate the HTML content | |
| const formattedRequirements = (requirements || '').split('\\n').filter(x => x && !x.startsWith('#')).map(x => x.trim()); | |
| const completedTemplate = `{stlite_html_template}`.replace('STARTING_CODE', escapedCode).replace('STARTING_REQUIREMENTS', formattedRequirements.map(x => `"${{x}}"`).join(', ') || ''); | |
| // Step 2: Create a Blob from the HTML content | |
| const blob = new Blob([completedTemplate], {{ type: "text/html" }}); | |
| // Step 3: Create a URL for the Blob | |
| const url = URL.createObjectURL(blob); | |
| // Step 4: Create a download link | |
| const downloadLink = document.createElement("a"); | |
| downloadLink.href = url; | |
| downloadLink.download = "stlite-app.html"; // Specify the filename for the download | |
| // Step 5: Trigger a click event on the download link | |
| downloadLink.click(); | |
| // Clean up by revoking the URL | |
| URL.revokeObjectURL(url); | |
| }}""" | |
| raise NotImplementedError(f'{demo_type} is not a supported demo type') | |