gstaff commited on
Commit
3b6c4d9
β€’
1 Parent(s): c1211e8

Add requirements support, query param, copy link, and automatic tab selection for stlite.

Browse files
app.py CHANGED
@@ -1,6 +1,7 @@
1
  import logging
2
  import os
3
  import re
 
4
  import warnings
5
  from pathlib import Path
6
 
@@ -98,9 +99,14 @@ def add_hotkeys() -> str:
98
  return Path("hotkeys.js").read_text()
99
 
100
 
101
- def apply_query_params(code: str, request: gr.Request) -> (str, str):
102
  params = dict(request.query_params)
103
- return params.get('code') or code, params.get('requirements') or ''
 
 
 
 
 
104
 
105
 
106
  def update_state(requirements: [str], error: str):
@@ -112,101 +118,104 @@ with gr.Blocks(title="KiteWind") as demo:
112
  gr.Markdown(
113
  '<h4 align="center">Chat-assisted web app creator by <a href="https://huggingface.co/gstaff">@gstaff</a></h4>')
114
  selectedTab = gr.State(value='gradio-lite')
115
- with gr.Tab('Gradio (gradio-lite)') as gradio_lite_tab:
116
- with gr.Row():
117
- with gr.Column():
118
- gr.Markdown("## 1. Run your app in the browser!")
119
- gr.HTML(value='<div id="gradioDemoDiv"></div>')
120
- gr.Markdown("## 2. Customize using voice requests!")
121
- with gr.Row():
122
- with gr.Column():
123
- with gr.Group():
124
- in_audio = gr.Audio(label="Record a voice request (click or press ctrl + ` to start/stop)",
125
- source='microphone', type='filepath', elem_classes=["record-btn"])
126
- in_prompt = gr.Textbox(label="Or type a text request and press Enter",
127
- placeholder="Need an idea? Try one of these:\n- Add a button to reverse the name\n- Change the greeting to Spanish\n- Put the reversed name output into a separate textbox")
128
- out_text = gr.TextArea(label="πŸ€– Chat Assistant Response")
129
- clear = gr.ClearButton([in_prompt, in_audio, out_text])
130
- with gr.Column():
131
- gradio_code_area = gr.Code(
132
- label="App Code - You can also edit directly and then click Update App or ctrl + space",
133
- language='python', value=starting_app_code(DemoType.GRADIO))
134
- gradio_requirements_area = gr.Code(label="App Requirements (additional modules pip installed for pyodide)")
135
- update_btn = gr.Button("Update App (Ctrl + Space)", variant="primary", elem_classes=["update-btn"])
136
- last_error = gr.State()
137
- code_update_params = {'fn': update_state, 'inputs': [gradio_code_area, gradio_requirements_area],
138
- 'outputs': [gradio_requirements_area, last_error],
139
- '_js': update_iframe_js(DemoType.GRADIO)}
140
- gen_text_params = {'fn': generate_text, 'inputs': [gradio_code_area, in_prompt],
141
- 'outputs': [out_text, gradio_code_area]}
142
- transcribe_params = {'fn': transcribe, 'inputs': [in_audio], 'outputs': [in_prompt, in_audio]}
143
- update_btn.click(**code_update_params)
144
- in_prompt.submit(**gen_text_params).then(**code_update_params)
145
- in_audio.stop_recording(**transcribe_params).then(**gen_text_params).then(**code_update_params)
146
- with gr.Row():
147
- with gr.Column():
148
- gr.Markdown("## 3. Export your app to share!")
149
- share_link_btn = gr.Button("πŸ”— Copy share link to clipboard")
150
- share_link_btn.click(link_copy_notify, [gradio_code_area, gradio_requirements_area], None, _js=copy_share_link_js(DemoType.GRADIO))
151
- copy_snippet_btn = gr.Button("βœ‚οΈ Copy app snippet to paste into another page")
152
- copy_snippet_btn.click(copy_notify, [gradio_code_area, gradio_requirements_area], None, _js=copy_snippet_js(DemoType.GRADIO))
153
- download_btn = gr.Button("πŸ—Ž Download app as a standalone file")
154
- download_btn.click(None, [gradio_code_area, gradio_requirements_area], None, _js=download_code_js(DemoType.GRADIO))
155
- with gr.Row():
156
- with gr.Column():
157
- gr.Markdown("## Current limitations")
158
- with gr.Accordion("Click to view", open=False):
159
- gr.Markdown(
160
- "- Only gradio-lite apps using the libraries available in pyodide are supported\n- The chat hasn't been tuned on gradio library data; it may make mistakes")
161
- with gr.Tab('Streamlit (stlite)') as stlite_tab:
162
- with gr.Row():
163
- with gr.Column():
164
- gr.Markdown("## 1. Run your app in the browser!")
165
- gr.HTML(value='<div id="stliteDemoDiv"></div>')
166
- gr.Markdown("## 2. Customize using voice requests!")
167
- with gr.Row():
168
- with gr.Column():
169
- with gr.Group():
170
- in_audio = gr.Audio(label="Record a voice request (click or press ctrl + ` to start/stop)",
171
- source='microphone', type='filepath', elem_classes=["record-btn"])
172
- in_prompt = gr.Textbox(label="Or type a text request and press Enter",
173
- placeholder="Need an idea? Try one of these:\n- Add a button to reverse the name\n- Change the greeting to Spanish\n- Change the theme to soft")
174
- out_text = gr.TextArea(label="πŸ€– Chat Assistant Response")
175
- clear_btn = gr.ClearButton([in_prompt, in_audio, out_text])
176
- with gr.Column():
177
- stlite_code_area = gr.Code(
178
- label="App Code - You can also edit directly and then click Update App or ctrl + space",
179
- language='python', value=starting_app_code(DemoType.STREAMLIT))
180
- requirements_area = gr.Code(label="App Requirements (additional modules pip installed for pyodide)")
181
- update_btn = gr.Button("Update App (Ctrl + Space)", variant="primary", elem_classes=["update-btn"])
182
- code_update_params = {'fn': None, 'inputs': [stlite_code_area, requirements_area], 'outputs': None,
183
- '_js': update_iframe_js(DemoType.STREAMLIT)}
184
- gen_text_params = {'fn': generate_text, 'inputs': [stlite_code_area, in_prompt],
185
- 'outputs': [out_text, stlite_code_area]}
186
- transcribe_params = {'fn': transcribe, 'inputs': [in_audio], 'outputs': [in_prompt, in_audio]}
187
- update_btn.click(**code_update_params)
188
- in_prompt.submit(**gen_text_params).then(**code_update_params)
189
- in_audio.stop_recording(**transcribe_params).then(**gen_text_params).then(**code_update_params)
190
- with gr.Row():
191
- with gr.Column():
192
- gr.Markdown("## 3. Export your app to share!")
193
- copy_snippet_btn = gr.Button("βœ‚οΈ Copy app snippet into paste in another page")
194
- copy_snippet_btn.click(copy_notify, [stlite_code_area, requirements_area], None, _js=copy_snippet_js(DemoType.STREAMLIT))
195
- download_btn = gr.Button("πŸ—Ž Download app as a standalone file")
196
- download_btn.click(None, stlite_code_area, None, _js=download_code_js(DemoType.STREAMLIT))
197
- with gr.Row():
198
- with gr.Column():
199
- gr.Markdown("## Current limitations")
200
- with gr.Accordion("Click to view", open=False):
201
- gr.Markdown(
202
- "- Only Streamlit apps using libraries available in pyodide are supported\n- The chat hasn't been tuned on Streamlit library data; it may make mistakes")
 
 
 
 
 
