@morscherlab/mint-sdk 1.0.63 → 1.1.0-beta.2

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.
Files changed (165) hide show
  1. package/README.md +101 -2
  2. package/dist/EmptyState-D1c4RdC7.js +638 -0
  3. package/dist/EmptyState-D1c4RdC7.js.map +1 -0
  4. package/dist/{ExperimentPopover-BbPkIFsI.js → ExperimentPopover-BFw3QX8L.js} +10 -9
  5. package/dist/{ExperimentPopover-BbPkIFsI.js.map → ExperimentPopover-BFw3QX8L.js.map} +1 -1
  6. package/dist/{ExperimentSelectorModal-DZeSd-yr.js → ExperimentSelectorModal-B7w6k3Y8.js} +12 -216
  7. package/dist/ExperimentSelectorModal-B7w6k3Y8.js.map +1 -0
  8. package/dist/{SettingsModal-CJgLadsN.js → SettingsModal-DPDwCbXK.js} +12 -9
  9. package/dist/{SettingsModal-CJgLadsN.js.map → SettingsModal-DPDwCbXK.js.map} +1 -1
  10. package/dist/__tests__/components/GeneratedPlotlyResult.test.d.ts +1 -0
  11. package/dist/__tests__/components/GeneratedPluginView.test.d.ts +1 -0
  12. package/dist/__tests__/components/GeneratedResultRenderer.test.d.ts +1 -0
  13. package/dist/__tests__/components/InstrumentAlertLog.test.d.ts +1 -0
  14. package/dist/__tests__/composables/useGeneratedAnalysis.test.d.ts +1 -0
  15. package/dist/__tests__/generatedUi.test.d.ts +1 -0
  16. package/dist/{analysisArtifactTypes-Cu40dzSG.js → analysisArtifactTypes-ejqR8MX3.js} +1 -1
  17. package/dist/analysisArtifactTypes-ejqR8MX3.js.map +1 -0
  18. package/dist/{auth-DZAwEeis.js → auth-B87xM--s.js} +1 -1
  19. package/dist/{auth-DZAwEeis.js.map → auth-B87xM--s.js.map} +1 -1
  20. package/dist/chunk-BoAXSpZd.js +33 -0
  21. package/dist/components/AppSidebar.vue.d.ts +1 -1
  22. package/dist/components/AutoGroupModal.vue.d.ts +2 -2
  23. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
  24. package/dist/components/CollapsibleCard.vue.d.ts +1 -1
  25. package/dist/components/ComponentBindingRenderer.vue.d.ts +1 -1
  26. package/dist/components/ConfirmDialog.vue.d.ts +1 -1
  27. package/dist/components/ExperimentDataViewer.vue.d.ts +1 -1
  28. package/dist/components/ExperimentTimeline.vue.d.ts +1 -1
  29. package/dist/components/FileUploader.vue.d.ts +4 -2
  30. package/dist/components/FitPanel.vue.d.ts +1 -1
  31. package/dist/components/FormActions.vue.d.ts +1 -1
  32. package/dist/components/FormBuilder.vue.d.ts +1 -1
  33. package/dist/components/GeneratedPathInput.vue.d.ts +12 -0
  34. package/dist/components/GeneratedPlotlyResult.vue.d.ts +11 -0
  35. package/dist/components/GeneratedPluginView.vue.d.ts +10 -0
  36. package/dist/components/GeneratedResultRenderer.vue.d.ts +6 -0
  37. package/dist/components/JobsStatusTray.vue.d.ts +5 -1
  38. package/dist/components/LcmsSequenceTable.vue.d.ts +1 -1
  39. package/dist/components/SampleLegend.vue.d.ts +1 -1
  40. package/dist/components/SampleSelector.vue.d.ts +2 -2
  41. package/dist/components/SettingsModal.vue.d.ts +1 -1
  42. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +2 -2
  43. package/dist/components/index.d.ts +4 -0
  44. package/dist/components/index.js +8 -9
  45. package/dist/components/internal/PlateMapEditorToolbarInternal.vue.d.ts +2 -2
  46. package/dist/{components-DdzHh3a_.js → components-UfkPN1bK.js} +2255 -2303
  47. package/dist/components-UfkPN1bK.js.map +1 -0
  48. package/dist/composables/index.d.ts +2 -3
  49. package/dist/composables/index.js +10 -11
  50. package/dist/composables/platformContextHelpers.d.ts +0 -2
  51. package/dist/composables/useAnalysisArtifacts.d.ts +1 -3
  52. package/dist/composables/useGeneratedAnalysis.d.ts +62 -0
  53. package/dist/composables/useJobsStatusTray.d.ts +48 -4
  54. package/dist/composables/usePlatformContext.d.ts +3 -0
  55. package/dist/composables/usePluginClient.d.ts +6 -3
  56. package/dist/{composables-BUUphBqg.js → composables-B512bWJP.js} +155 -74
  57. package/dist/composables-B512bWJP.js.map +1 -0
  58. package/dist/experiment-utils-CxkruZ3G.js +392 -0
  59. package/dist/experiment-utils-CxkruZ3G.js.map +1 -0
  60. package/dist/generated/schema.d.ts +7 -0
  61. package/dist/generated-ui/main.d.ts +0 -0
  62. package/dist/index.d.ts +1 -0
  63. package/dist/index.js +15 -18
  64. package/dist/index.js.map +1 -1
  65. package/dist/install.js +7 -8
  66. package/dist/install.js.map +1 -1
  67. package/dist/plotly.min-BR53hja4.js +180634 -0
  68. package/dist/plotly.min-BR53hja4.js.map +1 -0
  69. package/dist/stores/auth.d.ts +2 -2
  70. package/dist/stores/index.js +1 -1
  71. package/dist/styles.css +10543 -10404
  72. package/dist/templates/index.js +4 -3
  73. package/dist/templates/packs.d.ts +0 -1
  74. package/dist/{templates-CNbPQNID.js → templates-Cttbjbyc.js} +2 -4
  75. package/dist/templates-Cttbjbyc.js.map +1 -0
  76. package/dist/types/analysisArtifactTypes.d.ts +0 -8
  77. package/dist/types/form-builder.d.ts +1 -1
  78. package/dist/types/generated-ui.d.ts +170 -0
  79. package/dist/types/index.d.ts +2 -1
  80. package/dist/types/index.js +1 -1
  81. package/dist/types/instrument.d.ts +1 -0
  82. package/dist/types/jobs.d.ts +7 -1
  83. package/dist/types/platform.d.ts +2 -0
  84. package/dist/{useControlSchema-BZNdalmL.js → useControlSchema-Dallso-L.js} +1852 -171
  85. package/dist/useControlSchema-Dallso-L.js.map +1 -0
  86. package/dist/{useDropdownState-Bb-c_PeE.js → useDropdownState-gVaMXtwW.js} +2 -2
  87. package/dist/{useDropdownState-Bb-c_PeE.js.map → useDropdownState-gVaMXtwW.js.map} +1 -1
  88. package/dist/{useExperimentSelector-DRbj6vse.js → useExperimentSelector-BMjzNcf2.js} +4 -255
  89. package/dist/useExperimentSelector-BMjzNcf2.js.map +1 -0
  90. package/dist/{useFormBuilder-DKekvXRs.js → useFormBuilder-BXaaDI41.js} +2 -2
  91. package/dist/{useFormBuilder-DKekvXRs.js.map → useFormBuilder-BXaaDI41.js.map} +1 -1
  92. package/dist/{useAnalysisArtifacts-C3l0a0vu.js → useProtocolTemplates-BP7_4RGQ.js} +307 -475
  93. package/dist/useProtocolTemplates-BP7_4RGQ.js.map +1 -0
  94. package/package.json +7 -3
  95. package/src/__tests__/components/GeneratedPathInput.test.ts +132 -0
  96. package/src/__tests__/components/GeneratedPlotlyResult.test.ts +117 -0
  97. package/src/__tests__/components/GeneratedPluginView.test.ts +230 -0
  98. package/src/__tests__/components/GeneratedResultRenderer.test.ts +93 -0
  99. package/src/__tests__/components/InstrumentAlertLog.test.ts +59 -0
  100. package/src/__tests__/composables/formBuilderRegistry.test.ts +5 -1
  101. package/src/__tests__/composables/useAnalysisArtifacts.test.ts +1 -74
  102. package/src/__tests__/composables/useApi.test.ts +8 -0
  103. package/src/__tests__/composables/useGeneratedAnalysis.test.ts +1740 -0
  104. package/src/__tests__/composables/useJobsStatusTray.test.ts +179 -1
  105. package/src/__tests__/composables/usePlatformContext.test.ts +28 -0
  106. package/src/__tests__/composables/usePluginClient.test.ts +366 -20
  107. package/src/__tests__/composables/usePluginConfig.test.ts +323 -7
  108. package/src/__tests__/docs/extractDocsTemplates.test.ts +0 -1
  109. package/src/__tests__/generatedUi.test.ts +119 -0
  110. package/src/__tests__/templates/templates.test.ts +1 -1
  111. package/src/components/FileUploader.vue +49 -11
  112. package/src/components/GeneratedPathInput.vue +218 -0
  113. package/src/components/GeneratedPlotlyResult.vue +87 -0
  114. package/src/components/GeneratedPluginView.vue +279 -0
  115. package/src/components/GeneratedResultRenderer.vue +137 -0
  116. package/src/components/InstrumentAlertLog.vue +2 -2
  117. package/src/components/JobsStatusTray.vue +14 -8
  118. package/src/components/index.ts +4 -0
  119. package/src/composables/formBuilderRegistry.ts +2 -0
  120. package/src/composables/index.ts +18 -5
  121. package/src/composables/platformContextHelpers.ts +3 -10
  122. package/src/composables/useAnalysisArtifacts.ts +0 -48
  123. package/src/composables/useApi.ts +1 -0
  124. package/src/composables/useGeneratedAnalysis.ts +1252 -0
  125. package/src/composables/useJobsStatusTray.ts +234 -45
  126. package/src/composables/usePluginClient.ts +152 -25
  127. package/src/composables/usePluginConfig.ts +156 -20
  128. package/src/generated/schema.ts +130 -0
  129. package/src/generated-ui/main.ts +21 -0
  130. package/src/index.ts +3 -0
  131. package/src/jobs.ts +5 -0
  132. package/src/plotly-dist.d.ts +3 -0
  133. package/src/templates/packs.ts +0 -3
  134. package/src/types/analysisArtifactTypes.ts +0 -9
  135. package/src/types/form-builder.ts +1 -0
  136. package/src/types/generated-ui.ts +176 -0
  137. package/src/types/index.ts +2 -1
  138. package/src/types/instrument.ts +1 -0
  139. package/src/types/jobs.ts +10 -1
  140. package/src/types/platform.ts +2 -0
  141. package/dist/BaseButton-Dgqrze41.js +0 -74
  142. package/dist/BaseButton-Dgqrze41.js.map +0 -1
  143. package/dist/BaseModal-B9UA8Y_I.js +0 -165
  144. package/dist/BaseModal-B9UA8Y_I.js.map +0 -1
  145. package/dist/BaseSelect-ekgr9fDo.js +0 -179
  146. package/dist/BaseSelect-ekgr9fDo.js.map +0 -1
  147. package/dist/ExperimentPopover-Ryusjrhv.js +0 -4
  148. package/dist/ExperimentSelectorModal-Cg4-LXBD.js +0 -5
  149. package/dist/ExperimentSelectorModal-DZeSd-yr.js.map +0 -1
  150. package/dist/SettingsModal-Bl_9w2Y3.js +0 -5
  151. package/dist/analysisArtifactTypes-Cu40dzSG.js.map +0 -1
  152. package/dist/components-DdzHh3a_.js.map +0 -1
  153. package/dist/composables/analysisArtifactTarget.d.ts +0 -5
  154. package/dist/composables-BUUphBqg.js.map +0 -1
  155. package/dist/experiment-utils-Bfa7CwPU.js +0 -108
  156. package/dist/experiment-utils-Bfa7CwPU.js.map +0 -1
  157. package/dist/templates-CNbPQNID.js.map +0 -1
  158. package/dist/useAnalysisArtifacts-C3l0a0vu.js.map +0 -1
  159. package/dist/useControlSchema-BZNdalmL.js.map +0 -1
  160. package/dist/useEventListener-CfVkP9Xz.js +0 -57
  161. package/dist/useEventListener-CfVkP9Xz.js.map +0 -1
  162. package/dist/useExperimentSelector-DRbj6vse.js.map +0 -1
  163. package/src/__tests__/composables/analysisArtifactTarget.test.ts +0 -77
  164. package/src/composables/analysisArtifactTarget.ts +0 -53
  165. /package/dist/__tests__/{composables/analysisArtifactTarget.test.d.ts → components/GeneratedPathInput.test.d.ts} +0 -0
