@morscherlab/mint-sdk 1.0.55 → 1.0.57

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 (44) hide show
  1. package/dist/__tests__/components/ArtifactSaveDialog.test.d.ts +1 -0
  2. package/dist/__tests__/components/ArtifactSelectorModal.test.d.ts +1 -0
  3. package/dist/__tests__/composables/useAnalysisArtifacts.test.d.ts +1 -0
  4. package/dist/analysisArtifactTypes-Cu40dzSG.js +7 -0
  5. package/dist/analysisArtifactTypes-Cu40dzSG.js.map +1 -0
  6. package/dist/components/ArtifactSaveDialog.vue.d.ts +56 -0
  7. package/dist/components/ArtifactSelectorModal.vue.d.ts +41 -0
  8. package/dist/components/index.d.ts +2 -0
  9. package/dist/components/index.js +2 -2
  10. package/dist/{components-BQwOeyiy.js → components-PC3SVsaa.js} +1743 -1144
  11. package/dist/components-PC3SVsaa.js.map +1 -0
  12. package/dist/composables/index.d.ts +1 -0
  13. package/dist/composables/index.js +4 -3
  14. package/dist/composables/useAnalysisArtifacts.d.ts +59 -0
  15. package/dist/{composables-TpXyhRZZ.js → composables-OYTD0Y3r.js} +2 -2
  16. package/dist/{composables-TpXyhRZZ.js.map → composables-OYTD0Y3r.js.map} +1 -1
  17. package/dist/index.js +5 -4
  18. package/dist/index.js.map +1 -1
  19. package/dist/install.js +1 -1
  20. package/dist/styles.css +412 -0
  21. package/dist/types/analysisArtifactTypes.d.ts +100 -0
  22. package/dist/types/index.d.ts +2 -0
  23. package/dist/types/index.js +2 -0
  24. package/dist/{useJobsStatusTray-CVecN6Ao.js → useAnalysisArtifacts-Ob8UtVwl.js} +157 -2
  25. package/dist/useAnalysisArtifacts-Ob8UtVwl.js.map +1 -0
  26. package/package.json +1 -1
  27. package/src/__tests__/components/ArtifactSaveDialog.test.ts +516 -0
  28. package/src/__tests__/components/ArtifactSelectorModal.test.ts +178 -0
  29. package/src/__tests__/composables/useAnalysisArtifacts.test.ts +667 -0
  30. package/src/components/ArtifactSaveDialog.story.vue +293 -0
  31. package/src/components/ArtifactSaveDialog.vue +383 -0
  32. package/src/components/ArtifactSelectorModal.story.vue +295 -0
  33. package/src/components/ArtifactSelectorModal.vue +326 -0
  34. package/src/components/index.ts +2 -0
  35. package/src/composables/index.ts +6 -0
  36. package/src/composables/useAnalysisArtifacts.ts +305 -0
  37. package/src/styles/components/artifact-save-dialog.css +17 -0
  38. package/src/styles/components/artifact-selector-modal.css +216 -0
  39. package/src/styles/index.css +2 -0
  40. package/src/types/analysisArtifactTypes.ts +119 -0
  41. package/src/types/index.ts +17 -0
  42. package/dist/components-BQwOeyiy.js.map +0 -1
  43. package/dist/useJobsStatusTray-CVecN6Ao.js.map +0 -1
  44. /package/dist/{ExperimentPopover-8A4Rhffp.js → ExperimentPopover-Ryusjrhv.js} +0 -0