203
  gradio_lite_tab.select(lambda: "gradio-lite", None, selectedTab).then(None, None, None,
204
  _js=load_js(DemoType.GRADIO))
205
  stlite_tab.select(lambda: "stlite", None, selectedTab).then(None, None, None, _js=load_js(DemoType.STREAMLIT))
206
- demo.load(None, None, None, _js=load_js(DemoType.GRADIO))
207
  demo.load(None, None, None, _js=add_hotkeys())
208
- # TODO: select stlite tab and populate that code based on query param type
209
- demo.load(apply_query_params, gradio_code_area, [gradio_code_area, gradio_requirements_area])
210
  demo.css = "footer {visibility: hidden}"
211
 
212
  if __name__ == "__main__":
 
1
  import logging
2
  import os
3
  import re
4
+ import typing
5
  import warnings
6
  from pathlib import Path
7
 
 
99
  return Path("hotkeys.js").read_text()
100
 
101
 
102
+ def apply_query_params(gradio_code: str, stlite_code: str, request: gr.Request) -> (str, str, str, str, typing.Any):
103
  params = dict(request.query_params)
104
+ demo_type = params.get('type')
105
+ if demo_type == 'gradio':
106
+ return params.get('code') or gradio_code, params.get('requirements') or '', stlite_code, '', gr.Tabs(selected=0)
107
+ if demo_type == 'streamlit':
108
+ return gradio_code, '', params.get('code') or stlite_code, params.get('requirements') or '', gr.Tabs(selected=1)
109
+ return gradio_code, '', stlite_code, '', gr.Tabs(selected=0)
110
 
