@cfasim-ui/docs 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/charts/ChartMenu/ChartMenu.vue +140 -0
- package/charts/ChartMenu/download.ts +44 -0
- package/charts/ChartTooltip/ChartTooltip.vue +97 -0
- package/charts/ChoroplethMap/ChoroplethMap.md +398 -0
- package/charts/ChoroplethMap/ChoroplethMap.vue +777 -0
- package/charts/ChoroplethMap/hsaMapping.ts +4116 -0
- package/charts/DataTable/DataTable.md +143 -0
- package/charts/DataTable/DataTable.vue +277 -0
- package/charts/LineChart/LineChart.md +472 -0
- package/charts/LineChart/LineChart.vue +1216 -0
- package/charts/index.ts +23 -0
- package/charts/tooltip-position.ts +49 -0
- package/components/Box/Box.md +49 -0
- package/components/Box/Box.vue +52 -0
- package/components/Button/Button.md +67 -0
- package/components/Button/Button.vue +81 -0
- package/components/Expander/Expander.md +34 -0
- package/components/Expander/Expander.vue +95 -0
- package/components/Hint/Hint.md +29 -0
- package/components/Hint/Hint.vue +83 -0
- package/components/Icon/Icon.md +67 -0
- package/components/Icon/Icon.vue +112 -0
- package/components/LightDarkToggle/LightDarkToggle.vue +49 -0
- package/components/NumberInput/NumberInput.md +305 -0
- package/components/NumberInput/NumberInput.vue +531 -0
- package/components/SelectBox/SelectBox.md +110 -0
- package/components/SelectBox/SelectBox.vue +195 -0
- package/components/SidebarLayout/SidebarLayout.md +104 -0
- package/components/SidebarLayout/SidebarLayout.vue +466 -0
- package/components/Spinner/Spinner.md +51 -0
- package/components/Spinner/Spinner.vue +55 -0
- package/components/TextInput/TextInput.md +82 -0
- package/components/TextInput/TextInput.vue +94 -0
- package/components/Toggle/Toggle.md +81 -0
- package/components/Toggle/Toggle.vue +81 -0
- package/components/index.ts +15 -0
- package/index.json +121 -0
- package/package.json +24 -0
- package/pyodide/index.ts +7 -0
- package/pyodide/pyodide.worker.ts +233 -0
- package/pyodide/pyodideWorkerApi.ts +102 -0
- package/pyodide/useModel.ts +86 -0
- package/pyodide/vitePlugin.js +51 -0
- package/shared/ModelOutput.ts +88 -0
- package/shared/csv.ts +22 -0
- package/shared/index.ts +24 -0
- package/shared/transferUtils.ts +126 -0
- package/shared/useUrlParams.ts +296 -0
- package/theme/all.js +5 -0
- package/theme/base.css +176 -0
- package/theme/cfasim.css +3 -0
- package/theme/theme.css +113 -0
- package/theme/themes/cdc.css +22 -0
- package/theme/utilities.css +518 -0
- package/wasm/index.ts +2 -0
- package/wasm/useModel.ts +53 -0
- package/wasm/vitePlugin.js +35 -0
- package/wasm/wasm.worker.ts +74 -0
- package/wasm/wasmWorkerApi.ts +38 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, watch } from "vue";
|
|
3
|
+
import Hint from "../Hint/Hint.vue";
|
|
4
|
+
|
|
5
|
+
const model = defineModel<string>();
|
|
6
|
+
const local = ref(model.value);
|
|
7
|
+
|
|
8
|
+
watch(model, (v) => {
|
|
9
|
+
local.value = v;
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
function commit() {
|
|
13
|
+
model.value = local.value;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const props = defineProps<{
|
|
17
|
+
label?: string;
|
|
18
|
+
hideLabel?: boolean;
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
hint?: string;
|
|
21
|
+
}>();
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<template>
|
|
25
|
+
<label v-if="props.label" class="input-label">
|
|
26
|
+
<span
|
|
27
|
+
class="input-label-row"
|
|
28
|
+
:class="{ 'visually-hidden': props.hideLabel }"
|
|
29
|
+
>
|
|
30
|
+
{{ props.label }}
|
|
31
|
+
<Hint v-if="props.hint && !props.hideLabel" :text="props.hint" />
|
|
32
|
+
</span>
|
|
33
|
+
<input
|
|
34
|
+
type="text"
|
|
35
|
+
v-model="local"
|
|
36
|
+
:placeholder="props.placeholder"
|
|
37
|
+
@blur="commit"
|
|
38
|
+
@keydown.enter="commit"
|
|
39
|
+
/>
|
|
40
|
+
</label>
|
|
41
|
+
<div v-else>
|
|
42
|
+
<input
|
|
43
|
+
type="text"
|
|
44
|
+
v-model="local"
|
|
45
|
+
:placeholder="props.placeholder"
|
|
46
|
+
@blur="commit"
|
|
47
|
+
@keydown.enter="commit"
|
|
48
|
+
/>
|
|
49
|
+
</div>
|
|
50
|
+
</template>
|
|
51
|
+
|
|
52
|
+
<style scoped>
|
|
53
|
+
.input-label {
|
|
54
|
+
display: flex;
|
|
55
|
+
flex-direction: column;
|
|
56
|
+
gap: 0.25em;
|
|
57
|
+
font-size: var(--font-size-sm);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.input-label-row {
|
|
61
|
+
display: flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
justify-content: space-between;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
input {
|
|
67
|
+
display: block;
|
|
68
|
+
width: 100%;
|
|
69
|
+
height: 2.5em;
|
|
70
|
+
padding: 0 0.75em;
|
|
71
|
+
font-size: inherit;
|
|
72
|
+
background-color: var(--color-bg-0);
|
|
73
|
+
color: var(--color-text);
|
|
74
|
+
border: 1px solid var(--color-border);
|
|
75
|
+
border-radius: 0.375em;
|
|
76
|
+
transition:
|
|
77
|
+
border-color var(--transition-fast),
|
|
78
|
+
box-shadow var(--transition-fast);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
input:hover {
|
|
82
|
+
border-color: var(--color-border-hover);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
input:focus {
|
|
86
|
+
outline: none;
|
|
87
|
+
border-color: var(--color-border-focus);
|
|
88
|
+
box-shadow: var(--shadow-focus);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
input::placeholder {
|
|
92
|
+
color: var(--color-text-tertiary);
|
|
93
|
+
}
|
|
94
|
+
</style>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Toggle
|
|
2
|
+
|
|
3
|
+
A boolean switch built on reka-ui.
|
|
4
|
+
|
|
5
|
+
## Examples
|
|
6
|
+
|
|
7
|
+
<script setup>
|
|
8
|
+
import { ref } from 'vue'
|
|
9
|
+
const enabled = ref(false)
|
|
10
|
+
const disabled = ref(true)
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
### Basic
|
|
14
|
+
|
|
15
|
+
<ComponentDemo>
|
|
16
|
+
<Toggle v-model="enabled" label="Enable vaccination" />
|
|
17
|
+
|
|
18
|
+
<template #code>
|
|
19
|
+
|
|
20
|
+
```vue
|
|
21
|
+
<script setup>
|
|
22
|
+
import { ref } from "vue";
|
|
23
|
+
const enabled = ref(false);
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<Toggle v-model="enabled" label="Enable vaccination" />
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
</template>
|
|
30
|
+
</ComponentDemo>
|
|
31
|
+
|
|
32
|
+
### With hint
|
|
33
|
+
|
|
34
|
+
<ComponentDemo>
|
|
35
|
+
<Toggle
|
|
36
|
+
v-model="enabled"
|
|
37
|
+
label="Enable isolation"
|
|
38
|
+
hint="Whether symptomatic individuals are isolated"
|
|
39
|
+
/>
|
|
40
|
+
|
|
41
|
+
<template #code>
|
|
42
|
+
|
|
43
|
+
```vue
|
|
44
|
+
<Toggle
|
|
45
|
+
v-model="enabled"
|
|
46
|
+
label="Enable isolation"
|
|
47
|
+
hint="Whether symptomatic individuals are isolated"
|
|
48
|
+
/>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
</template>
|
|
52
|
+
</ComponentDemo>
|
|
53
|
+
|
|
54
|
+
### Disabled
|
|
55
|
+
|
|
56
|
+
<ComponentDemo>
|
|
57
|
+
<Toggle v-model="disabled" label="Locked setting" :disabled="true" />
|
|
58
|
+
|
|
59
|
+
<template #code>
|
|
60
|
+
|
|
61
|
+
```vue
|
|
62
|
+
<Toggle v-model="value" label="Locked setting" :disabled="true" />
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
</template>
|
|
66
|
+
</ComponentDemo>
|
|
67
|
+
|
|
68
|
+
## Model
|
|
69
|
+
|
|
70
|
+
| Name | Type |
|
|
71
|
+
|------|------|
|
|
72
|
+
| `v-model` | `boolean` |
|
|
73
|
+
|
|
74
|
+
## Props
|
|
75
|
+
|
|
76
|
+
| Prop | Type | Required | Default |
|
|
77
|
+
|------|------|----------|---------|
|
|
78
|
+
| `label` | `string` | Yes | — |
|
|
79
|
+
| `hint` | `string` | No | — |
|
|
80
|
+
| `disabled` | `boolean` | No | — |
|
|
81
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { SwitchRoot, SwitchThumb, useId } from "reka-ui";
|
|
3
|
+
import Hint from "../Hint/Hint.vue";
|
|
4
|
+
|
|
5
|
+
const model = defineModel<boolean>();
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
label: string;
|
|
9
|
+
hint?: string;
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
}>();
|
|
12
|
+
|
|
13
|
+
const id = useId();
|
|
14
|
+
</script>
|
|
15
|
+
|
|
16
|
+
<template>
|
|
17
|
+
<div class="toggle">
|
|
18
|
+
<label :for="id">{{ label }}</label>
|
|
19
|
+
<Hint v-if="props.hint" :text="props.hint" />
|
|
20
|
+
<SwitchRoot :id="id" v-model="model" :disabled="disabled" class="switch">
|
|
21
|
+
<SwitchThumb class="thumb" />
|
|
22
|
+
</SwitchRoot>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
25
|
+
|
|
26
|
+
<style scoped>
|
|
27
|
+
.toggle {
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
gap: 0.5em;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.toggle label {
|
|
34
|
+
user-select: none;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.switch {
|
|
38
|
+
--switch-w: 2.25em;
|
|
39
|
+
--switch-h: 1.25em;
|
|
40
|
+
--thumb-size: 1em;
|
|
41
|
+
--thumb-offset: 0.125em;
|
|
42
|
+
|
|
43
|
+
position: relative;
|
|
44
|
+
width: var(--switch-w);
|
|
45
|
+
height: var(--switch-h);
|
|
46
|
+
flex-shrink: 0;
|
|
47
|
+
background: var(--color-border-hover);
|
|
48
|
+
border-radius: var(--switch-h);
|
|
49
|
+
border: none;
|
|
50
|
+
padding: 0;
|
|
51
|
+
cursor: pointer;
|
|
52
|
+
transition: background 0.15s;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.switch[data-state="checked"] {
|
|
56
|
+
background: var(--color-primary);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.switch[data-disabled] {
|
|
60
|
+
opacity: 0.5;
|
|
61
|
+
cursor: not-allowed;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.thumb {
|
|
65
|
+
display: block;
|
|
66
|
+
width: var(--thumb-size);
|
|
67
|
+
height: var(--thumb-size);
|
|
68
|
+
background: white;
|
|
69
|
+
border-radius: 50%;
|
|
70
|
+
position: absolute;
|
|
71
|
+
top: var(--thumb-offset);
|
|
72
|
+
left: var(--thumb-offset);
|
|
73
|
+
transition: transform 0.15s;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.switch[data-state="checked"] .thumb {
|
|
77
|
+
transform: translateX(
|
|
78
|
+
calc(var(--switch-w) - var(--thumb-size) - var(--thumb-offset) * 2)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { default as Box } from "./Box/Box.vue";
|
|
2
|
+
export type { BoxVariant } from "./Box/Box.vue";
|
|
3
|
+
export { default as Button } from "./Button/Button.vue";
|
|
4
|
+
export { default as Expander } from "./Expander/Expander.vue";
|
|
5
|
+
export { default as Hint } from "./Hint/Hint.vue";
|
|
6
|
+
export { default as Icon } from "./Icon/Icon.vue";
|
|
7
|
+
export { default as LightDarkToggle } from "./LightDarkToggle/LightDarkToggle.vue";
|
|
8
|
+
export { default as NumberInput } from "./NumberInput/NumberInput.vue";
|
|
9
|
+
export { default as SelectBox } from "./SelectBox/SelectBox.vue";
|
|
10
|
+
export type { SelectOption } from "./SelectBox/SelectBox.vue";
|
|
11
|
+
export { default as SidebarLayout } from "./SidebarLayout/SidebarLayout.vue";
|
|
12
|
+
export type { Tab } from "./SidebarLayout/SidebarLayout.vue";
|
|
13
|
+
export { default as Spinner } from "./Spinner/Spinner.vue";
|
|
14
|
+
export { default as TextInput } from "./TextInput/TextInput.vue";
|
|
15
|
+
export { default as Toggle } from "./Toggle/Toggle.vue";
|
package/index.json
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.3.11",
|
|
3
|
+
"package": "@cfasim-ui/docs",
|
|
4
|
+
"content": {
|
|
5
|
+
"components": [
|
|
6
|
+
{
|
|
7
|
+
"name": "Box",
|
|
8
|
+
"slug": "box",
|
|
9
|
+
"docs": "components/Box/Box.md",
|
|
10
|
+
"source": "components/Box/Box.vue",
|
|
11
|
+
"keywords": []
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "Button",
|
|
15
|
+
"slug": "button",
|
|
16
|
+
"docs": "components/Button/Button.md",
|
|
17
|
+
"source": "components/Button/Button.vue",
|
|
18
|
+
"keywords": [
|
|
19
|
+
"button",
|
|
20
|
+
"click",
|
|
21
|
+
"action",
|
|
22
|
+
"primary",
|
|
23
|
+
"secondary"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "Expander",
|
|
28
|
+
"slug": "expander",
|
|
29
|
+
"docs": "components/Expander/Expander.md",
|
|
30
|
+
"source": "components/Expander/Expander.vue",
|
|
31
|
+
"keywords": []
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"name": "Hint",
|
|
35
|
+
"slug": "hint",
|
|
36
|
+
"docs": "components/Hint/Hint.md",
|
|
37
|
+
"source": "components/Hint/Hint.vue",
|
|
38
|
+
"keywords": []
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"name": "Icon",
|
|
42
|
+
"slug": "icon",
|
|
43
|
+
"docs": "components/Icon/Icon.md",
|
|
44
|
+
"source": "components/Icon/Icon.vue",
|
|
45
|
+
"keywords": []
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "NumberInput",
|
|
49
|
+
"slug": "number-input",
|
|
50
|
+
"docs": "components/NumberInput/NumberInput.md",
|
|
51
|
+
"source": "components/NumberInput/NumberInput.vue",
|
|
52
|
+
"keywords": []
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "SelectBox",
|
|
56
|
+
"slug": "select-box",
|
|
57
|
+
"docs": "components/SelectBox/SelectBox.md",
|
|
58
|
+
"source": "components/SelectBox/SelectBox.vue",
|
|
59
|
+
"keywords": []
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"name": "SidebarLayout",
|
|
63
|
+
"slug": "sidebar-layout",
|
|
64
|
+
"docs": "components/SidebarLayout/SidebarLayout.md",
|
|
65
|
+
"source": "components/SidebarLayout/SidebarLayout.vue",
|
|
66
|
+
"keywords": []
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "Spinner",
|
|
70
|
+
"slug": "spinner",
|
|
71
|
+
"docs": "components/Spinner/Spinner.md",
|
|
72
|
+
"source": "components/Spinner/Spinner.vue",
|
|
73
|
+
"keywords": []
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"name": "TextInput",
|
|
77
|
+
"slug": "text-input",
|
|
78
|
+
"docs": "components/TextInput/TextInput.md",
|
|
79
|
+
"source": "components/TextInput/TextInput.vue",
|
|
80
|
+
"keywords": []
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "Toggle",
|
|
84
|
+
"slug": "toggle",
|
|
85
|
+
"docs": "components/Toggle/Toggle.md",
|
|
86
|
+
"source": "components/Toggle/Toggle.vue",
|
|
87
|
+
"keywords": []
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
"charts": [
|
|
91
|
+
{
|
|
92
|
+
"name": "ChoroplethMap",
|
|
93
|
+
"slug": "choropleth-map",
|
|
94
|
+
"docs": "charts/ChoroplethMap/ChoroplethMap.md",
|
|
95
|
+
"source": "charts/ChoroplethMap/ChoroplethMap.vue",
|
|
96
|
+
"keywords": []
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
"name": "LineChart",
|
|
100
|
+
"slug": "line-chart",
|
|
101
|
+
"docs": "charts/LineChart/LineChart.md",
|
|
102
|
+
"source": "charts/LineChart/LineChart.vue",
|
|
103
|
+
"keywords": [
|
|
104
|
+
"line",
|
|
105
|
+
"chart",
|
|
106
|
+
"time-series",
|
|
107
|
+
"series",
|
|
108
|
+
"axis",
|
|
109
|
+
"svg"
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"name": "DataTable",
|
|
114
|
+
"slug": "data-table",
|
|
115
|
+
"docs": "charts/DataTable/DataTable.md",
|
|
116
|
+
"source": "charts/DataTable/DataTable.vue",
|
|
117
|
+
"keywords": []
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
}
|
|
121
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cfasim-ui/docs",
|
|
3
|
+
"version": "0.3.11",
|
|
4
|
+
"description": "LLM-friendly component and chart documentation for cfasim-ui",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/CDCgov/cfa-simulator.git",
|
|
9
|
+
"directory": "cfasim-ui/docs"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"index.json",
|
|
16
|
+
"components",
|
|
17
|
+
"charts",
|
|
18
|
+
"shared",
|
|
19
|
+
"pyodide",
|
|
20
|
+
"wasm",
|
|
21
|
+
"theme"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {}
|
|
24
|
+
}
|
package/pyodide/index.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import {
|
|
2
|
+
postWithTransfer,
|
|
3
|
+
postModelOutputsWithTransfer,
|
|
4
|
+
postErrorWithTransfer,
|
|
5
|
+
} from "@cfasim-ui/shared";
|
|
6
|
+
import type { ColumnDescriptor, ModelOutputsWire } from "@cfasim-ui/shared";
|
|
7
|
+
|
|
8
|
+
interface WorkerMessage {
|
|
9
|
+
id: number;
|
|
10
|
+
type?: "run" | "loadModule";
|
|
11
|
+
python?: string;
|
|
12
|
+
module?: string;
|
|
13
|
+
context?: Record<string, unknown>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let wheelMap: Record<string, string> = {};
|
|
17
|
+
|
|
18
|
+
const baseUrl = import.meta.env.BASE_URL ?? "/";
|
|
19
|
+
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
+
let micropip: any;
|
|
22
|
+
|
|
23
|
+
const pyodideReadyPromise = (async () => {
|
|
24
|
+
// @ts-expect-error - Pyodide types from CDN
|
|
25
|
+
const { loadPyodide } = await import(
|
|
26
|
+
/* @vite-ignore */ "https://cdn.jsdelivr.net/pyodide/v0.29.3/full/pyodide.mjs"
|
|
27
|
+
);
|
|
28
|
+
// Load micropip and numpy in parallel with Pyodide bootstrap
|
|
29
|
+
const pyodide = await loadPyodide({
|
|
30
|
+
packages: ["micropip", "numpy"],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
micropip = pyodide.pyimport("micropip");
|
|
34
|
+
|
|
35
|
+
// Build module→wheel lookup but don't install yet
|
|
36
|
+
const res = await fetch(`${self.location.origin}${baseUrl}wheels.json`);
|
|
37
|
+
const wheelFiles: string[] = await res.json();
|
|
38
|
+
for (const filename of wheelFiles) {
|
|
39
|
+
const moduleName = filename.split("-")[0];
|
|
40
|
+
wheelMap[moduleName] = filename;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return pyodide;
|
|
44
|
+
})();
|
|
45
|
+
|
|
46
|
+
let installPromise: Promise<void> | null = null;
|
|
47
|
+
|
|
48
|
+
function installAllWheels(): Promise<void> {
|
|
49
|
+
if (!installPromise) {
|
|
50
|
+
installPromise = (async () => {
|
|
51
|
+
const urls = Object.values(wheelMap).map(
|
|
52
|
+
(f) => `${self.location.origin}${baseUrl}${f}`,
|
|
53
|
+
);
|
|
54
|
+
if (urls.length > 0) {
|
|
55
|
+
await micropip.install(urls);
|
|
56
|
+
}
|
|
57
|
+
})();
|
|
58
|
+
installPromise.catch(() => {
|
|
59
|
+
installPromise = null;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return installPromise;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const modulePromises = new Map<string, Promise<void>>();
|
|
66
|
+
|
|
67
|
+
function ensureModule(
|
|
68
|
+
pyodide: Awaited<typeof pyodideReadyPromise>,
|
|
69
|
+
moduleName: string,
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
if (!modulePromises.has(moduleName)) {
|
|
72
|
+
if (!wheelMap[moduleName]) throw new Error(`Unknown module: ${moduleName}`);
|
|
73
|
+
const promise = (async () => {
|
|
74
|
+
await installAllWheels();
|
|
75
|
+
pyodide.pyimport(moduleName);
|
|
76
|
+
})();
|
|
77
|
+
promise.catch(() => {
|
|
78
|
+
modulePromises.delete(moduleName);
|
|
79
|
+
});
|
|
80
|
+
modulePromises.set(moduleName, promise);
|
|
81
|
+
}
|
|
82
|
+
return modulePromises.get(moduleName)!;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Map Python struct format characters to TypedArray constructors
|
|
86
|
+
const FORMAT_TO_TYPED_ARRAY: Record<
|
|
87
|
+
string,
|
|
88
|
+
new (buffer: ArrayBuffer) => ArrayBufferView
|
|
89
|
+
> = {
|
|
90
|
+
b: Int8Array,
|
|
91
|
+
B: Uint8Array,
|
|
92
|
+
h: Int16Array,
|
|
93
|
+
H: Uint16Array,
|
|
94
|
+
i: Int32Array,
|
|
95
|
+
I: Uint32Array,
|
|
96
|
+
f: Float32Array,
|
|
97
|
+
d: Float64Array,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
101
|
+
function extractNumpyBuffer(proxy: any): ArrayBuffer {
|
|
102
|
+
const pyBuffer = proxy.getBuffer();
|
|
103
|
+
const Ctor = FORMAT_TO_TYPED_ARRAY[pyBuffer.format] ?? Float64Array;
|
|
104
|
+
const typed = new Ctor(
|
|
105
|
+
pyBuffer.data.buffer.slice(
|
|
106
|
+
pyBuffer.data.byteOffset,
|
|
107
|
+
pyBuffer.data.byteOffset + pyBuffer.data.byteLength,
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
pyBuffer.release();
|
|
111
|
+
return typed.buffer as ArrayBuffer;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
115
|
+
function convertModelOutputs(jsResult: any): ModelOutputsWire | null {
|
|
116
|
+
if (!jsResult || typeof jsResult !== "object" || !jsResult.__modelOutputs) {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const outputs: ModelOutputsWire["outputs"] = {};
|
|
121
|
+
for (const [key, outputProxy] of Object.entries(
|
|
122
|
+
jsResult.outputs as Record<string, Record<string, unknown>>,
|
|
123
|
+
)) {
|
|
124
|
+
const wire = outputProxy as {
|
|
125
|
+
length: number;
|
|
126
|
+
columns: { name: string; type: string; enumLabels?: string[] }[];
|
|
127
|
+
buffers: unknown[];
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const buffers: ArrayBuffer[] = [];
|
|
131
|
+
for (const buf of wire.buffers) {
|
|
132
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
133
|
+
if (buf && typeof buf === "object" && (buf as any).getBuffer) {
|
|
134
|
+
buffers.push(extractNumpyBuffer(buf));
|
|
135
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
136
|
+
if ((buf as any).destroy) (buf as any).destroy();
|
|
137
|
+
} else if (buf instanceof ArrayBuffer) {
|
|
138
|
+
buffers.push(buf);
|
|
139
|
+
} else if (ArrayBuffer.isView(buf)) {
|
|
140
|
+
buffers.push(
|
|
141
|
+
buf.buffer.slice(
|
|
142
|
+
buf.byteOffset,
|
|
143
|
+
buf.byteOffset + buf.byteLength,
|
|
144
|
+
) as ArrayBuffer,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
outputs[key] = {
|
|
150
|
+
__modelOutput: true,
|
|
151
|
+
length: wire.length,
|
|
152
|
+
columns: wire.columns as ColumnDescriptor[],
|
|
153
|
+
buffers,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { __modelOutputs: true, outputs };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
self.onmessage = async (event: MessageEvent<WorkerMessage>) => {
|
|
161
|
+
const pyodide = await pyodideReadyPromise;
|
|
162
|
+
const { id, type, python, module: moduleName, context } = event.data;
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
if (type === "loadModule" && moduleName) {
|
|
166
|
+
await ensureModule(pyodide, moduleName);
|
|
167
|
+
postWithTransfer(self, id, true);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let globals;
|
|
172
|
+
if (context) {
|
|
173
|
+
const dict = pyodide.globals.get("dict");
|
|
174
|
+
globals = dict(Object.entries(context));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const t0 = performance.now();
|
|
178
|
+
|
|
179
|
+
// Use synchronous runPython (models don't use top-level await)
|
|
180
|
+
const rawResult = pyodide.runPython(
|
|
181
|
+
python!,
|
|
182
|
+
globals ? { globals } : undefined,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const tPython = performance.now();
|
|
186
|
+
|
|
187
|
+
// Destroy PyProxy if returned to prevent memory leaks
|
|
188
|
+
let result = rawResult;
|
|
189
|
+
if (rawResult && typeof rawResult === "object" && rawResult.destroy) {
|
|
190
|
+
if (rawResult.toJs) {
|
|
191
|
+
result = rawResult.toJs({ dict_converter: Object.fromEntries });
|
|
192
|
+
} else if (rawResult.getBuffer) {
|
|
193
|
+
// Single numpy array: use getBuffer() for direct typed array access
|
|
194
|
+
const pyBuffer = rawResult.getBuffer();
|
|
195
|
+
const Ctor = FORMAT_TO_TYPED_ARRAY[pyBuffer.format] ?? Float64Array;
|
|
196
|
+
result = new Ctor(
|
|
197
|
+
pyBuffer.data.buffer.slice(
|
|
198
|
+
pyBuffer.data.byteOffset,
|
|
199
|
+
pyBuffer.data.byteOffset + pyBuffer.data.byteLength,
|
|
200
|
+
),
|
|
201
|
+
);
|
|
202
|
+
pyBuffer.release();
|
|
203
|
+
} else {
|
|
204
|
+
result = rawResult.toString();
|
|
205
|
+
}
|
|
206
|
+
rawResult.destroy();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Destroy globals proxy if created
|
|
210
|
+
if (globals && globals.destroy) {
|
|
211
|
+
globals.destroy();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const tConvert = performance.now();
|
|
215
|
+
const bench = {
|
|
216
|
+
python_ms: Math.round((tPython - t0) * 10) / 10,
|
|
217
|
+
convert_ms: Math.round((tConvert - tPython) * 10) / 10,
|
|
218
|
+
};
|
|
219
|
+
console.log(
|
|
220
|
+
`[pyodide-worker] python=${bench.python_ms}ms convert=${bench.convert_ms}ms`,
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Check for ModelOutputs wire format
|
|
224
|
+
const modelOutputs = convertModelOutputs(result);
|
|
225
|
+
if (modelOutputs) {
|
|
226
|
+
postModelOutputsWithTransfer(self, id, modelOutputs);
|
|
227
|
+
} else {
|
|
228
|
+
postWithTransfer(self, id, result);
|
|
229
|
+
}
|
|
230
|
+
} catch (error) {
|
|
231
|
+
postErrorWithTransfer(self, id, error);
|
|
232
|
+
}
|
|
233
|
+
};
|