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

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 (181) hide show
  1. package/README.md +119 -6
  2. package/dist/EmptyState-DgBRoYAy.js +647 -0
  3. package/dist/EmptyState-DgBRoYAy.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-BBWXC7KW.js} +12 -216
  7. package/dist/ExperimentSelectorModal-BBWXC7KW.js.map +1 -0
  8. package/dist/{SettingsModal-CJgLadsN.js → SettingsModal-Dpg-X1OM.js} +12 -9
  9. package/dist/{SettingsModal-CJgLadsN.js.map → SettingsModal-Dpg-X1OM.js.map} +1 -1
  10. package/dist/__tests__/components/BaseToggle.test.d.ts +1 -0
  11. package/dist/__tests__/components/GeneratedPathInput.test.d.ts +1 -0
  12. package/dist/__tests__/components/GeneratedPlotlyResult.test.d.ts +1 -0
  13. package/dist/__tests__/components/GeneratedPluginView.test.d.ts +1 -0
  14. package/dist/__tests__/components/GeneratedResultRenderer.test.d.ts +1 -0
  15. package/dist/__tests__/components/InstrumentAlertLog.test.d.ts +1 -0
  16. package/dist/__tests__/components/SectionCard.test.d.ts +1 -0
  17. package/dist/__tests__/composables/useGeneratedAnalysis.test.d.ts +1 -0
  18. package/dist/__tests__/generatedUi.test.d.ts +1 -0
  19. package/dist/__tests__/types/GeneratedJsonSchema.test.d.ts +1 -0
  20. package/dist/{analysisArtifactTypes-Cu40dzSG.js → analysisArtifactTypes-ejqR8MX3.js} +1 -1
  21. package/dist/{analysisArtifactTypes-Cu40dzSG.js.map → analysisArtifactTypes-ejqR8MX3.js.map} +1 -1
  22. package/dist/{auth-DZAwEeis.js → auth-B87xM--s.js} +1 -1
  23. package/dist/{auth-DZAwEeis.js.map → auth-B87xM--s.js.map} +1 -1
  24. package/dist/chunk-BoAXSpZd.js +33 -0
  25. package/dist/components/AppSidebar.vue.d.ts +1 -1
  26. package/dist/components/AutoGroupModal.vue.d.ts +2 -2
  27. package/dist/components/BaseSelect.vue.d.ts +1 -1
  28. package/dist/components/BaseToggle.vue.d.ts +2 -0
  29. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
  30. package/dist/components/CollapsibleCard.vue.d.ts +1 -1
  31. package/dist/components/ComponentBindingRenderer.vue.d.ts +1 -1
  32. package/dist/components/ConfirmDialog.vue.d.ts +1 -1
  33. package/dist/components/ExperimentDataViewer.vue.d.ts +1 -1
  34. package/dist/components/ExperimentTimeline.vue.d.ts +1 -1
  35. package/dist/components/FileUploader.vue.d.ts +4 -2
  36. package/dist/components/FitPanel.vue.d.ts +2 -2
  37. package/dist/components/FormActions.vue.d.ts +1 -1
  38. package/dist/components/FormBuilder.vue.d.ts +1 -1
  39. package/dist/components/GeneratedPathInput.vue.d.ts +12 -0
  40. package/dist/components/GeneratedPlotlyResult.vue.d.ts +11 -0
  41. package/dist/components/GeneratedPluginView.vue.d.ts +10 -0
  42. package/dist/components/GeneratedResultRenderer.vue.d.ts +6 -0
  43. package/dist/components/JobsStatusTray.vue.d.ts +5 -1
  44. package/dist/components/LcmsSequenceTable.vue.d.ts +1 -1
  45. package/dist/components/PluginWorkspaceView.props.d.ts +1 -1
  46. package/dist/components/SampleLegend.vue.d.ts +1 -1
  47. package/dist/components/SampleSelector.vue.d.ts +2 -2
  48. package/dist/components/SectionCard.vue.d.ts +47 -0
  49. package/dist/components/SettingsModal.vue.d.ts +1 -1
  50. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +2 -2
  51. package/dist/components/index.d.ts +5 -0
  52. package/dist/components/index.js +8 -9
  53. package/dist/components/internal/PlateMapEditorToolbarInternal.vue.d.ts +2 -2
  54. package/dist/{components-DdzHh3a_.js → components-Ci_BTx00.js} +2322 -2292
  55. package/dist/components-Ci_BTx00.js.map +1 -0
  56. package/dist/composables/index.d.ts +3 -2
  57. package/dist/composables/index.js +10 -11
  58. package/dist/composables/useGeneratedAnalysis.d.ts +62 -0
  59. package/dist/composables/useJobsStatusTray.d.ts +48 -4
  60. package/dist/composables/usePlatformContext.d.ts +3 -0
  61. package/dist/composables/usePluginClient.d.ts +17 -8
  62. package/dist/{composables-BUUphBqg.js → composables-C0VEJ73O.js} +154 -38
  63. package/dist/composables-C0VEJ73O.js.map +1 -0
  64. package/dist/experiment-utils-CxkruZ3G.js +392 -0
  65. package/dist/experiment-utils-CxkruZ3G.js.map +1 -0
  66. package/dist/generated/schema.d.ts +7 -0
  67. package/dist/generated-ui/main.d.ts +0 -0
  68. package/dist/index.d.ts +1 -0
  69. package/dist/index.js +15 -18
  70. package/dist/index.js.map +1 -1
  71. package/dist/install.js +7 -8
  72. package/dist/install.js.map +1 -1
  73. package/dist/plotly.min-BR53hja4.js +180634 -0
  74. package/dist/plotly.min-BR53hja4.js.map +1 -0
  75. package/dist/stores/auth.d.ts +2 -2
  76. package/dist/stores/index.js +1 -1
  77. package/dist/styles.css +8657 -8217
  78. package/dist/tailwind.preset.d.ts +5 -0
  79. package/dist/tailwind.preset.js +7 -2
  80. package/dist/tailwind.preset.js.map +1 -1
  81. package/dist/templates/index.js +4 -3
  82. package/dist/templates/packs.d.ts +0 -1
  83. package/dist/{templates-CNbPQNID.js → templates-Cdj93S9C.js} +2 -4
  84. package/dist/templates-Cdj93S9C.js.map +1 -0
  85. package/dist/types/components.d.ts +2 -0
  86. package/dist/types/form-builder.d.ts +1 -1
  87. package/dist/types/generated-ui.d.ts +183 -0
  88. package/dist/types/index.d.ts +2 -1
  89. package/dist/types/index.js +1 -1
  90. package/dist/types/instrument.d.ts +1 -0
  91. package/dist/types/jobs.d.ts +7 -1
  92. package/dist/types/platform.d.ts +2 -0
  93. package/dist/{useControlSchema-BZNdalmL.js → useControlSchema-B_qgU4rW.js} +1882 -172
  94. package/dist/useControlSchema-B_qgU4rW.js.map +1 -0
  95. package/dist/{useDropdownState-Bb-c_PeE.js → useDropdownState-gVaMXtwW.js} +2 -2
  96. package/dist/{useDropdownState-Bb-c_PeE.js.map → useDropdownState-gVaMXtwW.js.map} +1 -1
  97. package/dist/{useExperimentSelector-DRbj6vse.js → useExperimentSelector-BuQZRgMt.js} +4 -255
  98. package/dist/useExperimentSelector-BuQZRgMt.js.map +1 -0
  99. package/dist/{useFormBuilder-DKekvXRs.js → useFormBuilder-D1G5J5Z5.js} +2 -2
  100. package/dist/{useFormBuilder-DKekvXRs.js.map → useFormBuilder-D1G5J5Z5.js.map} +1 -1
  101. package/dist/{useAnalysisArtifacts-C3l0a0vu.js → useProtocolTemplates-DeaOmMaw.js} +307 -475
  102. package/dist/useProtocolTemplates-DeaOmMaw.js.map +1 -0
  103. package/package.json +7 -3
  104. package/src/__tests__/components/BaseSelect.test.ts +52 -0
  105. package/src/__tests__/components/BaseToggle.test.ts +40 -0
  106. package/src/__tests__/components/GeneratedPathInput.test.ts +132 -0
  107. package/src/__tests__/components/GeneratedPlotlyResult.test.ts +117 -0
  108. package/src/__tests__/components/GeneratedPluginView.test.ts +240 -0
  109. package/src/__tests__/components/GeneratedResultRenderer.test.ts +93 -0
  110. package/src/__tests__/components/InstrumentAlertLog.test.ts +59 -0
  111. package/src/__tests__/components/PluginWorkspaceView.test.ts +68 -14
  112. package/src/__tests__/components/SectionCard.test.ts +181 -0
  113. package/src/__tests__/composables/formBuilderRegistry.test.ts +5 -1
  114. package/src/__tests__/composables/useApi.test.ts +8 -0
  115. package/src/__tests__/composables/useGeneratedAnalysis.test.ts +1742 -0
  116. package/src/__tests__/composables/useJobsStatusTray.test.ts +179 -1
  117. package/src/__tests__/composables/usePlatformContext.test.ts +28 -0
  118. package/src/__tests__/composables/usePluginClient.test.ts +402 -21
  119. package/src/__tests__/composables/usePluginConfig.test.ts +323 -7
  120. package/src/__tests__/docs/extractDocsTemplates.test.ts +0 -1
  121. package/src/__tests__/generatedUi.test.ts +119 -0
  122. package/src/__tests__/templates/templates.test.ts +1 -1
  123. package/src/__tests__/types/GeneratedJsonSchema.test.ts +62 -0
  124. package/src/components/BaseSelect.vue +16 -2
  125. package/src/components/BaseToggle.vue +3 -1
  126. package/src/components/FileUploader.vue +49 -11
  127. package/src/components/GeneratedPathInput.vue +218 -0
  128. package/src/components/GeneratedPlotlyResult.vue +87 -0
  129. package/src/components/GeneratedPluginView.vue +279 -0
  130. package/src/components/GeneratedResultRenderer.vue +137 -0
  131. package/src/components/InstrumentAlertLog.vue +2 -2
  132. package/src/components/JobsStatusTray.vue +14 -8
  133. package/src/components/PluginWorkspaceView.props.ts +1 -1
  134. package/src/components/PluginWorkspaceView.vue +1 -5
  135. package/src/components/SectionCard.story.vue +163 -0
  136. package/src/components/SectionCard.vue +98 -0
  137. package/src/components/index.ts +5 -0
  138. package/src/composables/formBuilderRegistry.ts +2 -0
  139. package/src/composables/index.ts +19 -0
  140. package/src/composables/useApi.ts +1 -0
  141. package/src/composables/useGeneratedAnalysis.ts +1270 -0
  142. package/src/composables/useJobsStatusTray.ts +234 -45
  143. package/src/composables/usePluginClient.ts +164 -30
  144. package/src/composables/usePluginConfig.ts +156 -20
  145. package/src/generated/schema.ts +130 -0
  146. package/src/generated-ui/main.ts +21 -0
  147. package/src/index.ts +3 -0
  148. package/src/jobs.ts +5 -0
  149. package/src/plotly-dist.d.ts +3 -0
  150. package/src/styles/components/section-card.css +161 -0
  151. package/src/styles/index.css +1 -0
  152. package/src/styles/variables.css +25 -0
  153. package/src/tailwind.preset.ts +5 -0
  154. package/src/templates/packs.ts +0 -3
  155. package/src/types/components.ts +12 -0
  156. package/src/types/form-builder.ts +1 -0
  157. package/src/types/generated-ui.ts +189 -0
  158. package/src/types/index.ts +4 -0
  159. package/src/types/instrument.ts +1 -0
  160. package/src/types/jobs.ts +10 -1
  161. package/src/types/platform.ts +2 -0
  162. package/dist/BaseButton-Dgqrze41.js +0 -74
  163. package/dist/BaseButton-Dgqrze41.js.map +0 -1
  164. package/dist/BaseModal-B9UA8Y_I.js +0 -165
  165. package/dist/BaseModal-B9UA8Y_I.js.map +0 -1
  166. package/dist/BaseSelect-ekgr9fDo.js +0 -179
  167. package/dist/BaseSelect-ekgr9fDo.js.map +0 -1
  168. package/dist/ExperimentPopover-Ryusjrhv.js +0 -4
  169. package/dist/ExperimentSelectorModal-Cg4-LXBD.js +0 -5
  170. package/dist/ExperimentSelectorModal-DZeSd-yr.js.map +0 -1
  171. package/dist/SettingsModal-Bl_9w2Y3.js +0 -5
  172. package/dist/components-DdzHh3a_.js.map +0 -1
  173. package/dist/composables-BUUphBqg.js.map +0 -1
  174. package/dist/experiment-utils-Bfa7CwPU.js +0 -108
  175. package/dist/experiment-utils-Bfa7CwPU.js.map +0 -1
  176. package/dist/templates-CNbPQNID.js.map +0 -1
  177. package/dist/useAnalysisArtifacts-C3l0a0vu.js.map +0 -1
  178. package/dist/useControlSchema-BZNdalmL.js.map +0 -1
  179. package/dist/useEventListener-CfVkP9Xz.js +0 -57
  180. package/dist/useEventListener-CfVkP9Xz.js.map +0 -1
  181. package/dist/useExperimentSelector-DRbj6vse.js.map +0 -1