111
 
112
  def update_state(requirements: [str], error: str):
 
118
  gr.Markdown(
119
  '<h4 align="center">Chat-assisted web app creator by <a href="https://huggingface.co/gstaff">@gstaff</a></h4>')
120
  selectedTab = gr.State(value='gradio-lite')
121
+ with gr.Tabs() as tabs:
122
+ with gr.Tab('Gradio (gradio-lite)', id=0) as gradio_lite_tab:
123
+ with gr.Row():
124
+ with gr.Column():
125
+ gr.Markdown("## 1. Run your app in the browser!")
126
+ gr.HTML(value='<div id="gradioDemoDiv"></div>')
127
+ gr.Markdown("## 2. Customize using voice requests!")
128
+ with gr.Row():
129
+ with gr.Column():
130
+ with gr.Group():
131
+ in_audio = gr.Audio(label="Record a voice request (click or press ctrl + ` to start/stop)",
132
+ source='microphone', type='filepath', elem_classes=["record-btn"])
133
+ in_prompt = gr.Textbox(label="Or type a text request and press Enter",
134
+ placeholder="Need an idea? Try one of these:\n- Add a button to reverse the name\n- Change the greeting to Spanish\n- Put the reversed name output into a separate textbox")
135
+ out_text = gr.TextArea(label="πŸ€– Chat Assistant Response")
136
+ clear = gr.ClearButton([in_prompt, in_audio, out_text])
137
+ with gr.Column():
138
+ gradio_code_area = gr.Code(
139
+ label="App Code - You can also edit directly and then click Update App or ctrl + space",
140
+ language='python', value=starting_app_code(DemoType.GRADIO))
141
+ gradio_requirements_area = gr.Code(label="App Requirements (additional modules pip installed for pyodide)")
142
+ update_btn = gr.Button("Update App (Ctrl + Space)", variant="primary", elem_classes=["update-btn"])
143
+ last_error = gr.State()
144
+ code_update_params = {'fn': update_state, 'inputs': [gradio_code_area, gradio_requirements_area],
145
+ 'outputs': [gradio_requirements_area, last_error],
146
+ '_js': update_iframe_js(DemoType.GRADIO)}
147
+ gen_text_params = {'fn': generate_text, 'inputs': [gradio_code_area, in_prompt],
148
+ 'outputs': [out_text, gradio_code_area]}
149
+ transcribe_params = {'fn': transcribe, 'inputs': [in_audio], 'outputs': [in_prompt, in_audio]}
150
+ update_btn.click(**code_update_params)
151
+ in_prompt.submit(**gen_text_params).then(**code_update_params)
152
+ in_audio.stop_recording(**transcribe_params).then(**gen_text_params).then(**code_update_params)
153
+ with gr.Row():
154
+ with gr.Column():
155
+ gr.Markdown("## 3. Export your app to share!")
156
+ share_link_btn = gr.Button("πŸ”— Copy share link to clipboard")
157
+ share_link_btn.click(link_copy_notify, [gradio_code_area, gradio_requirements_area], None, _js=copy_share_link_js(DemoType.GRADIO))
158
+ copy_snippet_btn = gr.Button("βœ‚οΈ Copy app snippet to paste into another page")
159
+ copy_snippet_btn.click(copy_notify, [gradio_code_area, gradio_requirements_area], None, _js=copy_snippet_js(DemoType.GRADIO))
160
+ download_btn = gr.Button("πŸ—Ž Download app as a standalone file")
161
+ download_btn.click(None, [gradio_code_area, gradio_requirements_area], None, _js=download_code_js(DemoType.GRADIO))
162
+ with gr.Row():
163
+ with gr.Column():
164
+ gr.Markdown("## Current limitations")
165
+ with gr.Accordion("Click to view", open=False):
166
+ gr.Markdown(
167
+ "- Only gradio-lite apps using the libraries available in pyodide are supported\n- The chat hasn't been tuned on gradio library data; it may make mistakes")
168
+ with gr.Tab('Streamlit (stlite)', id=1) as stlite_tab:
169
+ with gr.Row():
170
+ with gr.Column():
171
+ gr.Markdown("## 1. Run your app in the browser!")
172
+ gr.HTML(value='<div id="stliteDemoDiv"></div>')
173
+ gr.Markdown("## 2. Customize using voice requests!")
174
+ with gr.Row():
175
+ with gr.Column():
176
+ with gr.Group():
177
+ in_audio = gr.Audio(label="Record a voice request (click or press ctrl + ` to start/stop)",
178
+ source='microphone', type='filepath', elem_classes=["record-btn"])
179
+ in_prompt = gr.Textbox(label="Or type a text request and press Enter",
180
+ placeholder="Need an idea? Try one of these:\n- Add a button to reverse the name\n- Change the greeting to Spanish\n- Change the theme to soft")
181
+ out_text = gr.TextArea(label="πŸ€– Chat Assistant Response")
182
+ clear_btn = gr.ClearButton([in_prompt, in_audio, out_text])
183
+ with gr.Column():
184
+ stlite_code_area = gr.Code(
185
+ label="App Code - You can also edit directly and then click Update App or ctrl + space",
186
+ language='python', value=starting_app_code(DemoType.STREAMLIT))
187
+ stlite_requirements_area = gr.Code(label="App Requirements (additional modules pip installed for pyodide)")
188
+ update_btn = gr.Button("Update App (Ctrl + Space)", variant="primary", elem_classes=["update-btn"])
189
+ last_error = gr.State()
190
+ code_update_params = {'fn': None, 'inputs': [stlite_code_area, stlite_requirements_area], 'outputs': [stlite_requirements_area, last_error],
191
+ '_js': update_iframe_js(DemoType.STREAMLIT)}
192
+ gen_text_params = {'fn': generate_text, 'inputs': [stlite_code_area, in_prompt],
193
+ 'outputs': [out_text, stlite_code_area]}
194
+ transcribe_params = {'fn': transcribe, 'inputs': [in_audio], 'outputs': [in_prompt, in_audio]}
195
+ update_btn.click(**code_update_params)
196
+ in_prompt.submit(**gen_text_params).then(**code_update_params)
197
+ in_audio.stop_recording(**transcribe_params).then(**gen_text_params).then(**code_update_params)
198
+ with gr.Row():
199
+ with gr.Column():
200
+ gr.Markdown("## 3. Export your app to share!")
201
+ share_link_btn = gr.Button("πŸ”— Copy share link to clipboard")
202
+ share_link_btn.click(link_copy_notify, [stlite_code_area, stlite_requirements_area], None,
203
+ _js=copy_share_link_js(DemoType.STREAMLIT))
204
+ copy_snippet_btn = gr.Button("βœ‚οΈ Copy app snippet into paste in another page")
205
+ copy_snippet_btn.click(copy_notify, [stlite_code_area, stlite_requirements_area], None, _js=copy_snippet_js(DemoType.STREAMLIT))
206
+ download_btn = gr.Button("πŸ—Ž Download app as a standalone file")
207
+ download_btn.click(None, [stlite_code_area, stlite_requirements_area], None, _js=download_code_js(DemoType.STREAMLIT))
208
+ with gr.Row():
209
+ with gr.Column():
210
+ gr.Markdown("## Current limitations")
211
+ with gr.Accordion("Click to view", open=False):
212
+ gr.Markdown(
213
+ "- Only Streamlit apps using libraries available in pyodide are supported\n- The chat hasn't been tuned on Streamlit library data; it may make mistakes")
214
  gradio_lite_tab.select(lambda: "gradio-lite", None, selectedTab).then(None, None, None,
215
  _js=load_js(DemoType.GRADIO))
