File size: 19,519 Bytes
1ba22b4
4b286f3
 
1ba22b4
 
 
 
 
 
4b286f3
 
82502f1
4b286f3
 
 
1ba22b4
 
82502f1
1ba22b4
4b286f3
 
 
 
1ba22b4
 
4b286f3
149da6f
 
 
73c77f1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3b6c4d9
 
 
 
 
 
 
73c77f1
82502f1
4b286f3
 
82502f1
4b286f3
 
73c77f1
 
 
7563e81
73c77f1
a8e9c78
82502f1
4b286f3
82502f1
4b286f3
149da6f
4b286f3
1ba22b4
4b286f3
149da6f
 
 
3b6c4d9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82502f1
4b286f3
 
82502f1
4b286f3
 
 
3b6c4d9
 
 
 
 
 
82502f1
4b286f3
 
 
149da6f
4b286f3
 
 
 
1ba22b4
 
a8e9c78
 
 
 
 
82502f1
3aacece
 
 
713a4fb
 
3aacece
82502f1
713a4fb
a8e9c78
713a4fb
 
a8e9c78
713a4fb
 
 
 
 
a8e9c78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
713a4fb
 
 
 
a8e9c78
 
 
 
713a4fb
82502f1
 
73c77f1
a8e9c78
73c77f1
 
3b6c4d9
a8e9c78
73c77f1
 
 
a8e9c78
 
82502f1
1ba22b4
3b6c4d9
 
 
 
 
4b286f3
82502f1
3b6c4d9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4b286f3
 
3b6c4d9
 
 
 
 
 
 
 
 
 
 
4b286f3
 
 
 
73c77f1
 
4d17237
 
3b6c4d9
 
 
 
 
 
 
 
 
 
 
 
4d17237
 
7df958b
 
 
 
73c77f1
 
 
 
1ba22b4
 
a8e9c78
7563e81
82502f1
4b286f3
7563e81
4b286f3
 
a8e9c78
4b286f3
1ba22b4
3b6c4d9
7563e81
82502f1
4b286f3
3b6c4d9
 
4b286f3
 
 
3b6c4d9
4b286f3
 
 
 
1ba22b4
 
a8e9c78
7563e81
4b286f3
a8e9c78
82502f1
4b286f3
 
82502f1
4b286f3
 
82502f1
4b286f3
 
 
 
82502f1
4b286f3
 
82502f1
4b286f3
 
 
1ba22b4
3b6c4d9
7563e81
149da6f
3b6c4d9
 
149da6f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4b286f3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
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) => {{
                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);
                        console.log(allRequirements, errorResult);
                        return [allRequirements, errorResult];
                    }}
                }};
                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');
                currentUrl.searchParams.set('requirements', allRequirements.join('\\n'));
                currentUrl.searchParams.set('code', code);
                // Replace the current URL with the updated one
                history.replaceState({{}}, '', currentUrl.href);
                
                return [allRequirements, errorResult];
            }}"""
    elif demo_type == DemoType.STREAMLIT:
        return f"""async (code, requirements) => {{ 
            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 {{
                    await appController.install(formattedRequirements);
                    const newCode = code + ` # Update tag ${{Math.random()}}`;
                    const entrypointFile = "streamlit_app.py";
                    // TODO: As code rerun happens inside streamlit this won't throw an error for self-healing imports
                    await appController.writeFile(entrypointFile, newCode);
                }}
                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 [allRequirements, errorResult];
                }}
            }};
            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');
            currentUrl.searchParams.set('requirements', allRequirements.join('\\n'));
            currentUrl.searchParams.set('code', code);
            // Replace the current URL with the updated one
            history.replaceState({{}}, '', currentUrl.href);
            
            return [allRequirements.join('\\n'), errorResult];
        }}"""
    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')