@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.
Files changed (41) hide show
  1. package/dist/components/ExperimentCodeBadge.vue.d.ts +7 -1
  2. package/dist/components/ExperimentCodeBadge.vue.js +41 -5
  3. package/dist/components/ExperimentCodeBadge.vue.js.map +1 -1
  4. package/dist/components/ExperimentDataViewer.vue.d.ts +1 -1
  5. package/dist/components/ExperimentDataViewer.vue.js +98 -44
  6. package/dist/components/ExperimentDataViewer.vue.js.map +1 -1
  7. package/dist/components/ExperimentSelectorModal.vue.d.ts +3 -1
  8. package/dist/components/ExperimentSelectorModal.vue.js +117 -63
  9. package/dist/components/ExperimentSelectorModal.vue.js.map +1 -1
  10. package/dist/composables/experiment-utils.d.ts +5 -0
  11. package/dist/composables/experiment-utils.js +34 -0
  12. package/dist/composables/experiment-utils.js.map +1 -0
  13. package/dist/composables/index.d.ts +2 -0
  14. package/dist/composables/index.js +7 -0
  15. package/dist/composables/index.js.map +1 -1
  16. package/dist/composables/useExperimentData.d.ts +17 -0
  17. package/dist/composables/useExperimentData.js +62 -0
  18. package/dist/composables/useExperimentData.js.map +1 -0
  19. package/dist/composables/useExperimentSelector.d.ts +5 -1
  20. package/dist/composables/useExperimentSelector.js +39 -9
  21. package/dist/composables/useExperimentSelector.js.map +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +7 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/styles.css +121 -10
  26. package/package.json +1 -1
  27. package/src/components/ExperimentCodeBadge.story.vue +77 -0
  28. package/src/components/ExperimentCodeBadge.vue +46 -3
  29. package/src/components/ExperimentDataViewer.story.vue +174 -0
  30. package/src/components/ExperimentDataViewer.vue +49 -12
  31. package/src/components/ExperimentSelectorModal.story.vue +244 -0
  32. package/src/components/ExperimentSelectorModal.vue +75 -37
  33. package/src/components/FitPanel.story.vue +125 -0
  34. package/src/composables/experiment-utils.ts +32 -0
  35. package/src/composables/index.ts +11 -0
  36. package/src/composables/useExperimentData.ts +85 -0
  37. package/src/composables/useExperimentSelector.ts +48 -9
  38. package/src/index.ts +9 -0
  39. package/src/styles/components/experiment-code-badge.css +20 -0
  40. package/src/styles/components/experiment-data-viewer.css +8 -1
  41. 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
+ }
@@ -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 filters: ExperimentFilters = reactive({
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
- experiments.value = data.experiments
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
- experiments.value = []
61
- total.value = 0
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-selector__loading {
25
+ /* Loading skeleton */
26
+ .mld-experiment-selector__skeleton {
27
27
  display: flex;
28
- justify-content: center;
29
- padding: 2rem 0;
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;