216
  stlite_tab.select(lambda: "stlite", None, selectedTab).then(None, None, None, _js=load_js(DemoType.STREAMLIT))
 
217
  demo.load(None, None, None, _js=add_hotkeys())
218
+ demo.load(apply_query_params, [gradio_code_area, stlite_code_area], [gradio_code_area, gradio_requirements_area, stlite_code_area, stlite_requirements_area, tabs])
 
219
  demo.css = "footer {visibility: hidden}"
220
 
221
  if __name__ == "__main__":
templates.py CHANGED
@@ -48,8 +48,13 @@ def load_js(demo_type: DemoType) -> str:
48
  // Parse the query string into an object
49
  const queryParams = parseQueryString(queryString);
50
  // Access individual parameters
51
- const codeValue = queryParams.code;
52
- const requirementsValue = queryParams.requirements;
 
 
 
 
 
53
 
54
  const htmlString = '<iframe id="gradio-iframe" width="100%" height="512px" src="about:blank"></iframe>';
55
  const parser = new DOMParser();
@@ -74,6 +79,35 @@ def load_js(demo_type: DemoType) -> str:
74
  if (window.stliteLoaded) {{
75
  return
76
  }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  const htmlString = '<iframe id="stlite-iframe" width="100%" height="512px" src="about:blank"></iframe>';
78
  const parser = new DOMParser();
79
  const doc = parser.parseFromString(htmlString, 'text/html');
@@ -81,7 +115,12 @@ def load_js(demo_type: DemoType) -> str:
81
  const div = document.getElementById('stliteDemoDiv');
82
  div.appendChild(iframe);
83
 
84
- const template = `{stlite_html_template.replace('STARTING_CODE', starting_app_code(demo_type))}`;
 
 
 
 
 
85
  const frame = document.getElementById('stlite-iframe');
86
  frame.contentWindow.document.open();
87
  frame.contentWindow.document.write(template);
@@ -150,6 +189,7 @@ def update_iframe_js(demo_type: DemoType) -> str:
150
  const allRequirements = formattedRequirements.concat(installedRequirements);
151
  // Update URL query params to include the current demo code state
152
  const currentUrl = new URL(window.location.href);
 
153
  currentUrl.searchParams.set('requirements', allRequirements.join('\\n'));
154
  currentUrl.searchParams.set('code', code);
155
  // Replace the current URL with the updated one
@@ -158,14 +198,56 @@ def update_iframe_js(demo_type: DemoType) -> str:
158
  return [allRequirements, errorResult];
159
  }}"""
160
  elif demo_type == DemoType.STREAMLIT:
161
- return f"""async (code, requirements) => {{
 
 
 
 
162
  async function update() {{
163
  const appController = document.getElementById('stlite-iframe').contentWindow.window.appController;
164
- const newCode = code + ` # Update tag ${{Math.random()}}`;
165
- const entrypointFile = "streamlit_app.py";
166
- appController.writeFile(entrypointFile, newCode);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
  }};
168
  await update();
 
 
 
 
 
 
 
 
 
 
 
169
  }}"""
170
  raise NotImplementedError(f'{demo_type} is not a supported demo type')
171
 
@@ -174,6 +256,18 @@ def copy_share_link_js(demo_type: DemoType) -> str:
174
  if demo_type == DemoType.GRADIO:
175
  return f"""async (code, requirements) => {{
176
  const url = new URL(window.location.href);
 
 
 
 
 
 
 
 
 
 
 
 
177
  url.searchParams.set('requirements', requirements);
178
  url.searchParams.set('code', code);
179
  // TODO: Figure out why link doesn't load as expected in Spaces.
@@ -196,14 +290,16 @@ def copy_snippet_js(demo_type: DemoType) -> str:
196
  return [code, requirements];
197
  }}"""
198
  elif demo_type == DemoType.STREAMLIT:
199
- return f"""async (code) => {{
200
  const escapedCode = code.replaceAll(String.fromCharCode(92), String.fromCharCode(92) + String.fromCharCode(92) + String.fromCharCode(92)).replaceAll('`', String.fromCharCode(92) + '`');
201
  const template = `{stlite_snippet_template}`;
202
  // Step 1: Generate the HTML content
203
- const completedTemplate = template.replace('STARTING_CODE', code);
 
204
 
205
  const snippet = completedTemplate;
206
  await navigator.clipboard.writeText(snippet);
 
207
  }}"""
208
  raise NotImplementedError(f'{demo_type} is not a supported demo type')
209
 
@@ -233,10 +329,11 @@ def download_code_js(demo_type: DemoType) -> str:
233
  URL.revokeObjectURL(url);
234
  }}"""