@@ -0,0 +1,218 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref } from 'vue'
3
+
4
+ import { createGeneratedAnalysisTransport } from '../composables/useGeneratedAnalysis'
5
+ import {
6
+ getInjectedPlatformContext,
7
+ resolveCurrentExperimentId,
8
+ } from '../composables/platformContextHelpers'
9
+ import type { AnalysisArtifactDetail } from '../types/analysisArtifactTypes'
10
+ import type { PluginJobPathInput } from '../types/generated-ui'
11
+ import AlertBox from './AlertBox.vue'
12
+ import ArtifactSelectorModal from './ArtifactSelectorModal.vue'
13
+ import BaseButton from './BaseButton.vue'
14
+ import FileUploader from './FileUploader.vue'
15
+
16
+ interface ArtifactFileReference {
17
+ key: string
18
+ plugin_id?: string
19
+ experiment_id?: number
20
+ filename?: string
21
+ content_type?: string
22
+ }
23
+
24
+ const props = defineProps<{
25
+ modelValue?: PluginJobPathInput | PluginJobPathInput[] | null
26
+ disabled?: boolean
27
+ multiple?: boolean
28
+ }>()
29
+
30
+ const emit = defineEmits<{
31
+ 'update:modelValue': [value: PluginJobPathInput | PluginJobPathInput[] | null]
32
+ }>()
33
+
34
+ const mode = ref<'file' | 'folder'>('file')
35
+ const artifactOpen = ref(false)
36
+ const artifactLoading = ref(false)
37
+ const artifactError = ref('')
38
+ const platform = getInjectedPlatformContext()
39
+ const experimentId = resolveCurrentExperimentId(platform)
40
+ const canSelectArtifact = computed(() => Boolean(platform?.isIntegrated && experimentId))
41
+ const transport = createGeneratedAnalysisTransport()
42
+
43
+ function selectMode(nextMode: 'file' | 'folder'): void {
44
+ mode.value = nextMode
45
+ emit('update:modelValue', null)
46
+ }
47
+
48
+ function selectFiles(
49
+ files: File[],
50
+ rootName?: string,
51
+ relativePaths?: string[],
52
+ ): void {
53
+ const paths = relativePaths ?? files.map(file => file.name)
54
+ const selections: PluginJobPathInput[] = props.multiple && !rootName
55
+ ? files.map((file, index) => ({
56
+ kind: 'path',
57
+ files: [file],
58
+ relativePaths: [paths[index] ?? file.name],
59
+ }))
60
+ : [{
61
+ kind: 'path',
62
+ files,
63
+ relativePaths: paths,
64
+ ...(rootName ? { rootName } : {}),
65
+ }]
66
+ if (props.multiple) {
67
+ const existing = Array.isArray(props.modelValue) ? props.modelValue : []
68
+ emit('update:modelValue', [...existing, ...selections])
69
+ } else {
70
+ emit('update:modelValue', selections[0] ?? null)
71
+ }
72
+ }
73
+
74
+ function removeFiles(files: File[]): void {
75
+ const removed = new Set(files)
76
+ const current = Array.isArray(props.modelValue)
77
+ ? props.modelValue
78
+ : props.modelValue
79
+ ? [props.modelValue]
80
+ : []
81
+ const remaining = current.flatMap((selection) => {
82
+ const indexes = selection.files
83
+ .map((file, index) => removed.has(file) ? -1 : index)
84
+ .filter(index => index >= 0)
85
+ if (!indexes.length) return []
86
+ return [{
87
+ ...selection,
88
+ files: indexes.map(index => selection.files[index]!),
89
+ relativePaths: indexes.map(index => selection.relativePaths[index]!),
90
+ }]
91
+ })
92
+ emit(
93
+ 'update:modelValue',
94
+ props.multiple ? remaining : remaining[0] ?? null,
95
+ )
96
+ }
97
+
98
+ async function selectArtifact(detail: AnalysisArtifactDetail): Promise<void> {
99
+ artifactLoading.value = true
100
+ artifactError.value = ''
101
+ try {
102
+ const reference = artifactFileReference(detail)
103
+ const resolvedExperimentId = reference.experiment_id ?? detail.experiment_id
104
+ const pluginId = reference.plugin_id ?? detail.plugin_id
105
+ const baseUrl = (platform?.platformApiUrl ?? '/api').replace(/\/$/, '')
106
+ const objectPath = reference.key.split('/').map(encodeURIComponent).join('/')
107
+ const url = `${baseUrl}/experiments/${resolvedExperimentId}/objects/${objectPath}`
108
+ + `?plugin_id=${encodeURIComponent(pluginId)}&format=raw`
109
+ const response = await transport(url, { method: 'GET' })
110
+ if (!response.ok) throw new Error(`Artifact download failed (${response.status})`)
111
+ const resultFilename = String(detail.result.filename ?? '')
112
+ const keySegments = reference.key.split('/')
113
+ const filename = reference.filename
114
+ || resultFilename
115
+ || keySegments[keySegments.length - 1]
116
+ || 'artifact.bin'
117
+ const contentType = reference.content_type
118
+ ?? response.headers.get('Content-Type')
119
+ ?? 'application/octet-stream'
120
+ const file = new File([await response.blob()], filename, { type: contentType })
121
+ selectFiles([file], undefined, [filename])
122
+ } catch (cause) {
123
+ artifactError.value = cause instanceof Error ? cause.message : 'Failed to load artifact'
124
+ } finally {
125
+ artifactLoading.value = false
126
+ }
127
+ }
128
+
129
+ function artifactFileReference(detail: AnalysisArtifactDetail): ArtifactFileReference {
130
+ if (detail.result.schema_version !== 'mint.analysis_file.v1') {
131
+ throw new Error('Only file-backed analysis artifacts can be used as a path')
132
+ }
133
+ const reference = detail.result.file
134
+ if (!reference || typeof reference !== 'object' || !('key' in reference)) {
135
+ throw new Error('The selected artifact has no file reference')
136
+ }
137
+ const candidate = reference as Record<string, unknown>
138
+ if (typeof candidate.key !== 'string' || !candidate.key) {
139
+ throw new Error('The selected artifact has no object key')
140
+ }
141
+ return {
142
+ key: candidate.key,
143
+ ...(typeof candidate.plugin_id === 'string' ? { plugin_id: candidate.plugin_id } : {}),
144
+ ...(typeof candidate.experiment_id === 'number'
145
+ ? { experiment_id: candidate.experiment_id }
146
+ : {}),
147
+ ...(typeof candidate.filename === 'string' ? { filename: candidate.filename } : {}),
148
+ ...(typeof candidate.content_type === 'string'
149
+ ? { content_type: candidate.content_type }
150
+ : {}),
151
+ }
152
+ }
153
+ </script>
154
+
155
+ <template>
156
+ <div class="mint-generated-path-input">
157
+ <div class="mint-generated-path-input__modes" role="group" aria-label="Path type">
158
+ <BaseButton
159
+ size="sm"
160
+ :variant="mode === 'file' ? 'primary' : 'secondary'"
161
+ :disabled="disabled"
162
+ @click="selectMode('file')"
163
+ >
164
+ File
165
+ </BaseButton>
166
+ <BaseButton
167
+ size="sm"
168
+ :variant="mode === 'folder' ? 'primary' : 'secondary'"
169
+ :disabled="disabled"
170
+ @click="selectMode('folder')"
171
+ >
172
+ Folder
173
+ </BaseButton>
174
+ <BaseButton
175
+ v-if="canSelectArtifact"
176
+ size="sm"
177
+ variant="secondary"
178
+ :disabled="disabled"
179
+ :loading="artifactLoading"
180
+ @click="artifactOpen = true"
181
+ >
182
+ Existing artifact
183
+ </BaseButton>
184
+ </div>
185
+
186
+ <FileUploader
187
+ :key="mode"
188
+ :mode="mode"
189
+ :multiple="multiple && mode === 'file'"
190
+ :disabled="disabled"
191
+ @upload="selectFiles"
192
+ @remove="removeFiles"
193
+ />
194
+
195
+ <AlertBox v-if="artifactError" type="error" :message="artifactError" />
196
+ <ArtifactSelectorModal
197
+ v-if="canSelectArtifact"
198
+ v-model="artifactOpen"
199
+ :experiment-id="experimentId"
200
+ :fetch-detail-on-select="true"
201
+ title="Select a file artifact"
202
+ @select-detail="selectArtifact"
203
+ />
204
+ </div>
205
+ </template>
206
+
207
+ <style scoped>
208
+ .mint-generated-path-input {
209
+ display: grid;
210
+ gap: 0.75rem;
211
+ }
212
+
213
+ .mint-generated-path-input__modes {
214
+ display: flex;
215
+ flex-wrap: wrap;
216
+ gap: 0.5rem;
217
+ }
218
+ </style>
@@ -0,0 +1,87 @@
1
+ <script setup lang="ts">
2
+ import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
3
+ import type * as PlotlyType from 'plotly.js'
4
+
5
+ import ChartContainer from './ChartContainer.vue'
6
+
7
+ const props = defineProps<{
8
+ spec: Record<string, unknown>
9
+ fallback?: { media_type: string; data: string }
10
+ }>()
11
+
12
+ const chartElement = ref<HTMLDivElement | null>(null)
13
+ const failed = ref(false)
14
+ let plotly: typeof PlotlyType | null = null
15
+
16
+ async function loadPlotly(): Promise<typeof PlotlyType> {
17
+ const module = await import('plotly.js-dist-min')
18
+ return ('default' in module ? module.default : module) as unknown as typeof PlotlyType
19
+ }
20
+
21
+ async function renderChart(): Promise<void> {
22
+ if (!chartElement.value) return
23
+ failed.value = false
24
+ await nextTick()
25
+ try {
26
+ const renderer = plotly ?? await loadPlotly()
27
+ plotly = renderer
28
+ const data = Array.isArray(props.spec.data) ? props.spec.data : []
29
+ const layout = props.spec.layout && typeof props.spec.layout === 'object'
30
+ ? props.spec.layout
31
+ : {}
32
+ await renderer.react(
33
+ chartElement.value,
34
+ data as PlotlyType.Data[],
35
+ layout as Partial<PlotlyType.Layout>,
36
+ { responsive: true, displaylogo: false },
37
+ )
38
+ failed.value = false
39
+ } catch {
40
+ failed.value = true
41
+ }
42
+ }
43
+
44
+ onMounted(renderChart)
45
+ watch(() => props.spec, async () => {
46
+ await nextTick()
47
+ await renderChart()
48
+ })
49
+ onBeforeUnmount(() => {
50
+ if (plotly && chartElement.value) plotly.purge(chartElement.value)
51
+ })
52
+ </script>
53
+
54
+ <template>
55
+ <ChartContainer title="Interactive chart">
56
+ <img
57
+ v-if="failed && fallback"
58
+ class="mint-generated-result__image"
59
+ :src="`data:${fallback.media_type};base64,${fallback.data}`"
60
+ alt="Static Plotly chart fallback"
61
+ >
62
+ <p
63
+ v-else-if="failed"
64
+ class="mint-generated-result__plotly-error"
65
+ role="alert"
66
+ >
67
+ Unable to display this chart.
68
+ </p>
69
+ <div
70
+ ref="chartElement"
71
+ v-show="!failed"
72
+ class="mint-generated-result__plotly"
73
+ aria-label="Interactive Plotly chart"
74
+ />
75
+ </ChartContainer>
76
+ </template>
77
+
78
+ <style scoped>
79
+ .mint-generated-result__plotly-error {
80
+ margin: 0;
81
+ padding: 1rem;
82
+ border: 1px solid var(--color-danger-border, var(--color-border));
83
+ border-radius: 0.75rem;
84
+ background: var(--color-danger-bg, var(--color-bg-secondary));
85
+ color: var(--color-danger-text, var(--color-text-primary));
86
+ }
87
+ </style>
@@ -0,0 +1,279 @@
1
+ <script setup lang="ts">
2
+ import { computed, onMounted, reactive, ref, watch } from 'vue'
3
+
4
+ import { generatedJobDefaults, generatedJobFormSchema, normalizeGeneratedJobInput } from '../generated/schema'
5
+ import type { FormSchema } from '../types/form-builder'
6
+ import type {
7
+ GeneratedAnalysisJob,
8
+ GeneratedAnalysisResult,
9
+ GeneratedJobDefinition,
10
+ } from '../types/generated-ui'
11
+ import type { PillNavItem, SidebarToolSection } from '../types'
12
+ import {
13
+ useGeneratedAnalysis,
14
+ type GeneratedAnalysisTransport,
15
+ } from '../composables/useGeneratedAnalysis'
16
+ import AlertBox from './AlertBox.vue'
17
+ import EmptyState from './EmptyState.vue'
18
+ import GeneratedResultRenderer from './GeneratedResultRenderer.vue'
19
+ import JobsStatusTray from './JobsStatusTray.vue'
20
+ import LoadingSpinner from './LoadingSpinner.vue'
21
+ import PluginWorkspaceView from './PluginWorkspaceView.vue'
22
+
23
+ const props = withDefaults(defineProps<{
24
+ baseUrl: string
25
+ showStandaloneLabel?: boolean
26
+ transport?: GeneratedAnalysisTransport
27
+ }>(), {
28
+ showStandaloneLabel: false,
29
+ })
30
+
31
+ const generated = useGeneratedAnalysis({
32
+ baseUrl: props.baseUrl,
33
+ transport: props.transport,
34
+ })
35
+ const activeDefinitionId = ref('')
36
+ const selectedJobId = ref('')
37
+ const valuesByDefinition = reactive<Record<string, Record<string, unknown>>>({})
38
+ const definitions = computed(() => generated.manifest.value?.jobs ?? [])
39
+ const activeDefinition = computed(() => (
40
+ definitions.value.find(definition => definition.id === activeDefinitionId.value) ?? null
41
+ ))
42
+
43
+ const pillNav = computed<PillNavItem[]>(() => definitions.value.map(definition => ({
44
+ id: definition.id,
45
+ label: definition.title,
46
+ })))
47
+
48
+ const panels = computed<Record<string, SidebarToolSection[]>>(() => Object.fromEntries(
49
+ definitions.value.map(definition => [definition.id, [{
50
+ id: definition.id,
51
+ label: 'Parameters',
52
+ subtitle: resourceSummary(definition),
53
+ defaultOpen: true,
54
+ }]]),
55
+ ))
56
+
57
+ const forms = computed<Record<string, FormSchema>>(() => Object.fromEntries(
58
+ definitions.value.map(definition => [definition.id, generatedJobFormSchema(definition)]),
59
+ ))
60
+
61
+ const currentValues = computed(() => valuesByDefinition[activeDefinitionId.value] ?? {})
62
+ const currentJob = computed(() => {
63
+ const currentSessionId = generated.session.value?.id
64
+ const currentDefinitionId = activeDefinitionId.value
65
+ const selected = generated.jobs.value.find(candidate => (
66
+ candidate.job_id === selectedJobId.value
67
+ && candidate.status === 'completed'
68
+ && candidate.metadata.definition_id === currentDefinitionId
69
+ && candidate.metadata.has_result === true
70
+ ))
71
+ return selected ?? generated.jobs.value.find(candidate => (
72
+ candidate.session_id === currentSessionId
73
+ && candidate.status === 'completed'
74
+ && candidate.metadata.definition_id === currentDefinitionId
75
+ && candidate.metadata.has_result === true
76
+ ))
77
+ })
78
+ const currentResult = computed<GeneratedAnalysisResult | null>(() => (
79
+ currentJob.value ? generated.resultForJob(currentJob.value.job_id) : null
80
+ ))
81
+
82
+ watch(definitions, (nextDefinitions) => {
83
+ for (const definition of nextDefinitions) {
84
+ valuesByDefinition[definition.id] ??= generatedJobDefaults(definition)
85
+ }
86
+ if (!nextDefinitions.some(definition => definition.id === activeDefinitionId.value)) {
87
+ activeDefinitionId.value = nextDefinitions[0]?.id ?? ''
88
+ }
89
+ }, { immediate: true })
90
+
91
+ watch(currentJob, (job) => {
92
+ if (job) void generated.loadResult(job)
93
+ }, { immediate: true })
94
+
95
+ function updateValues(values: Record<string, unknown>): void {
96
+ if (activeDefinitionId.value) valuesByDefinition[activeDefinitionId.value] = { ...values }
97
+ }
98
+
99
+ async function submitJob(sectionId: string, values: Record<string, unknown>): Promise<void> {
100
+ const definition = definitions.value.find(candidate => candidate.id === sectionId)
101
+ if (!definition) return
102
+ selectedJobId.value = ''
103
+ valuesByDefinition[definition.id] = { ...values }
104
+ await generated.submit(definition.id, normalizeGeneratedJobInput(definition, values))
105
+ }
106
+
107
+ function selectJob(job: GeneratedAnalysisJob): void {
108
+ selectedJobId.value = job.job_id
109
+ activeDefinitionId.value = String(job.metadata.definition_id ?? activeDefinitionId.value)
110
+ }
111
+
112
+ function resourceSummary(definition: GeneratedJobDefinition): string {
113
+ if (definition.lifecycle === 'service' || definition.profile?.kind === 'service') {
114
+ return 'Host service'
115
+ }
116
+ if (definition.lifecycle === 'staged' || definition.profile?.kind === 'staged') {
117
+ return 'Staged upload'
118
+ }
119
+ return `${definition.cpu} CPU`
120
+ }
121
+
122
+ onMounted(() => {
123
+ void generated.initialize().catch(() => undefined)
124
+ })
125
+ </script>
126
+
127
+ <template>
128
+ <PluginWorkspaceView
129
+ :title="generated.manifest.value?.ui.title ?? 'Analysis'"
130
+ :subtitle="generated.manifest.value?.ui.description"
131
+ :show-standalone-label="showStandaloneLabel"
132
+ :show-settings="false"
133
+ :pill-nav="pillNav"
134
+ :active-view="activeDefinitionId"
135
+ :panels="panels"
136
+ :forms="forms"
137
+ sidebar-title="Parameters"
138
+ sidebar-position="left"
139
+ :values="currentValues"
140
+ :show-form-actions="true"
141
+ :form-loading="generated.isSubmitting.value"
142
+ :form-disabled="generated.isSubmitting.value"
143
+ @update:active-view="activeDefinitionId = $event"
144
+ @update:values="updateValues"
145
+ @form-submit="submitJob"
146
+ >
147
+ <main class="mint-generated-workspace">
148
+ <div v-if="generated.isLoading.value" class="mint-generated-workspace__loading">
149
+ <LoadingSpinner size="lg" />
150
+ <span>Loading analysis workspace</span>
151
+ </div>
152
+
153
+ <template v-else>
154
+ <AlertBox
155
+ v-if="generated.error.value"
156
+ type="error"
157
+ :message="generated.error.value"
158
+ />
159
+
160
+ <header v-if="activeDefinition" class="mint-generated-workspace__header">
161
+ <div>
162
+ <p class="mint-generated-workspace__eyebrow">Analysis result</p>
163
+ <h1>{{ activeDefinition.title }}</h1>
164
+ <p v-if="activeDefinition.description">{{ activeDefinition.description }}</p>
165
+ </div>
166
+ <div class="mint-generated-workspace__resources" aria-label="Job resource requirements">
167
+ <span>{{ resourceSummary(activeDefinition) }}</span>
168
+ </div>
169
+ </header>
170
+
171
+ <section class="mint-generated-workspace__result">
172
+ <GeneratedResultRenderer
173
+ v-if="currentResult"
174
+ :result="currentResult"
175
+ />
176
+ <EmptyState
177
+ v-else
178
+ title="No result yet"
179
+ description="Set the parameters and run the analysis. Jobs continue independently if you switch tabs."
180
+ size="md"
181
+ />
182
+ </section>
183
+ </template>
184
+
185
+ <JobsStatusTray
186
+ :source="generated"
187
+ :teleport-to="false"
188
+ @select="selectJob"
189
+ />
190
+ </main>
191
+ </PluginWorkspaceView>
192
+ </template>
193
+
194
+ <style scoped>
195
+ .mint-generated-workspace {
196
+ position: relative;
197
+ min-width: 0;
198
+ min-height: calc(100vh - 5rem);
199
+ padding: clamp(1rem, 2.5vw, 2.5rem);
200
+ background: var(--color-bg-primary);
201
+ }
202
+
203
+ .mint-generated-workspace__loading {
204
+ display: flex;
205
+ min-height: 24rem;
206
+ align-items: center;
207
+ justify-content: center;
208
+ gap: 0.75rem;
209
+ color: var(--color-text-secondary);
210
+ }
211
+
212
+ .mint-generated-workspace__header {
213
+ display: flex;
214
+ align-items: flex-start;
215
+ justify-content: space-between;
216
+ gap: 1.5rem;
217
+ max-width: 76rem;
218
+ margin: 0 auto 1.25rem;
219
+ }
220
+
221
+ .mint-generated-workspace__header h1 {
222
+ margin: 0;
223
+ color: var(--color-text-primary);
224
+ font-size: clamp(1.35rem, 2vw, 1.8rem);
225
+ line-height: 1.2;
226
+ }
227
+
228
+ .mint-generated-workspace__header p:not(.mint-generated-workspace__eyebrow) {
229
+ margin: 0.4rem 0 0;
230
+ color: var(--color-text-secondary);
231
+ }
232
+
233
+ .mint-generated-workspace__eyebrow {
234
+ margin: 0 0 0.35rem;
235
+ color: var(--color-text-muted);
236
+ font-size: 0.72rem;
237
+ font-weight: 700;
238
+ letter-spacing: 0.08em;
239
+ text-transform: uppercase;
240
+ }
241
+
242
+ .mint-generated-workspace__resources {
243
+ display: flex;
244
+ flex-wrap: wrap;
245
+ justify-content: flex-end;
246
+ gap: 0.4rem;
247
+ }
248
+
249
+ .mint-generated-workspace__resources span {
250
+ padding: 0.3rem 0.55rem;
251
+ border: 1px solid var(--color-border);
252
+ border-radius: 999px;
253
+ background: var(--color-bg-secondary);
254
+ color: var(--color-text-secondary);
255
+ font-size: 0.75rem;
256
+ font-weight: 600;
257
+ }
258
+
259
+ .mint-generated-workspace__result {
260
+ max-width: 76rem;
261
+ min-height: 24rem;
262
+ margin: 0 auto;
263
+ padding: clamp(1rem, 2vw, 1.75rem);
264
+ border: 1px solid var(--color-border);
265
+ border-radius: 1rem;
266
+ background: var(--color-bg-secondary);
267
+ box-shadow: var(--shadow-sm);
268
+ }
269
+
270
+ @media (max-width: 640px) {
271
+ .mint-generated-workspace__header {
272
+ flex-direction: column;
273
+ }
274
+
275
+ .mint-generated-workspace__resources {
276
+ justify-content: flex-start;
277
+ }
278
+ }
279
+ </style>