@@ -0,0 +1,305 @@
1
+ import {
2
+ computed,
3
+ ref,
4
+ toValue,
5
+ watch,
6
+ type ComputedRef,
7
+ type MaybeRefOrGetter,
8
+ type Ref,
9
+ } from 'vue'
10
+ import { useApi } from './useApi'
11
+ import { useRequestSyncState } from './useRequestSyncState'
12
+ import {
13
+ getInjectedPlatformContext,
14
+ resolveCurrentExperimentId,
15
+ } from './platformContextHelpers'
16
+ import type {
17
+ AnalysisArtifactDetail,
18
+ AnalysisArtifactListResponse,
19
+ AnalysisArtifactMetadataUpdate,
20
+ AnalysisArtifactStatusFilter,
21
+ AnalysisArtifactSummary,
22
+ } from '../types/analysisArtifactTypes'
23
+
24
+ export { ANALYSIS_FILE_ARTIFACT_SCHEMA } from '../types/analysisArtifactTypes'
25
+
26
+ export interface UseAnalysisArtifactsOptions {
27
+ /** Experiment id; defaults to platform injection or URL. Refetches when it changes. */
28
+ experimentId?: MaybeRefOrGetter<number | undefined>
29
+ /** Override API base URL. */
30
+ apiBaseUrl?: string
31
+ /** Scope to a single owning plugin; omit or null to keep all plugins. */
32
+ pluginId?: MaybeRefOrGetter<string | null | undefined>
33
+ /** Always request archived artifacts alongside active ones. */
34
+ includeArchived?: MaybeRefOrGetter<boolean | undefined>
35
+ /** Fetch as soon as an experiment id is available. */
36
+ immediate?: boolean
37
+ }
38
+
39
+ export interface UseAnalysisArtifactsReturn {
40
+ /** Fetched artifacts, scoped to pluginId when set. */
41
+ artifacts: ComputedRef<AnalysisArtifactSummary[]>
42
+ /** Artifacts after applying statusFilter, pluginFilter, and search. */
43
+ visibleArtifacts: ComputedRef<AnalysisArtifactSummary[]>
44
+ /** Detail loaded by the last successful getDetail call, or null. */
45
+ selectedDetail: Ref<AnalysisArtifactDetail | null>
46
+ /** Resolved experiment id from options, platform injection, or URL. */
47
+ currentExperimentId: ComputedRef<number | undefined>
48
+ /** Whether an experiment id is available. */
49
+ hasCurrentExperiment: ComputedRef<boolean>
50
+ /** Whether any list or detail fetch is in progress. */
51
+ isLoading: ComputedRef<boolean>
52
+ /** Whether any archive/restore/metadata mutation is in progress. */
53
+ isMutating: ComputedRef<boolean>
54
+ /** Error message from the last failed operation, or null. */
55
+ error: Ref<string | null>
56
+ /** Timestamp of the last successful load, or null. */
57
+ lastLoadedAt: Ref<Date | null>
58
+ /** Case-insensitive filter over display_name and artifact_key. */
59
+ search: Ref<string>
60
+ /** Lifecycle filter; widening past "active" refetches with include_archived. */
61
+ statusFilter: Ref<AnalysisArtifactStatusFilter>
62
+ /** Plugin filter applied to visibleArtifacts, or null for all plugins. */
63
+ pluginFilter: Ref<string | null>
64
+ /** Fetch the artifact list for the current experiment. */
65
+ list: () => Promise<AnalysisArtifactSummary[]>
66
+ /** Alias of list. */
67
+ refresh: () => Promise<AnalysisArtifactSummary[]>
68
+ /** Fetch one artifact with its result payload; null when it fails or is superseded. */
69
+ getDetail: (artifactId: number) => Promise<AnalysisArtifactDetail | null>
70
+ /** Update display_name and/or note; resolves to the updated summary or null. */
71
+ updateMetadata: (
72
+ artifactId: number,
73
+ updates: AnalysisArtifactMetadataUpdate,
74
+ ) => Promise<AnalysisArtifactSummary | null>
75
+ /** Archive an artifact; resolves to the updated summary or null. */
76
+ archive: (artifactId: number) => Promise<AnalysisArtifactSummary | null>
77
+ /** Restore an archived artifact; resolves to the updated summary or null. */
78
+ restore: (artifactId: number) => Promise<AnalysisArtifactSummary | null>
79
+ /** Whether a key exists in the loaded artifacts (no network call). */
80
+ hasArtifactKey: (artifactKey: string) => boolean
81
+ /** Reset artifacts, selection, and error. */
82
+ clear: () => void
83
+ }
84
+
85
+ /** Lists and manages analysis artifacts through the user-facing platform API. */
86
+ export function useAnalysisArtifacts(
87
+ options: UseAnalysisArtifactsOptions = {},
88
+ ): UseAnalysisArtifactsReturn {
89
+ const injectedContext = getInjectedPlatformContext()
90
+ const api = useApi({ baseUrl: options.apiBaseUrl ?? injectedContext?.platformApiUrl })
91
+
92
+ const currentExperimentId = computed<number | undefined>(
93
+ () => toValue(options.experimentId) ?? resolveCurrentExperimentId(injectedContext),
94
+ )
95
+ const hasCurrentExperiment = computed(() => currentExperimentId.value !== undefined)
96
+ const scopePluginId = computed(() => toValue(options.pluginId) ?? null)
97
+
98
+ const request = useRequestSyncState('Analysis artifact request failed.')
99
+ const fetchedArtifacts = ref<AnalysisArtifactSummary[]>([])
100
+ const selectedDetail = ref<AnalysisArtifactDetail | null>(null)
101
+
102
+ // Counters, not booleans: overlapping requests must not clear each other's flag.
103
+ const loadingCount = ref(0)
104
+ const mutatingCount = ref(0)
105
+ const isLoading = computed(() => loadingCount.value > 0)
106
+ const isMutating = computed(() => mutatingCount.value > 0)
107
+
108
+ const search = ref('')
109
+ const statusFilter = ref<AnalysisArtifactStatusFilter>('active')
110
+ const pluginFilter = ref<string | null>(null)
111
+
112
+ const needsArchived = computed(
113
+ () => Boolean(toValue(options.includeArchived)) || statusFilter.value !== 'active',
114
+ )
115
+
116
+ const artifacts = computed(() => (
117
+ scopePluginId.value
118
+ ? fetchedArtifacts.value.filter((artifact) => artifact.plugin_id === scopePluginId.value)
119
+ : fetchedArtifacts.value
120
+ ))
121
+
122
+ const visibleArtifacts = computed(() => {
123
+ const query = search.value.trim().toLowerCase()
124
+ return artifacts.value.filter((artifact) => {
125
+ if (statusFilter.value !== 'all' && artifact.status !== statusFilter.value) return false
126
+ if (pluginFilter.value && artifact.plugin_id !== pluginFilter.value) return false
127
+ if (!query) return true
128
+ return (
129
+ artifact.display_name.toLowerCase().includes(query)
130
+ || artifact.artifact_key.toLowerCase().includes(query)
131
+ )
132
+ })
133
+ })
134
+
135
+ // Generation tokens: a superseded response must commit nothing — not rows, not the
136
+ // shared error, not lastLoadedAt — or a late failure would bury fresh results.
137
+ let listToken = 0
138
+ let detailToken = 0
139
+ const hasRequestedList = ref(false)
140
+
141
+ function currentExperimentIdOrError(action: string): number | undefined {
142
+ const id = currentExperimentId.value
143
+ if (id === undefined) {
144
+ request.setError(`No current experiment is selected for ${action}`)
145
+ return undefined
146
+ }
147
+ return id
148
+ }
149
+
150
+ function replaceArtifact(summary: AnalysisArtifactSummary): void {
151
+ const index = fetchedArtifacts.value.findIndex((artifact) => artifact.id === summary.id)
152
+ if (index >= 0) {
153
+ fetchedArtifacts.value.splice(index, 1, summary)
154
+ }
155
+ }
156
+
157
+ async function list(): Promise<AnalysisArtifactSummary[]> {
158
+ const experimentId = currentExperimentIdOrError('listing analysis artifacts')
159
+ if (experimentId === undefined) {
160
+ fetchedArtifacts.value = []
161
+ return []
162
+ }
163
+ const token = ++listToken
164
+ hasRequestedList.value = true
165
+ loadingCount.value += 1
166
+ request.clearError()
167
+ try {
168
+ const data = await api.get<AnalysisArtifactListResponse>(
169
+ `/experiments/${experimentId}/analysis-artifacts?include_archived=${needsArchived.value}`,
170
+ )
171
+ if (token !== listToken) return artifacts.value
172
+ // Copy: replaceArtifact splices in place, and the response array is not ours.
173
+ fetchedArtifacts.value = [...(data?.artifacts ?? [])]
174
+ request.markLoaded()
175
+ return artifacts.value
176
+ } catch (err) {
177
+ if (token !== listToken) return []
178
+ request.setError(err)
179
+ fetchedArtifacts.value = []
180
+ return []
181
+ } finally {
182
+ loadingCount.value -= 1
183
+ }
184
+ }
185
+
186
+ async function getDetail(artifactId: number): Promise<AnalysisArtifactDetail | null> {
187
+ const experimentId = currentExperimentIdOrError('loading an analysis artifact')
188
+ if (experimentId === undefined) return null
189
+ const token = ++detailToken
190
+ loadingCount.value += 1
191
+ request.clearError()
192
+ try {
193
+ const detail = await api.get<AnalysisArtifactDetail>(
194
+ `/experiments/${experimentId}/analysis-artifacts/${artifactId}`,
195
+ )
196
+ if (token !== detailToken) return null
197
+ selectedDetail.value = detail
198
+ request.markLoaded()
199
+ return detail
200
+ } catch (err) {
201
+ if (token !== detailToken) return null
202
+ request.setError(err)
203
+ return null
204
+ } finally {
205
+ loadingCount.value -= 1
206
+ }
207
+ }
208
+
209
+ async function mutate(
210
+ action: string,
211
+ operation: (experimentId: number) => Promise<AnalysisArtifactSummary>,
212
+ ): Promise<AnalysisArtifactSummary | null> {
213
+ const experimentId = currentExperimentIdOrError(action)
214
+ if (experimentId === undefined) return null
215
+ mutatingCount.value += 1
216
+ request.clearError()
217
+ try {
218
+ const summary = await operation(experimentId)
219
+ replaceArtifact(summary)
220
+ request.markSaved()
221
+ return summary
222
+ } catch (err) {
223
+ request.setError(err)
224
+ return null
225
+ } finally {
226
+ mutatingCount.value -= 1
227
+ }
228
+ }
229
+
230
+ async function updateMetadata(
231
+ artifactId: number,
232
+ updates: AnalysisArtifactMetadataUpdate,
233
+ ): Promise<AnalysisArtifactSummary | null> {
234
+ return mutate('updating artifact metadata', (experimentId) =>
235
+ api.patch<AnalysisArtifactSummary>(
236
+ `/experiments/${experimentId}/analysis-artifacts/${artifactId}`,
237
+ updates,
238
+ ))
239
+ }
240
+
241
+ async function archive(artifactId: number): Promise<AnalysisArtifactSummary | null> {
242
+ return mutate('archiving an analysis artifact', (experimentId) =>
243
+ api.post<AnalysisArtifactSummary>(
244
+ `/experiments/${experimentId}/analysis-artifacts/${artifactId}/archive`,
245
+ ))
246
+ }
247
+
248
+ async function restore(artifactId: number): Promise<AnalysisArtifactSummary | null> {
249
+ return mutate('restoring an analysis artifact', (experimentId) =>
250
+ api.post<AnalysisArtifactSummary>(
251
+ `/experiments/${experimentId}/analysis-artifacts/${artifactId}/restore`,
252
+ ))
253
+ }
254
+
255
+ function hasArtifactKey(artifactKey: string): boolean {
256
+ return artifacts.value.some((artifact) => artifact.artifact_key === artifactKey)
257
+ }
258
+
259
+ /** Drop experiment-scoped state and orphan every in-flight response. */
260
+ function clear(): void {
261
+ listToken += 1
262
+ detailToken += 1
263
+ fetchedArtifacts.value = []
264
+ selectedDetail.value = null
265
+ request.clearError()
266
+ }
267
+
268
+ // Refetch when the scope changes or archived rows are newly needed. `immediate`
269
+ // covers a late-resolving experiment id; otherwise stay quiet until something
270
+ // has asked for a list at least once.
271
+ watch([currentExperimentId, needsArchived], ([experimentId], [previousExperimentId]) => {
272
+ if (experimentId !== previousExperimentId) {
273
+ // Scope changed: the previous experiment's rows and any in-flight reply for
274
+ // them must not survive into the new one.
275
+ clear()
276
+ }
277
+ if (experimentId === undefined) return
278
+ if (options.immediate || hasRequestedList.value) {
279
+ void list()
280
+ }
281
+ }, { immediate: options.immediate })
282
+
283
+ return {
284
+ artifacts,
285
+ visibleArtifacts,
286
+ selectedDetail,
287
+ currentExperimentId,
288
+ hasCurrentExperiment,
289
+ isLoading,
290
+ isMutating,
291
+ error: request.error,
292
+ lastLoadedAt: request.lastLoadedAt,
293
+ search,
294
+ statusFilter,
295
+ pluginFilter,
296
+ list,
297
+ refresh: list,
298
+ getDetail,
299
+ updateMetadata,
300
+ archive,
301
+ restore,
302
+ hasArtifactKey,
303
+ clear,
304
+ }
305
+ }
@@ -0,0 +1,17 @@
1
+ .mint-artifact-save-dialog {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 1rem;
5
+ }
6
+
7
+ .mint-artifact-save-dialog__immutable {
8
+ margin: 0;
9
+ font-size: 0.8125rem;
10
+ color: var(--text-muted);
11
+ }
12
+
13
+ .mint-artifact-save-dialog__immutable code {
14
+ font-family: var(--font-mono, 'Fira Code', monospace);
15
+ font-size: 0.75rem;
16
+ color: var(--text-secondary);
17
+ }
@@ -0,0 +1,216 @@
1
+ /* ArtifactSelectorModal Component Styles */
2
+
3
+ .mint-artifact-selector {
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: 0.75rem;
7
+ min-height: 0;
8
+ }
9
+
10
+ /* Filter row */
11
+ .mint-artifact-selector__filters-row {
12
+ display: flex;
13
+ align-items: center;
14
+ gap: 0.5rem;
15
+ flex-wrap: wrap;
16
+ }
17
+
18
+ .mint-artifact-selector__search {
19
+ flex: 1;
20
+ min-width: 10rem;
21
+ }
22
+
23
+ .mint-artifact-selector__filter-select {
24
+ flex-shrink: 0;
25
+ width: 10rem;
26
+ }
27
+
28
+ /* Loading skeleton */
29
+ .mint-artifact-selector__skeleton {
30
+ display: flex;
31
+ flex-direction: column;
32
+ border: 1px solid var(--border-color);
33
+ border-radius: var(--radius-md);
34
+ overflow: hidden;
35
+ }
36
+
37
+ .mint-artifact-selector__skeleton-row {
38
+ display: flex;
39
+ align-items: center;
40
+ gap: 0.75rem;
41
+ padding: 0.75rem 1rem;
42
+ background-color: var(--bg-primary);
43
+ }
44
+
45
+ .mint-artifact-selector__skeleton-content {
46
+ flex: 1;
47
+ display: flex;
48
+ flex-direction: column;
49
+ gap: 6px;
50
+ }
51
+
52
+ /* Error */
53
+ .mint-artifact-selector__error {
54
+ padding: 0.75rem;
55
+ border: 1px solid color-mix(in srgb, var(--mint-error) 30%, transparent);
56
+ border-radius: var(--radius-md);
57
+ background-color: var(--mint-error-bg);
58
+ color: var(--mint-error);
59
+ font-size: 0.875rem;
60
+ }
61
+
62
+ /* Scrollable list */
63
+ .mint-artifact-selector__list {
64
+ max-height: min(28rem, calc(100vh - 16rem));
65
+ overflow-y: auto;
66
+ display: flex;
67
+ flex-direction: column;
68
+ border: 1px solid var(--border-color);
69
+ border-radius: var(--radius-md);
70
+ background-color: var(--bg-primary);
71
+ scrollbar-width: thin;
72
+ overscroll-behavior: contain;
73
+ }
74
+
75
+ .mint-artifact-selector__list::-webkit-scrollbar {
76
+ width: 0.5rem;
77
+ }
78
+
79
+ .mint-artifact-selector__list::-webkit-scrollbar-track {
80
+ background: transparent;
81
+ }
82
+
83
+ .mint-artifact-selector__list::-webkit-scrollbar-thumb {
84
+ background-color: color-mix(in srgb, var(--text-muted) 30%, transparent);
85
+ border: 2px solid transparent;
86
+ border-radius: 9999px;
87
+ background-clip: content-box;
88
+ }
89
+
90
+ /* Row */
91
+ .mint-artifact-selector__row {
92
+ position: relative;
93
+ display: flex;
94
+ align-items: center;
95
+ gap: 0.75rem;
96
+ min-height: 4.125rem;
97
+ padding: 0.75rem 1rem 0.75rem 1.125rem;
98
+ cursor: pointer;
99
+ border-left: 0 solid transparent;
100
+ border-bottom: 1px solid var(--border-light);
101
+ background-color: var(--bg-primary);
102
+ transition:
103
+ background-color 0.15s ease,
104
+ box-shadow 0.15s ease;
105
+ }
106
+
107
+ .mint-artifact-selector__row:hover {
108
+ background-color: var(--bg-hover);
109
+ }
110
+
111
+ .mint-artifact-selector__row--active {
112
+ box-shadow: inset 3px 0 0 var(--color-primary);
113
+ background-color: var(--color-primary-soft);
114
+ }
115
+
116
+ .mint-artifact-selector__row--active:hover {
117
+ background-color: var(--color-primary-soft);
118
+ }
119
+
120
+ /* Keyboard focused row */
121
+ .mint-artifact-selector__row--focused {
122
+ background-color: var(--bg-hover);
123
+ outline: none;
124
+ box-shadow:
125
+ inset 0 0 0 2px var(--color-primary),
126
+ inset 3px 0 0 var(--color-primary);
127
+ }
128
+
129
+ .mint-artifact-selector__row--active.mint-artifact-selector__row--focused {
130
+ background-color: var(--color-primary-soft);
131
+ }
132
+
133
+ .mint-artifact-selector__row--archived .mint-artifact-selector__name,
134
+ .mint-artifact-selector__row--archived .mint-artifact-selector__meta {
135
+ opacity: var(--mint-disabled-opacity, 0.6);
136
+ }
137
+
138
+ /* Row content */
139
+ .mint-artifact-selector__row-content {
140
+ flex: 1;
141
+ min-width: 0;
142
+ }
143
+
144
+ .mint-artifact-selector__name {
145
+ display: flex;
146
+ align-items: center;
147
+ gap: 0.5rem;
148
+ font-size: 0.875rem;
149
+ font-weight: 500;
150
+ color: var(--text-primary);
151
+ min-width: 0;
152
+ }
153
+
154
+ .mint-artifact-selector__key {
155
+ font-family: var(--font-mono, 'Fira Code', monospace);
156
+ font-size: 0.6875rem;
157
+ padding: 0.0625rem 0.375rem;
158
+ border-radius: var(--radius-sm);
159
+ background-color: var(--bg-secondary);
160
+ color: var(--text-secondary);
161
+ white-space: nowrap;
162
+ }
163
+
164
+ .mint-artifact-selector__meta {
165
+ display: flex;
166
+ align-items: center;
167
+ gap: 0.5rem;
168
+ flex-wrap: wrap;
169
+ font-size: 0.75rem;
170
+ color: var(--text-muted);
171
+ margin-top: 0.125rem;
172
+ }
173
+
174
+ .mint-artifact-selector__plugin {
175
+ font-family: var(--font-mono, 'Fira Code', monospace);
176
+ color: var(--text-secondary);
177
+ }
178
+
179
+ /* Status pill + row actions */
180
+ .mint-artifact-selector__row-side {
181
+ display: flex;
182
+ align-items: center;
183
+ gap: 0.5rem;
184
+ flex-shrink: 0;
185
+ }
186
+
187
+ .mint-artifact-selector__action {
188
+ padding: 0.25rem 0.5rem;
189
+ border: 1px solid var(--border-color);
190
+ border-radius: var(--radius-sm);
191
+ background: var(--bg-primary);
192
+ color: var(--text-secondary);
193
+ font-size: 0.75rem;
194
+ cursor: pointer;
195
+ white-space: nowrap;
196
+ transition:
197
+ background-color 0.15s ease,
198
+ border-color 0.15s ease,
199
+ color 0.15s ease;
200
+ }
201
+
202
+ .mint-artifact-selector__action:hover:not(:disabled) {
203
+ border-color: var(--color-primary);
204
+ color: var(--color-primary);
205
+ background-color: var(--bg-hover);
206
+ }
207
+
208
+ .mint-artifact-selector__action:focus-visible {
209
+ outline: none;
210
+ box-shadow: var(--focus-ring);
211
+ }
212
+
213
+ .mint-artifact-selector__action:disabled {
214
+ opacity: var(--mint-disabled-opacity, 0.6);
215
+ cursor: not-allowed;
216
+ }
@@ -85,5 +85,7 @@
85
85
  @import './components/experiment-data-viewer.css';