235
  elif demo_type == demo_type.STREAMLIT:
236
- return f"""(code) => {{
237
  const escapedCode = code.replaceAll(String.fromCharCode(92), String.fromCharCode(92) + String.fromCharCode(92)).replaceAll('`', String.fromCharCode(92) + '`');
238
  // Step 1: Generate the HTML content
239
- const completedTemplate = `{stlite_html_template}`.replace('STARTING_CODE', escapedCode);
 
240
 
241
  // Step 2: Create a Blob from the HTML content
242
  const blob = new Blob([completedTemplate], {{ type: "text/html" }});
 
48
  // Parse the query string into an object
49
  const queryParams = parseQueryString(queryString);
50
  // Access individual parameters
51
+ const typeValue = queryParams.type;
52
+ let codeValue = null;
53
+ let requirementsValue = null;
54
+ if (typeValue === 'gradio') {{
55
+ codeValue = queryParams.code;
56
+ requirementsValue = queryParams.requirements;
57
+ }}
58
 
59
  const htmlString = '<iframe id="gradio-iframe" width="100%" height="512px" src="about:blank"></iframe>';
60
  const parser = new DOMParser();
 
79
  if (window.stliteLoaded) {{
80
  return
81
  }}
82
+
83
+ // Get the query string from the URL
84
+ const queryString = window.location.search;
85
+ // Use a function to parse the query string into an object
86
+ function parseQueryString(queryString) {{
87
+ const params = {{}};
88
+ const queryStringWithoutQuestionMark = queryString.substring(1); // Remove the leading question mark
89
+ const keyValuePairs = queryStringWithoutQuestionMark.split('&');
90
+
91
+ keyValuePairs.forEach(keyValue => {{
92
+ const [key, value] = keyValue.split('=');
93
+ if (value) {{
94
+ params[key] = decodeURIComponent(value.replace(/\+/g, ' '));
95
+ }}
96
+ }});
97
+
98
+ return params;
99
+ }}
100
+ // Parse the query string into an object
101
+ const queryParams = parseQueryString(queryString);
102
+ // Access individual parameters
103
+ const typeValue = queryParams.type;
104
+ let codeValue = null;
105
+ let requirementsValue = null;
106
+ if (typeValue === 'streamlit') {{
107
+ codeValue = queryParams.code;
108
+ requirementsValue = queryParams.requirements;
109
+ }}
110
+
111
  const htmlString = '<iframe id="stlite-iframe" width="100%" height="512px" src="about:blank"></iframe>';
112
  const parser = new DOMParser();
113
  const doc = parser.parseFromString(htmlString, 'text/html');
 
115
  const div = document.getElementById('stliteDemoDiv');
116
  div.appendChild(iframe);
117
 
118
+ let template = `{stlite_html_template.replace('STARTING_CODE', starting_app_code(demo_type))}`;
119
+ if (codeValue) {{
120
+ template = `{stlite_html_template}`.replace('STARTING_CODE', codeValue.replaceAll(String.fromCharCode(92), String.fromCharCode(92) + String.fromCharCode(92)).replaceAll('`', String.fromCharCode(92) + '`'));
121
+ }}
122
+ const formattedRequirements = (requirementsValue || '').split('\\n').filter(x => x && !x.startsWith('#')).map(x => x.trim());
123
+ template = template.replace('STARTING_REQUIREMENTS', formattedRequirements.map(x => `"${{x}}"`).join(', ') || '');
124
  const frame = document.getElementById('stlite-iframe');
125
  frame.contentWindow.document.open();
126
  frame.contentWindow.document.write(template);
 
189
  const allRequirements = formattedRequirements.concat(installedRequirements);
190
  // Update URL query params to include the current demo code state
191
  const currentUrl = new URL(window.location.href);
192
+ currentUrl.searchParams.set('type', 'gradio');
193
  currentUrl.searchParams.set('requirements', allRequirements.join('\\n'));
194
  currentUrl.searchParams.set('code', code);
195
  // Replace the current URL with the updated one
 
198
  return [allRequirements, errorResult];
199
  }}"""
