@morscherlab/mld-sdk 0.7.8 → 0.8.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.
- package/dist/components/ExperimentCodeBadge.vue.d.ts +7 -1
- package/dist/components/ExperimentCodeBadge.vue.js +41 -5
- package/dist/components/ExperimentCodeBadge.vue.js.map +1 -1
- package/dist/components/ExperimentDataViewer.vue.d.ts +1 -1
- package/dist/components/ExperimentDataViewer.vue.js +98 -44
- package/dist/components/ExperimentDataViewer.vue.js.map +1 -1
- package/dist/components/ExperimentSelectorModal.vue.d.ts +3 -1
- package/dist/components/ExperimentSelectorModal.vue.js +117 -63
- package/dist/components/ExperimentSelectorModal.vue.js.map +1 -1
- package/dist/composables/experiment-utils.d.ts +5 -0
- package/dist/composables/experiment-utils.js +34 -0
- package/dist/composables/experiment-utils.js.map +1 -0
- package/dist/composables/index.d.ts +2 -0
- package/dist/composables/index.js +7 -0
- package/dist/composables/index.js.map +1 -1
- package/dist/composables/useExperimentData.d.ts +17 -0
- package/dist/composables/useExperimentData.js +62 -0
- package/dist/composables/useExperimentData.js.map +1 -0
- package/dist/composables/useExperimentSelector.d.ts +5 -1
- package/dist/composables/useExperimentSelector.js +39 -9
- package/dist/composables/useExperimentSelector.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/styles.css +121 -10
- package/package.json +1 -1
- package/src/components/ExperimentCodeBadge.story.vue +77 -0
- package/src/components/ExperimentCodeBadge.vue +46 -3
- package/src/components/ExperimentDataViewer.story.vue +174 -0
- package/src/components/ExperimentDataViewer.vue +49 -12
- package/src/components/ExperimentSelectorModal.story.vue +244 -0
- package/src/components/ExperimentSelectorModal.vue +75 -37
- package/src/components/FitPanel.story.vue +125 -0
- package/src/composables/experiment-utils.ts +32 -0
- package/src/composables/index.ts +11 -0
- package/src/composables/useExperimentData.ts +85 -0
- package/src/composables/useExperimentSelector.ts +48 -9
- package/src/index.ts +9 -0
- package/src/styles/components/experiment-code-badge.css +20 -0
- package/src/styles/components/experiment-data-viewer.css +8 -1
- package/src/styles/components/experiment-selector-modal.css +39 -4
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ExperimentStatus, SelectOption, PillVariant } from '../types'
|
|
2
|
+
|
|
3
|
+
export function formatExperimentDate(dateStr: string): string {
|
|
4
|
+
try {
|
|
5
|
+
return new Date(dateStr).toLocaleDateString(undefined, {
|
|
6
|
+
year: 'numeric',
|
|
7
|
+
month: 'short',
|
|
8
|
+
day: 'numeric',
|
|
9
|
+
})
|
|
10
|
+
} catch {
|
|
11
|
+
return dateStr
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const EXPERIMENT_STATUS_OPTIONS: SelectOption<string>[] = [
|
|
16
|
+
{ value: '', label: 'All statuses' },
|
|
17
|
+
{ value: 'planned', label: 'Planned' },
|
|
18
|
+
{ value: 'ongoing', label: 'Ongoing' },
|
|
19
|
+
{ value: 'completed', label: 'Completed' },
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
export const EXPERIMENT_STATUS_VARIANT_MAP: Record<ExperimentStatus, PillVariant> = {
|
|
23
|
+
planned: 'default',
|
|
24
|
+
ongoing: 'primary',
|
|
25
|
+
completed: 'success',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const EXPERIMENT_STATUS_LABELS: Record<ExperimentStatus, string> = {
|
|
29
|
+
planned: 'Planned',
|
|
30
|
+
ongoing: 'Ongoing',
|
|
31
|
+
completed: 'Completed',
|
|
32
|
+
}
|
package/src/composables/index.ts
CHANGED
|
@@ -93,6 +93,17 @@ export {
|
|
|
93
93
|
type UseExperimentSelectorOptions,
|
|
94
94
|
type UseExperimentSelectorReturn,
|
|
95
95
|
} from './useExperimentSelector'
|
|
96
|
+
export {
|
|
97
|
+
formatExperimentDate,
|
|
98
|
+
EXPERIMENT_STATUS_OPTIONS,
|
|
99
|
+
EXPERIMENT_STATUS_VARIANT_MAP,
|
|
100
|
+
EXPERIMENT_STATUS_LABELS,
|
|
101
|
+
} from './experiment-utils'
|
|
102
|
+
export {
|
|
103
|
+
useExperimentData,
|
|
104
|
+
type UseExperimentDataOptions,
|
|
105
|
+
type UseExperimentDataReturn,
|
|
106
|
+
} from './useExperimentData'
|
|
96
107
|
export {
|
|
97
108
|
getFieldRegistryEntry,
|
|
98
109
|
getTypeDefault,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { ref, computed, type Ref, type ComputedRef } from 'vue'
|
|
2
|
+
import { useApi } from './useApi'
|
|
3
|
+
import type { TreeNode, SummaryData } from '../types'
|
|
4
|
+
|
|
5
|
+
export interface UseExperimentDataOptions {
|
|
6
|
+
apiBaseUrl?: string
|
|
7
|
+
immediate?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface UseExperimentDataReturn {
|
|
11
|
+
data: Ref<Record<string, unknown> | null>
|
|
12
|
+
treeData: ComputedRef<TreeNode[]>
|
|
13
|
+
tableData: ComputedRef<Record<string, unknown>[]>
|
|
14
|
+
summaryData: ComputedRef<SummaryData | null>
|
|
15
|
+
isLoading: Ref<boolean>
|
|
16
|
+
error: Ref<string | null>
|
|
17
|
+
fetch: (experimentId: number) => Promise<void>
|
|
18
|
+
refresh: () => Promise<void>
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function useExperimentData(
|
|
22
|
+
options: UseExperimentDataOptions = {},
|
|
23
|
+
): UseExperimentDataReturn {
|
|
24
|
+
const api = useApi({ baseUrl: options.apiBaseUrl })
|
|
25
|
+
|
|
26
|
+
const data = ref<Record<string, unknown> | null>(null)
|
|
27
|
+
const isLoading = ref(false)
|
|
28
|
+
const error = ref<string | null>(null)
|
|
29
|
+
let lastExperimentId: number | null = null
|
|
30
|
+
|
|
31
|
+
const treeData = computed<TreeNode[]>(() => {
|
|
32
|
+
if (!data.value) return []
|
|
33
|
+
const tree = data.value.tree_data ?? data.value.treeData
|
|
34
|
+
return Array.isArray(tree) ? tree as TreeNode[] : []
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const tableData = computed<Record<string, unknown>[]>(() => {
|
|
38
|
+
if (!data.value) return []
|
|
39
|
+
const table = data.value.table_data ?? data.value.tableData
|
|
40
|
+
return Array.isArray(table) ? table as Record<string, unknown>[] : []
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const summaryData = computed<SummaryData | null>(() => {
|
|
44
|
+
if (!data.value) return null
|
|
45
|
+
const summary = data.value.summary_data ?? data.value.summaryData
|
|
46
|
+
if (summary && typeof summary === 'object' && 'metadata' in (summary as Record<string, unknown>)) {
|
|
47
|
+
return summary as SummaryData
|
|
48
|
+
}
|
|
49
|
+
return null
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
async function fetchData(experimentId: number): Promise<void> {
|
|
53
|
+
lastExperimentId = experimentId
|
|
54
|
+
isLoading.value = true
|
|
55
|
+
error.value = null
|
|
56
|
+
try {
|
|
57
|
+
const result = await api.get<Record<string, unknown>>(
|
|
58
|
+
`/api/experiments/${experimentId}/data`,
|
|
59
|
+
)
|
|
60
|
+
data.value = result
|
|
61
|
+
} catch (e) {
|
|
62
|
+
error.value = e instanceof Error ? e.message : 'Failed to fetch experiment data'
|
|
63
|
+
data.value = null
|
|
64
|
+
} finally {
|
|
65
|
+
isLoading.value = false
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function refresh(): Promise<void> {
|
|
70
|
+
if (lastExperimentId !== null) {
|
|
71
|
+
await fetchData(lastExperimentId)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
data,
|
|
77
|
+
treeData,
|
|
78
|
+
tableData,
|
|
79
|
+
summaryData,
|
|
80
|
+
isLoading,
|
|
81
|
+
error,
|
|
82
|
+
fetch: fetchData,
|
|
83
|
+
refresh,
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ref, reactive, watch, type Ref } from 'vue'
|
|
1
|
+
import { ref, reactive, computed, watch, onScopeDispose, type Ref, type ComputedRef } from 'vue'
|
|
2
2
|
import { useApi } from './useApi'
|
|
3
3
|
import type { ExperimentSummary, ExperimentListResponse, ExperimentFilters } from '../types'
|
|
4
4
|
|
|
@@ -16,7 +16,11 @@ export interface UseExperimentSelectorReturn {
|
|
|
16
16
|
filters: ExperimentFilters
|
|
17
17
|
isLoading: Ref<boolean>
|
|
18
18
|
error: Ref<string | null>
|
|
19
|
+
page: Ref<number>
|
|
20
|
+
hasMore: ComputedRef<boolean>
|
|
19
21
|
fetch: () => Promise<void>
|
|
22
|
+
loadMore: () => Promise<void>
|
|
23
|
+
reset: () => void
|
|
20
24
|
select: (experiment: ExperimentSummary) => void
|
|
21
25
|
clear: () => void
|
|
22
26
|
}
|
|
@@ -32,12 +36,9 @@ export function useExperimentSelector(
|
|
|
32
36
|
const selectedExperiment = ref<ExperimentSummary | null>(null)
|
|
33
37
|
const isLoading = ref(false)
|
|
34
38
|
const error = ref<string | null>(null)
|
|
39
|
+
const page = ref(0)
|
|
35
40
|
|
|
36
|
-
const
|
|
37
|
-
search: undefined,
|
|
38
|
-
status: undefined,
|
|
39
|
-
project: undefined,
|
|
40
|
-
})
|
|
41
|
+
const hasMore = computed(() => experiments.value.length < total.value)
|
|
41
42
|
|
|
42
43
|
async function fetchExperiments(): Promise<void> {
|
|
43
44
|
isLoading.value = true
|
|
@@ -49,21 +50,42 @@ export function useExperimentSelector(
|
|
|
49
50
|
if (filters.search) params.set('search', filters.search)
|
|
50
51
|
if (filters.project) params.set('project', filters.project)
|
|
51
52
|
params.set('limit', String(limit))
|
|
53
|
+
params.set('skip', String(page.value * limit))
|
|
52
54
|
|
|
53
55
|
const query = params.toString()
|
|
54
56
|
const url = `/api/experiments${query ? `?${query}` : ''}`
|
|
55
57
|
const data = await api.get<ExperimentListResponse>(url)
|
|
56
|
-
|
|
58
|
+
|
|
59
|
+
if (page.value === 0) {
|
|
60
|
+
experiments.value = data.experiments
|
|
61
|
+
} else {
|
|
62
|
+
experiments.value = [...experiments.value, ...data.experiments]
|
|
63
|
+
}
|
|
57
64
|
total.value = data.total
|
|
58
65
|
} catch (e) {
|
|
59
66
|
error.value = e instanceof Error ? e.message : 'Failed to fetch experiments'
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
if (page.value === 0) {
|
|
68
|
+
experiments.value = []
|
|
69
|
+
total.value = 0
|
|
70
|
+
}
|
|
62
71
|
} finally {
|
|
63
72
|
isLoading.value = false
|
|
64
73
|
}
|
|
65
74
|
}
|
|
66
75
|
|
|
76
|
+
async function loadMore(): Promise<void> {
|
|
77
|
+
if (!hasMore.value || isLoading.value) return
|
|
78
|
+
page.value++
|
|
79
|
+
await fetchExperiments()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function reset(): void {
|
|
83
|
+
page.value = 0
|
|
84
|
+
experiments.value = []
|
|
85
|
+
total.value = 0
|
|
86
|
+
fetchExperiments()
|
|
87
|
+
}
|
|
88
|
+
|
|
67
89
|
function select(experiment: ExperimentSummary): void {
|
|
68
90
|
selectedExperiment.value = experiment
|
|
69
91
|
}
|
|
@@ -73,8 +95,15 @@ export function useExperimentSelector(
|
|
|
73
95
|
filters.search = undefined
|
|
74
96
|
filters.status = undefined
|
|
75
97
|
filters.project = undefined
|
|
98
|
+
page.value = 0
|
|
76
99
|
}
|
|
77
100
|
|
|
101
|
+
const filters: ExperimentFilters = reactive({
|
|
102
|
+
search: undefined,
|
|
103
|
+
status: undefined,
|
|
104
|
+
project: undefined,
|
|
105
|
+
})
|
|
106
|
+
|
|
78
107
|
// Debounced watch on search filter
|
|
79
108
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null
|
|
80
109
|
watch(
|
|
@@ -82,6 +111,7 @@ export function useExperimentSelector(
|
|
|
82
111
|
() => {
|
|
83
112
|
if (debounceTimer) clearTimeout(debounceTimer)
|
|
84
113
|
debounceTimer = setTimeout(() => {
|
|
114
|
+
page.value = 0
|
|
85
115
|
fetchExperiments()
|
|
86
116
|
}, 300)
|
|
87
117
|
},
|
|
@@ -91,10 +121,15 @@ export function useExperimentSelector(
|
|
|
91
121
|
watch(
|
|
92
122
|
() => [filters.status, filters.project],
|
|
93
123
|
() => {
|
|
124
|
+
page.value = 0
|
|
94
125
|
fetchExperiments()
|
|
95
126
|
},
|
|
96
127
|
)
|
|
97
128
|
|
|
129
|
+
onScopeDispose(() => {
|
|
130
|
+
if (debounceTimer) clearTimeout(debounceTimer)
|
|
131
|
+
})
|
|
132
|
+
|
|
98
133
|
if (immediate) {
|
|
99
134
|
fetchExperiments()
|
|
100
135
|
}
|
|
@@ -106,7 +141,11 @@ export function useExperimentSelector(
|
|
|
106
141
|
filters,
|
|
107
142
|
isLoading,
|
|
108
143
|
error,
|
|
144
|
+
page,
|
|
145
|
+
hasMore,
|
|
109
146
|
fetch: fetchExperiments,
|
|
147
|
+
loadMore,
|
|
148
|
+
reset,
|
|
110
149
|
select,
|
|
111
150
|
clear,
|
|
112
151
|
}
|
package/src/index.ts
CHANGED
|
@@ -150,6 +150,15 @@ export {
|
|
|
150
150
|
useExperimentSelector,
|
|
151
151
|
type UseExperimentSelectorOptions,
|
|
152
152
|
type UseExperimentSelectorReturn,
|
|
153
|
+
// Experiment utilities
|
|
154
|
+
formatExperimentDate,
|
|
155
|
+
EXPERIMENT_STATUS_OPTIONS,
|
|
156
|
+
EXPERIMENT_STATUS_VARIANT_MAP,
|
|
157
|
+
EXPERIMENT_STATUS_LABELS,
|
|
158
|
+
// Experiment data
|
|
159
|
+
useExperimentData,
|
|
160
|
+
type UseExperimentDataOptions,
|
|
161
|
+
type UseExperimentDataReturn,
|
|
153
162
|
} from './composables'
|
|
154
163
|
|
|
155
164
|
// Stores
|
|
@@ -7,7 +7,27 @@
|
|
|
7
7
|
color: var(--mld-color-text-secondary, #666);
|
|
8
8
|
border-radius: var(--mld-radius-sm, 4px);
|
|
9
9
|
letter-spacing: 0.02em;
|
|
10
|
+
transition: background-color 0.15s ease, color 0.15s ease;
|
|
10
11
|
}
|
|
11
12
|
.mld-exp-code--sm { padding: 1px 6px; font-size: 11px; }
|
|
12
13
|
.mld-exp-code--md { padding: 2px 8px; font-size: 12px; }
|
|
13
14
|
.mld-exp-code--lg { padding: 3px 10px; font-size: 14px; }
|
|
15
|
+
|
|
16
|
+
/* Copyable state */
|
|
17
|
+
.mld-exp-code--copyable {
|
|
18
|
+
cursor: pointer;
|
|
19
|
+
user-select: none;
|
|
20
|
+
}
|
|
21
|
+
.mld-exp-code--copyable:hover {
|
|
22
|
+
background: var(--mld-color-surface-3, #e4e4e4);
|
|
23
|
+
}
|
|
24
|
+
.mld-exp-code--copyable:focus-visible {
|
|
25
|
+
outline: 2px solid var(--color-primary, #3b82f6);
|
|
26
|
+
outline-offset: 1px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Copied feedback */
|
|
30
|
+
.mld-exp-code--copied {
|
|
31
|
+
background: var(--mld-color-success-soft, #dcfce7);
|
|
32
|
+
color: var(--mld-color-success, #16a34a);
|
|
33
|
+
}
|
|
@@ -19,7 +19,14 @@
|
|
|
19
19
|
max-height: 600px;
|
|
20
20
|
overflow-y: auto;
|
|
21
21
|
}
|
|
22
|
-
.mld-data-viewer__loading
|
|
22
|
+
.mld-data-viewer__loading {
|
|
23
|
+
padding: 24px 12px;
|
|
24
|
+
}
|
|
25
|
+
.mld-data-viewer__skeleton {
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
gap: 12px;
|
|
29
|
+
}
|
|
23
30
|
.mld-data-viewer__empty {
|
|
24
31
|
text-align: center;
|
|
25
32
|
padding: 32px;
|
|
@@ -22,11 +22,28 @@
|
|
|
22
22
|
width: 10rem;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/* Loading */
|
|
26
|
-
.mld-experiment-
|
|
25
|
+
/* Loading skeleton */
|
|
26
|
+
.mld-experiment-selector__skeleton {
|
|
27
27
|
display: flex;
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
gap: 1px;
|
|
30
|
+
border: 1px solid var(--border-color);
|
|
31
|
+
border-radius: var(--mld-radius);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.mld-experiment-selector__skeleton-row {
|
|
35
|
+
display: flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
gap: 0.75rem;
|
|
38
|
+
padding: 0.75rem 1rem;
|
|
39
|
+
background-color: var(--bg-primary);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.mld-experiment-selector__skeleton-content {
|
|
43
|
+
flex: 1;
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
gap: 6px;
|
|
30
47
|
}
|
|
31
48
|
|
|
32
49
|
/* Error */
|
|
@@ -76,6 +93,18 @@
|
|
|
76
93
|
background-color: var(--color-primary-soft);
|
|
77
94
|
}
|
|
78
95
|
|
|
96
|
+
/* Keyboard focused row */
|
|
97
|
+
.mld-experiment-selector__row--focused {
|
|
98
|
+
background-color: var(--bg-hover);
|
|
99
|
+
outline: 2px solid var(--color-primary, #3b82f6);
|
|
100
|
+
outline-offset: -2px;
|
|
101
|
+
border-radius: 2px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.mld-experiment-selector__row--active.mld-experiment-selector__row--focused {
|
|
105
|
+
background-color: var(--color-primary-soft);
|
|
106
|
+
}
|
|
107
|
+
|
|
79
108
|
/* Row content */
|
|
80
109
|
.mld-experiment-selector__row-content {
|
|
81
110
|
flex: 1;
|
|
@@ -83,9 +112,15 @@
|
|
|
83
112
|
}
|
|
84
113
|
|
|
85
114
|
.mld-experiment-selector__name {
|
|
115
|
+
display: flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
gap: 0.5rem;
|
|
86
118
|
font-size: 0.875rem;
|
|
87
119
|
font-weight: 500;
|
|
88
120
|
color: var(--text-primary);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.mld-experiment-selector__name > span:first-child {
|
|
89
124
|
white-space: nowrap;
|
|
90
125
|
overflow: hidden;
|
|
91
126
|
text-overflow: ellipsis;
|