86
86
  @import './components/experiment-code-badge.css';
87
87
  @import './components/experiment-selector-modal.css';
88
+ @import './components/artifact-selector-modal.css';
89
+ @import './components/artifact-save-dialog.css';
88
90
  @import './components/experiment-popover.css';
89
91
  @import './components/fit-panel.css';
@@ -0,0 +1,119 @@
1
+ import type { TreeNode } from './componentLabTypes'
2
+ import type { SummaryData } from './componentWorkflowTypes'
3
+
4
+ /** Result schema identifier for file-backed analysis artifacts. */
5
+ export const ANALYSIS_FILE_ARTIFACT_SCHEMA = 'mint.analysis_file.v1'
6
+
7
+ /** Known artifact lifecycle states; kept open for forward compatibility. */
8
+ export type AnalysisArtifactStatus = 'active' | 'archived' | (string & {})
9
+
10
+ /** Status filter vocabulary for artifact lists. */
11
+ export type AnalysisArtifactStatusFilter = 'active' | 'archived' | 'all'
12
+
13
+ /** Summary row returned by GET /experiments/{id}/analysis-artifacts. */
14
+ export interface AnalysisArtifactSummary {
15
+ id: number
16
+ experiment_id: number
17
+ plugin_id: string
18
+ artifact_key: string
19
+ display_name: string
20
+ note?: string | null
21
+ status: AnalysisArtifactStatus
22
+ result_keys: string[]
23
+ created_at?: string | null
24
+ updated_at?: string | null
25
+ archived_at?: string | null
26
+ archived_by?: number | null
27
+ }
28
+
29
+ /** Full artifact returned by GET /experiments/{id}/analysis-artifacts/{artifactId}. */
30
+ export interface AnalysisArtifactDetail extends AnalysisArtifactSummary {
31
+ result: Record<string, unknown>
32
+ tree: TreeNode[]
33
+ summary: SummaryData | null
34
+ }
35
+
36
+ /** Response of GET /experiments/{id}/analysis-artifacts. */
37
+ export interface AnalysisArtifactListResponse {
38
+ artifacts: AnalysisArtifactSummary[]
39
+ }
40
+
41
+ /** Body of PATCH /experiments/{id}/analysis-artifacts/{artifactId}. */
42
+ export interface AnalysisArtifactMetadataUpdate {
43
+ display_name?: string
44
+ /** undefined = keep, string = set, null = clear. */
45
+ note?: string | null
46
+ }
47
+
48
+ /** How a save payload should be applied by the plugin backend. */
49
+ export type ArtifactSaveMode = 'create' | 'upsert' | 'update'
50
+
51
+ interface ArtifactSavePayloadBase {
52
+ experimentId: number
53
+ artifactKey: string
54
+ }
55
+
56
+ /**
57
+ * JSON artifact save — handler maps to save_analysis_artifact, which upserts by key.
58
+ * That call only assigns display_name/note when they are provided, and cannot clear
59
+ * either; use the metadata PATCH (useAnalysisArtifacts.updateMetadata) for that.
60
+ */
61
+ export interface JsonArtifactSavePayload extends ArtifactSavePayloadBase {
62
+ kind: 'json'
63
+ mode: 'create' | 'upsert'
64
+ result: Record<string, unknown>
65
+ displayName?: string
66
+ note?: string
67
+ }
68
+
69
+ /** File artifact create — handler maps to save_analysis_file_artifact (create-only: a reused key conflicts). */
70
+ export interface FileArtifactCreatePayload extends ArtifactSavePayloadBase {
71
+ kind: 'file'
72
+ mode: 'create'
73
+ file: File
74
+ /** Maps to the Python `kind=` argument (defaults to "file" backend-side). */
75
+ fileKind: string
76
+ filename?: string
77
+ displayName?: string
78
+ note?: string
79
+ contentType?: string
80
+ metadata?: Record<string, unknown>
81
+ }
82
+
83
+ /**
84
+ * File artifact replacement — handler maps to update_analysis_file_artifact
85
+ * (transactional CAS replace). `kind` and `filename` are immutable backend-side, so
86
+ * `fileKind` echoes the artifact's existing kind and no filename is carried. `note`
87
+ * is three-state: omitted preserves, null clears, a string sets. `metadata` REPLACES
88
+ * the previous result metadata rather than merging, so it always carries the full
89
+ * intended map — omitting it clears whatever was there.
90
+ */
91
+ export interface FileArtifactUpdatePayload extends ArtifactSavePayloadBase {
92
+ kind: 'file'
93
+ mode: 'update'
94
+ file: File
95
+ fileKind: string
96
+ note?: string | null
97
+ contentType?: string
98
+ metadata?: Record<string, unknown>
99
+ }
100
+
101
+ export type ArtifactSavePayload =
102
+ | JsonArtifactSavePayload
103
+ | FileArtifactCreatePayload
104
+ | FileArtifactUpdatePayload
105
+
106
+ /** Optional handler result; cleanupPending surfaces AnalysisFileArtifactUpdate.cleanup_pending. */
107
+ export interface ArtifactSaveResult {
108
+ artifact?: AnalysisArtifactSummary
109
+ cleanupPending?: boolean
110
+ }
111
+
112
+ /**
113
+ * Plugin-supplied save handler. The dialog never writes to the platform;
114
+ * the handler forwards the payload to the plugin backend, which calls
115
+ * save_analysis_artifact / save_analysis_file_artifact / update_analysis_file_artifact.
116
+ */
117
+ export type ArtifactSaveHandler = (
118
+ payload: ArtifactSavePayload,
119
+ ) => Promise<ArtifactSaveResult | void>
@@ -176,6 +176,23 @@ export type {
176
176
  TreeNode,
177
177
  } from './components'
178
178
 
179
+ export type {
180
+ AnalysisArtifactStatus,
181
+ AnalysisArtifactStatusFilter,
182
+ AnalysisArtifactSummary,
183
+ AnalysisArtifactDetail,
184
+ AnalysisArtifactListResponse,
185
+ AnalysisArtifactMetadataUpdate,
186
+ ArtifactSaveMode,
187
+ JsonArtifactSavePayload,
188
+ FileArtifactCreatePayload,
189
+ FileArtifactUpdatePayload,
190
+ ArtifactSavePayload,
191
+ ArtifactSaveResult,
192
+ ArtifactSaveHandler,
193
+ } from './analysisArtifactTypes'
194
+ export { ANALYSIS_FILE_ARTIFACT_SCHEMA } from './analysisArtifactTypes'
195
+
179
196
  export type {
180
197
  JobListPayload,
181
198
  JobProgress,