200
  elif demo_type == DemoType.STREAMLIT:
201
+ return f"""async (code, requirements) => {{
202
+ const formattedRequirements = requirements.split('\\n').filter(x => x && !x.startsWith('#')).map(x => x.trim());
203
+ let errorResult = null;
204
+ const attemptedRequirements = new Set();
205
+ const installedRequirements = [];
206
  async function update() {{
207
  const appController = document.getElementById('stlite-iframe').contentWindow.window.appController;
208
+ try {{
209
+ await appController.install(formattedRequirements);
210
+ const newCode = code + ` # Update tag ${{Math.random()}}`;
211
+ const entrypointFile = "streamlit_app.py";
212
+ // TODO: As code rerun happens inside streamlit this won't throw an error for self-healing imports
213
+ await appController.writeFile(entrypointFile, newCode);
214
+ }}
215
+ catch (e) {{
216
+ // If the error is caused by a missing module try once to install it and update again.
217
+ if (e.toString().includes('ModuleNotFoundError')) {{
218
+ try {{
219
+ const guessedModuleName = e.toString().split("'")[1].replaceAll('_', '-');
220
+ if (attemptedRequirements.has(guessedModuleName)) {{
221
+ throw Error(`Could not install pyodide module ${{guessedModuleName}}`);
222
+ }}
223
+ console.log(`Attempting to install missing pyodide module "${{guessedModuleName}}"`);
224
+ attemptedRequirements.add(guessedModuleName);
225
+ await appController.install([guessedModuleName]);
226
+ installedRequirements.push(guessedModuleName);
227
+ return await update();
228
+ }}
229
+ catch (err) {{
230
+ console.log(err);
231
+ }}
232
+ }}
233
+
234
+ errorResult = e.toString();
235
+ const allRequirements = formattedRequirements.concat(installedRequirements);
236
+ return [allRequirements, errorResult];
237
+ }}
238
  }};
239
  await update();
240
+
241
+ const allRequirements = formattedRequirements.concat(installedRequirements);
242
+ // Update URL query params to include the current demo code state
243
+ const currentUrl = new URL(window.location.href);
244
+ currentUrl.searchParams.set('type', 'streamlit');
245
+ currentUrl.searchParams.set('requirements', allRequirements.join('\\n'));
246
+ currentUrl.searchParams.set('code', code);
247
+ // Replace the current URL with the updated one
248
+ history.replaceState({{}}, '', currentUrl.href);
249
+
250
+ return [allRequirements.join('\\n'), errorResult];
251
  }}"""