@@ -19,7 +19,8 @@ const props = withDefaults(defineProps<Props>(), {
19
19
  })
20
20
 
21
21
  const emit = defineEmits<{
22
- upload: [files: File[]]
22
+ upload: [files: File[], rootName?: string, relativePaths?: string[]]
23
+ remove: [files: File[]]
23
24
  error: [message: string]
24
25
  }>()
25
26
 
@@ -116,20 +117,31 @@ function validateFiles(files: FileList | File[]): File[] {
116
117
  return validFiles
117
118
  }
118
119
 
119
- function handleFiles(files: FileList | File[], folder?: string) {
120
+ function handleFiles(
121
+ files: FileList | File[],
122
+ folder?: string,
123
+ explicitRelativePaths?: string[],
124
+ ) {
125
+ const candidates = Array.from(files)
120
126
  const validFiles = validateFiles(files)
121
127
  if (validFiles.length === 0) return
128
+ const relativePaths = validFiles.map((file) => {
129
+ const candidateIndex = candidates.indexOf(file)
130
+ return explicitRelativePaths?.[candidateIndex]
131
+ || file.webkitRelativePath
132
+ || (folder ? `${folder}/${file.name}` : file.name)
133
+ })
122
134
 
123
135
  if (isFolder.value) {
124
136
  folderName.value = folder || extractFolderName(validFiles)
125
137
  selectedFiles.value = validFiles
126
- emit('upload', validFiles)
138
+ emit('upload', validFiles, folderName.value ?? undefined, relativePaths)
127
139
  } else if (!props.multiple) {
128
140
  selectedFiles.value = [validFiles[0]]
129
- emit('upload', [validFiles[0]])
141
+ emit('upload', [validFiles[0]], undefined, [relativePaths[0]])
130
142
  } else {
131
143
  selectedFiles.value = [...selectedFiles.value, ...validFiles]
132
- emit('upload', validFiles)
144
+ emit('upload', validFiles, undefined, relativePaths)
133
145
  }
134
146
  }
135
147
 
@@ -156,25 +168,39 @@ function handleDrop(event: DragEvent) {
156
168
 
157
169
  async function handleFolderDrop(items: DataTransferItemList) {
158
170
  const files: File[] = []
171
+ const relativePaths: string[] = []
159
172
  let folderNameFromDrop: string | undefined
160
173
 
161
174
  for (const item of Array.from(items)) {
162
175
  const entry = item.webkitGetAsEntry?.()
163
176
  if (entry?.isDirectory) {
164
177
  folderNameFromDrop = entry.name
165
- await readDirectory(entry as FileSystemDirectoryEntry, files)
178
+ await readDirectory(
179
+ entry as FileSystemDirectoryEntry,
180
+ files,
181
+ relativePaths,
182
+ entry.name,
183
+ )
166
184
  } else if (entry?.isFile) {
167
185
  const file = await getFile(entry as FileSystemFileEntry)
168
- if (file) files.push(file)
186
+ if (file) {
187
+ files.push(file)
188
+ relativePaths.push(file.name)
189
+ }
169
190
  }
170
191
  }
171
192
 
172
193
  if (files.length > 0) {
173
- handleFiles(files, folderNameFromDrop)
194
+ handleFiles(files, folderNameFromDrop, relativePaths)
174
195
  }
175
196
  }
176
197
 
177
- async function readDirectory(directory: FileSystemDirectoryEntry, files: File[]): Promise<void> {
198
+ async function readDirectory(
199
+ directory: FileSystemDirectoryEntry,
200
+ files: File[],
201
+ relativePaths: string[],
202
+ prefix: string,
203
+ ): Promise<void> {
178
204
  const reader = directory.createReader()
179
205
  // readEntries returns at most 100 entries per call per spec — must loop
180
206
  let batch: FileSystemEntry[]
@@ -185,9 +211,17 @@ async function readDirectory(directory: FileSystemDirectoryEntry, files: File[])
185
211
  for (const entry of batch) {
186
212
  if (entry.isFile) {
187
213
  const file = await getFile(entry as FileSystemFileEntry)
188
- if (file) files.push(file)
214
+ if (file) {
215
+ files.push(file)
216
+ relativePaths.push(`${prefix}/${file.name}`)
217
+ }
189
218
  } else if (entry.isDirectory) {
190
- await readDirectory(entry as FileSystemDirectoryEntry, files)
219
+ await readDirectory(
220
+ entry as FileSystemDirectoryEntry,
221
+ files,
222
+ relativePaths,
223
+ `${prefix}/${entry.name}`,
224
+ )
191
225
  }
192
226
  }
193
227
  } while (batch.length > 0)
@@ -225,15 +259,19 @@ function openFilePicker() {
225
259
  }
226
260
 
227
261
  function removeFile(index: number) {
262
+ const removed = selectedFiles.value[index]
228
263
  selectedFiles.value = selectedFiles.value.filter((_, i) => i !== index)
229
264
  if (isFolder.value && selectedFiles.value.length === 0) {
230
265
  folderName.value = null
231
266
  }
267
+ if (removed) emit('remove', [removed])
232
268
  }
233
269
 
234
270
  function clearAll() {
271
+ const removed = [...selectedFiles.value]
235
272
  selectedFiles.value = []
236
273
  folderName.value = null
274
+ if (removed.length) emit('remove', removed)
237
275
  }
238
276
 
239
277
  function formatFileSize(bytes: number): string {
@@ -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>