252
  raise NotImplementedError(f'{demo_type} is not a supported demo type')
253
 
 
256
  if demo_type == DemoType.GRADIO:
257
  return f"""async (code, requirements) => {{
258
  const url = new URL(window.location.href);
259
+ url.searchParams.set('type', 'gradio');
260
+ url.searchParams.set('requirements', requirements);
261
+ url.searchParams.set('code', code);
262
+ // TODO: Figure out why link doesn't load as expected in Spaces.
263
+ const shareLink = url.toString().replace('gstaff-kitewind.hf.space', 'huggingface.co/spaces/gstaff/KiteWind');
264
+ await navigator.clipboard.writeText(shareLink);
265
+ return [code, requirements];
266
+ }}"""
267
+ if demo_type == DemoType.STREAMLIT:
268
+ return f"""async (code, requirements) => {{
269
+ const url = new URL(window.location.href);
270
+ url.searchParams.set('type', 'streamlit');
271
  url.searchParams.set('requirements', requirements);
272
  url.searchParams.set('code', code);
273
  // TODO: Figure out why link doesn't load as expected in Spaces.
 
290
  return [code, requirements];
291
  }}"""
292
  elif demo_type == DemoType.STREAMLIT:
293
+ return f"""async (code, requirements) => {{
294
  const escapedCode = code.replaceAll(String.fromCharCode(92), String.fromCharCode(92) + String.fromCharCode(92) + String.fromCharCode(92)).replaceAll('`', String.fromCharCode(92) + '`');
295
  const template = `{stlite_snippet_template}`;
296
  // Step 1: Generate the HTML content
297
+ const formattedRequirements = (requirements || '').split('\\n').filter(x => x && !x.startsWith('#')).map(x => x.trim());
298
+ const completedTemplate = template.replace('STARTING_CODE', code).replace('STARTING_REQUIREMENTS', formattedRequirements.map(x => `"${{x}}"`).join(', ') || '');
299
 
300
  const snippet = completedTemplate;
301
  await navigator.clipboard.writeText(snippet);
302
+ return [code, requirements];
303
  }}"""
304
  raise NotImplementedError(f'{demo_type} is not a supported demo type')
305
 
 
329
  URL.revokeObjectURL(url);
330
  }}"""
331
  elif demo_type == demo_type.STREAMLIT:
332
+ return f"""(code, requirements) => {{
333
  const escapedCode = code.replaceAll(String.fromCharCode(92), String.fromCharCode(92) + String.fromCharCode(92)).replaceAll('`', String.fromCharCode(92) + '`');
334
  // Step 1: Generate the HTML content
335
+ const formattedRequirements = (requirements || '').split('\\n').filter(x => x && !x.startsWith('#')).map(x => x.trim());
336
+ const completedTemplate = `{stlite_html_template}`.replace('STARTING_CODE', escapedCode).replace('STARTING_REQUIREMENTS', formattedRequirements.map(x => `"${{x}}"`).join(', ') || '');
337
 
338
  // Step 2: Create a Blob from the HTML content
339
  const blob = new Blob([completedTemplate], {{ type: "text/html" }});
templates/stlite/stlite-snippet-template.html CHANGED
@@ -9,7 +9,7 @@
9
  const streamlitConfig = "[server]\\\\nrunOnSave = true";
10
  const code = \\\`STARTING_CODE\\\`;
11
  const appController = stlite.mount({
12
- requirements: ["matplotlib"], // Packages to install
13
  entrypoint: "streamlit_app.py", // The target file of the streamlit run command
14
  files: {
15
  ".streamlit/config.toml": streamlitConfig,
 
9
  const streamlitConfig = "[server]\\\\nrunOnSave = true";
10
  const code = \\\`STARTING_CODE\\\`;
11
  const appController = stlite.mount({
12
+ requirements: [STARTING_REQUIREMENTS], // Packages to install
13
  entrypoint: "streamlit_app.py", // The target file of the streamlit run command
14
  files: {
15
  ".streamlit/config.toml": streamlitConfig,
templates/stlite/stlite-template.html CHANGED
@@ -16,7 +16,7 @@
16
  const code = \`STARTING_CODE\`;
17
  // Mount options defined here: https://github.com/whitphx/stlite/blob/main/packages/mountable/src/options.ts#L7
18
  const appController = stlite.mount({
19
- requirements: ["matplotlib"], // Packages to install
20
  entrypoint: "streamlit_app.py", // The target file of the streamlit run command
21
  files: {
22
  ".streamlit/config.toml": streamlitConfig,
 
16
  const code = \`STARTING_CODE\`;
17
  // Mount options defined here: https://github.com/whitphx/stlite/blob/main/packages/mountable/src/options.ts#L7
18
  const appController = stlite.mount({
19
+ requirements: [STARTING_REQUIREMENTS], // Packages to install
20
  entrypoint: "streamlit_app.py", // The target file of the streamlit run command
21
  files: {
22
  ".streamlit/config.toml": streamlitConfig,