@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
@@ -1,38 +1,37 @@
1
- import { defineComponent, watch, openBlock, createBlock, withCtx, createElementVNode, createVNode, unref, createElementBlock, toDisplayString, Fragment, renderList, normalizeClass, createCommentVNode, createTextVNode } from "vue";
1
+ import { defineComponent, ref, watch, openBlock, createBlock, withCtx, createElementVNode, createVNode, unref, createElementBlock, Fragment, renderList, toDisplayString, normalizeClass, createTextVNode, createCommentVNode, nextTick } from "vue";
2
2
  import { useExperimentSelector } from "../composables/useExperimentSelector.js";
3
+ import { EXPERIMENT_STATUS_OPTIONS, formatExperimentDate, EXPERIMENT_STATUS_VARIANT_MAP } from "../composables/experiment-utils.js";
3
4
  import _sfc_main$1 from "./BaseModal.vue.js";
4
5
  /* empty css */
5
6
  import _sfc_main$2 from "./BaseInput.vue.js";
6
7
  /* empty css */
7
8
  import _sfc_main$3 from "./BaseSelect.vue.js";
8
9
  /* empty css */
9
- import _sfc_main$6 from "./BasePill.vue.js";
10
+ import _sfc_main$7 from "./BasePill.vue.js";
11
+ /* empty css */
12
+ import _sfc_main$4 from "./Skeleton.vue.js";
10
13
  /* empty css */
11
- import _sfc_main$4 from "./LoadingSpinner.vue.js";
12
- /* empty css */
13
14
  import _sfc_main$5 from "./EmptyState.vue.js";
14
15
  /* empty css */
15
- const _hoisted_1 = { class: "mld-experiment-selector" };
16
- const _hoisted_2 = { class: "mld-experiment-selector__filters" };
17
- const _hoisted_3 = { class: "mld-experiment-selector__search" };
18
- const _hoisted_4 = { class: "mld-experiment-selector__status-filter" };
19
- const _hoisted_5 = {
16
+ import _sfc_main$6 from "./ExperimentCodeBadge.vue.js";
17
+ /* empty css */
18
+ const _hoisted_1 = { class: "mld-experiment-selector__filters" };
19
+ const _hoisted_2 = { class: "mld-experiment-selector__search" };
20
+ const _hoisted_3 = { class: "mld-experiment-selector__status-filter" };
21
+ const _hoisted_4 = {
20
22
  key: 0,
21
- class: "mld-experiment-selector__loading"
23
+ class: "mld-experiment-selector__skeleton"
22
24
  };
25
+ const _hoisted_5 = { class: "mld-experiment-selector__skeleton-content" };
23
26
  const _hoisted_6 = {
24
27
  key: 1,
25
28
  class: "mld-experiment-selector__error"
26
29
  };
27
- const _hoisted_7 = {
28
- key: 3,
29
- class: "mld-experiment-selector__list"
30
- };
31
- const _hoisted_8 = ["onClick"];
32
- const _hoisted_9 = { class: "mld-experiment-selector__row-content" };
33
- const _hoisted_10 = { class: "mld-experiment-selector__name" };
34
- const _hoisted_11 = { class: "mld-experiment-selector__meta" };
35
- const _hoisted_12 = { key: 0 };
30
+ const _hoisted_7 = ["onClick", "onMouseenter"];
31
+ const _hoisted_8 = { class: "mld-experiment-selector__row-content" };
32
+ const _hoisted_9 = { class: "mld-experiment-selector__name" };
33
+ const _hoisted_10 = { class: "mld-experiment-selector__meta" };
34
+ const _hoisted_11 = { key: 0 };
36
35
  const _sfc_main = /* @__PURE__ */ defineComponent({
37
36
  __name: "ExperimentSelectorModal",
38
37
  props: {
@@ -40,7 +39,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
40
39
  experimentType: {},
41
40
  currentExperimentId: { default: null },
42
41
  title: { default: "Select Experiment" },
43
- size: { default: "md" }
42
+ size: { default: "full" }
44
43
  },
45
44
  emits: ["update:modelValue", "select"],
46
45
  setup(__props, { emit: __emit }) {
@@ -55,17 +54,8 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
55
54
  } = useExperimentSelector({
56
55
  experimentType: props.experimentType
57
56
  });
58
- const statusOptions = [
59
- { value: "", label: "All statuses" },
60
- { value: "planned", label: "Planned" },
61
- { value: "ongoing", label: "Ongoing" },
62
- { value: "completed", label: "Completed" }
63
- ];
64
- const statusVariantMap = {
65
- planned: "default",
66
- ongoing: "primary",
67
- completed: "success"
68
- };
57
+ const activeIndex = ref(-1);
58
+ const listRef = ref(null);
69
59
  function handleStatusChange(value) {
70
60
  filters.status = String(value) || null;
71
61
  }
@@ -73,21 +63,44 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
73
63
  emit("select", experiment);
74
64
  emit("update:modelValue", false);
75
65
  }
76
- function formatDate(dateStr) {
77
- try {
78
- return new Date(dateStr).toLocaleDateString(void 0, {
79
- year: "numeric",
80
- month: "short",
81
- day: "numeric"
82
- });
83
- } catch {
84
- return dateStr;
66
+ function handleKeydown(event) {
67
+ if (!experiments.value.length) return;
68
+ switch (event.key) {
69
+ case "ArrowDown":
70
+ event.preventDefault();
71
+ activeIndex.value = Math.min(activeIndex.value + 1, experiments.value.length - 1);
72
+ scrollActiveIntoView();
73
+ break;
74
+ case "ArrowUp":
75
+ event.preventDefault();
76
+ activeIndex.value = Math.max(activeIndex.value - 1, 0);
77
+ scrollActiveIntoView();
78
+ break;
79
+ case "Enter":
80
+ event.preventDefault();
81
+ if (activeIndex.value >= 0 && activeIndex.value < experiments.value.length) {
82
+ handleSelect(experiments.value[activeIndex.value]);
83
+ }
84
+ break;
85
85
  }
86
86
  }
87
+ function scrollActiveIntoView() {
88
+ nextTick(() => {
89
+ var _a;
90
+ const row = (_a = listRef.value) == null ? void 0 : _a.querySelector(".mld-experiment-selector__row--focused");
91
+ row == null ? void 0 : row.scrollIntoView({ block: "nearest" });
92
+ });
93
+ }
94
+ watch(experiments, () => {
95
+ activeIndex.value = -1;
96
+ });
87
97
  watch(
88
98
  () => props.modelValue,
89
99
  (isOpen) => {
90
- if (isOpen) fetchExperiments();
100
+ if (isOpen) {
101
+ activeIndex.value = -1;
102
+ fetchExperiments();
103
+ }
91
104
  }
92
105
  );
93
106
  return (_ctx, _cache) => {
@@ -98,9 +111,12 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
98
111
  "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => emit("update:modelValue", $event))
99
112
  }, {
100
113
  default: withCtx(() => [
101
- createElementVNode("div", _hoisted_1, [
102
- createElementVNode("div", _hoisted_2, [
103
- createElementVNode("div", _hoisted_3, [
114
+ createElementVNode("div", {
115
+ class: "mld-experiment-selector",
116
+ onKeydown: handleKeydown
117
+ }, [
118
+ createElementVNode("div", _hoisted_1, [
119
+ createElementVNode("div", _hoisted_2, [
104
120
  createVNode(_sfc_main$2, {
105
121
  modelValue: unref(filters).search,
106
122
  "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => unref(filters).search = $event),
@@ -109,38 +125,76 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
109
125
  type: "search"
110
126
  }, null, 8, ["modelValue"])
111
127
  ]),
112
- createElementVNode("div", _hoisted_4, [
128
+ createElementVNode("div", _hoisted_3, [
113
129
  createVNode(_sfc_main$3, {
114
130
  "model-value": unref(filters).status ?? "",
115
- options: statusOptions,
131
+ options: unref(EXPERIMENT_STATUS_OPTIONS),
116
132
  size: "sm",
117
133
  "onUpdate:modelValue": handleStatusChange
118
- }, null, 8, ["model-value"])
134
+ }, null, 8, ["model-value", "options"])
119
135
  ])
120
136
  ]),
121
- unref(isLoading) ? (openBlock(), createElementBlock("div", _hoisted_5, [
122
- createVNode(_sfc_main$4, { size: "md" })
137
+ unref(isLoading) ? (openBlock(), createElementBlock("div", _hoisted_4, [
138
+ (openBlock(), createElementBlock(Fragment, null, renderList(4, (n) => {
139
+ return createElementVNode("div", {
140
+ key: n,
141
+ class: "mld-experiment-selector__skeleton-row"
142
+ }, [
143
+ createElementVNode("div", _hoisted_5, [
144
+ createVNode(_sfc_main$4, {
145
+ width: 120 + n * 20,
146
+ height: "14px"
147
+ }, null, 8, ["width"]),
148
+ createVNode(_sfc_main$4, {
149
+ width: "80px",
150
+ height: "10px"
151
+ })
152
+ ]),
153
+ createVNode(_sfc_main$4, {
154
+ width: "60px",
155
+ height: "20px",
156
+ variant: "rounded"
157
+ })
158
+ ]);
159
+ }), 64))
123
160
  ])) : unref(error) ? (openBlock(), createElementBlock("div", _hoisted_6, toDisplayString(unref(error)), 1)) : unref(experiments).length === 0 ? (openBlock(), createBlock(_sfc_main$5, {
124
161
  key: 2,
125
162
  title: "No experiments found",
126
163
  description: "Try adjusting your search or filters.",
127
164
  size: "sm"
128
- })) : (openBlock(), createElementBlock("div", _hoisted_7, [
129
- (openBlock(true), createElementBlock(Fragment, null, renderList(unref(experiments), (exp) => {
165
+ })) : (openBlock(), createElementBlock("div", {
166
+ key: 3,
167
+ ref_key: "listRef",
168
+ ref: listRef,
169
+ class: "mld-experiment-selector__list"
170
+ }, [
171
+ (openBlock(true), createElementBlock(Fragment, null, renderList(unref(experiments), (exp, idx) => {
130
172
  return openBlock(), createElementBlock("div", {
131
173
  key: exp.id,
132
- class: normalizeClass(["mld-experiment-selector__row", { "mld-experiment-selector__row--active": exp.id === __props.currentExperimentId }]),
133
- onClick: ($event) => handleSelect(exp)
174
+ class: normalizeClass(["mld-experiment-selector__row", {
175
+ "mld-experiment-selector__row--active": exp.id === __props.currentExperimentId,
176
+ "mld-experiment-selector__row--focused": idx === activeIndex.value
177
+ }]),
178
+ onClick: ($event) => handleSelect(exp),
179
+ onMouseenter: ($event) => activeIndex.value = idx
134
180
  }, [
135
- createElementVNode("div", _hoisted_9, [
136
- createElementVNode("div", _hoisted_10, toDisplayString(exp.name), 1),
137
- createElementVNode("div", _hoisted_11, [
138
- exp.project ? (openBlock(), createElementBlock("span", _hoisted_12, toDisplayString(exp.project), 1)) : createCommentVNode("", true),
139
- createElementVNode("span", null, toDisplayString(formatDate(exp.created_at)), 1)
181
+ createElementVNode("div", _hoisted_8, [
182
+ createElementVNode("div", _hoisted_9, [
183
+ createTextVNode(toDisplayString(exp.name) + " ", 1),
184
+ exp.experiment_code ? (openBlock(), createBlock(_sfc_main$6, {
185
+ key: 0,
186
+ code: exp.experiment_code,
187
+ size: "sm",
188
+ copyable: false
189
+ }, null, 8, ["code"])) : createCommentVNode("", true)
190
+ ]),
191
+ createElementVNode("div", _hoisted_10, [
192
+ exp.project ? (openBlock(), createElementBlock("span", _hoisted_11, toDisplayString(exp.project), 1)) : createCommentVNode("", true),
193
+ createElementVNode("span", null, toDisplayString(unref(formatExperimentDate)(exp.created_at)), 1)
140
194
  ])
141
195
  ]),
142
- createVNode(_sfc_main$6, {
143
- variant: statusVariantMap[exp.status],
196
+ createVNode(_sfc_main$7, {
197
+ variant: unref(EXPERIMENT_STATUS_VARIANT_MAP)[exp.status],
144
198
  size: "sm"
145
199
  }, {
146
200
  default: withCtx(() => [
@@ -148,10 +202,10 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
148
202
  ]),
149
203
  _: 2
150
204
  }, 1032, ["variant"])
151
- ], 10, _hoisted_8);
205
+ ], 42, _hoisted_7);
152
206
  }), 128))
153
- ]))
154
- ])
207
+ ], 512))
208
+ ], 32)
155
209
  ]),
156
210
  _: 1
157
211
  }, 8, ["model-value", "title", "size"]);
@@ -1 +1 @@
1
- {"version":3,"file":"ExperimentSelectorModal.vue.js","sources":["../../src/components/ExperimentSelectorModal.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { watch } from 'vue'\nimport type { ModalSize, ExperimentSummary, ExperimentStatus, SelectOption } from '../types'\nimport { useExperimentSelector } from '../composables/useExperimentSelector'\nimport BaseModal from './BaseModal.vue'\nimport BaseInput from './BaseInput.vue'\nimport BaseSelect from './BaseSelect.vue'\nimport BasePill from './BasePill.vue'\nimport LoadingSpinner from './LoadingSpinner.vue'\nimport EmptyState from './EmptyState.vue'\n\ninterface Props {\n modelValue: boolean\n experimentType?: string\n currentExperimentId?: number | null\n title?: string\n size?: ModalSize\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n currentExperimentId: null,\n title: 'Select Experiment',\n size: 'md',\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: boolean]\n select: [experiment: ExperimentSummary]\n}>()\n\nconst {\n experiments,\n filters,\n isLoading,\n error,\n fetch: fetchExperiments,\n} = useExperimentSelector({\n experimentType: props.experimentType,\n})\n\nconst statusOptions: SelectOption<string>[] = [\n { value: '', label: 'All statuses' },\n { value: 'planned', label: 'Planned' },\n { value: 'ongoing', label: 'Ongoing' },\n { value: 'completed', label: 'Completed' },\n]\n\nconst statusVariantMap: Record<ExperimentStatus, string> = {\n planned: 'default',\n ongoing: 'primary',\n completed: 'success',\n}\n\nfunction handleStatusChange(value: string | number) {\n filters.status = (String(value) || null) as ExperimentStatus | null\n}\n\nfunction handleSelect(experiment: ExperimentSummary) {\n emit('select', experiment)\n emit('update:modelValue', false)\n}\n\nfunction formatDate(dateStr: string): string {\n try {\n return new Date(dateStr).toLocaleDateString(undefined, {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n })\n } catch {\n return dateStr\n }\n}\n\n// Fetch on open\nwatch(\n () => props.modelValue,\n (isOpen) => {\n if (isOpen) fetchExperiments()\n },\n)\n</script>\n\n<template>\n <BaseModal\n :model-value=\"modelValue\"\n :title=\"title\"\n :size=\"size\"\n @update:model-value=\"emit('update:modelValue', $event)\"\n >\n <div class=\"mld-experiment-selector\">\n <!-- Filter bar -->\n <div class=\"mld-experiment-selector__filters\">\n <div class=\"mld-experiment-selector__search\">\n <BaseInput\n v-model=\"filters.search\"\n placeholder=\"Search experiments...\"\n size=\"sm\"\n type=\"search\"\n />\n </div>\n <div class=\"mld-experiment-selector__status-filter\">\n <BaseSelect\n :model-value=\"filters.status ?? ''\"\n :options=\"statusOptions\"\n size=\"sm\"\n @update:model-value=\"handleStatusChange\"\n />\n </div>\n </div>\n\n <!-- Loading -->\n <div v-if=\"isLoading\" class=\"mld-experiment-selector__loading\">\n <LoadingSpinner size=\"md\" />\n </div>\n\n <!-- Error -->\n <div v-else-if=\"error\" class=\"mld-experiment-selector__error\">\n {{ error }}\n </div>\n\n <!-- Empty -->\n <EmptyState\n v-else-if=\"experiments.length === 0\"\n title=\"No experiments found\"\n description=\"Try adjusting your search or filters.\"\n size=\"sm\"\n />\n\n <!-- Experiment list -->\n <div v-else class=\"mld-experiment-selector__list\">\n <div\n v-for=\"exp in experiments\"\n :key=\"exp.id\"\n class=\"mld-experiment-selector__row\"\n :class=\"{ 'mld-experiment-selector__row--active': exp.id === currentExperimentId }\"\n @click=\"handleSelect(exp)\"\n >\n <div class=\"mld-experiment-selector__row-content\">\n <div class=\"mld-experiment-selector__name\">{{ exp.name }}</div>\n <div class=\"mld-experiment-selector__meta\">\n <span v-if=\"exp.project\">{{ exp.project }}</span>\n <span>{{ formatDate(exp.created_at) }}</span>\n </div>\n </div>\n <BasePill :variant=\"statusVariantMap[exp.status] as any\" size=\"sm\">\n {{ exp.status }}\n </BasePill>\n </div>\n </div>\n </div>\n </BaseModal>\n</template>\n\n<style>\n@import '../styles/components/experiment-selector-modal.css';\n</style>\n"],"names":["_createBlock","BaseModal","_createElementVNode","_createVNode","BaseInput","_unref","BaseSelect","_openBlock","_createElementBlock","LoadingSpinner","_toDisplayString","EmptyState","_Fragment","_renderList","BasePill","_createTextVNode"],"mappings":"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"}
1
+ {"version":3,"file":"ExperimentSelectorModal.vue.js","sources":["../../src/components/ExperimentSelectorModal.vue"],"sourcesContent":["<script setup lang=\"ts\">\nimport { ref, watch, nextTick } from 'vue'\nimport type { ModalSize, ExperimentSummary, ExperimentStatus } from '../types'\nimport { useExperimentSelector } from '../composables/useExperimentSelector'\nimport {\n formatExperimentDate,\n EXPERIMENT_STATUS_OPTIONS,\n EXPERIMENT_STATUS_VARIANT_MAP,\n} from '../composables/experiment-utils'\nimport BaseModal from './BaseModal.vue'\nimport BaseInput from './BaseInput.vue'\nimport BaseSelect from './BaseSelect.vue'\nimport BasePill from './BasePill.vue'\nimport Skeleton from './Skeleton.vue'\nimport EmptyState from './EmptyState.vue'\nimport ExperimentCodeBadge from './ExperimentCodeBadge.vue'\n\ninterface Props {\n modelValue: boolean\n experimentType?: string\n currentExperimentId?: number | null\n title?: string\n size?: ModalSize\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n currentExperimentId: null,\n title: 'Select Experiment',\n size: 'full',\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: boolean]\n select: [experiment: ExperimentSummary]\n}>()\n\nconst {\n experiments,\n filters,\n isLoading,\n error,\n fetch: fetchExperiments,\n} = useExperimentSelector({\n experimentType: props.experimentType,\n})\n\nconst activeIndex = ref(-1)\nconst listRef = ref<HTMLElement | null>(null)\n\nfunction handleStatusChange(value: string | number) {\n filters.status = (String(value) || null) as ExperimentStatus | null\n}\n\nfunction handleSelect(experiment: ExperimentSummary) {\n emit('select', experiment)\n emit('update:modelValue', false)\n}\n\nfunction handleKeydown(event: KeyboardEvent) {\n if (!experiments.value.length) return\n\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault()\n activeIndex.value = Math.min(activeIndex.value + 1, experiments.value.length - 1)\n scrollActiveIntoView()\n break\n case 'ArrowUp':\n event.preventDefault()\n activeIndex.value = Math.max(activeIndex.value - 1, 0)\n scrollActiveIntoView()\n break\n case 'Enter':\n event.preventDefault()\n if (activeIndex.value >= 0 && activeIndex.value < experiments.value.length) {\n handleSelect(experiments.value[activeIndex.value])\n }\n break\n }\n}\n\nfunction scrollActiveIntoView() {\n nextTick(() => {\n const row = listRef.value?.querySelector('.mld-experiment-selector__row--focused')\n row?.scrollIntoView({ block: 'nearest' })\n })\n}\n\n// Reset active index when experiments change\nwatch(experiments, () => { activeIndex.value = -1 })\n\n// Fetch on open\nwatch(\n () => props.modelValue,\n (isOpen) => {\n if (isOpen) {\n activeIndex.value = -1\n fetchExperiments()\n }\n },\n)\n</script>\n\n<template>\n <BaseModal\n :model-value=\"modelValue\"\n :title=\"title\"\n :size=\"size\"\n @update:model-value=\"emit('update:modelValue', $event)\"\n >\n <div class=\"mld-experiment-selector\" @keydown=\"handleKeydown\">\n <!-- Filter bar -->\n <div class=\"mld-experiment-selector__filters\">\n <div class=\"mld-experiment-selector__search\">\n <BaseInput\n v-model=\"filters.search\"\n placeholder=\"Search experiments...\"\n size=\"sm\"\n type=\"search\"\n />\n </div>\n <div class=\"mld-experiment-selector__status-filter\">\n <BaseSelect\n :model-value=\"filters.status ?? ''\"\n :options=\"EXPERIMENT_STATUS_OPTIONS\"\n size=\"sm\"\n @update:model-value=\"handleStatusChange\"\n />\n </div>\n </div>\n\n <!-- Loading skeleton -->\n <div v-if=\"isLoading\" class=\"mld-experiment-selector__skeleton\">\n <div v-for=\"n in 4\" :key=\"n\" class=\"mld-experiment-selector__skeleton-row\">\n <div class=\"mld-experiment-selector__skeleton-content\">\n <Skeleton :width=\"120 + n * 20\" height=\"14px\" />\n <Skeleton width=\"80px\" height=\"10px\" />\n </div>\n <Skeleton width=\"60px\" height=\"20px\" variant=\"rounded\" />\n </div>\n </div>\n\n <!-- Error -->\n <div v-else-if=\"error\" class=\"mld-experiment-selector__error\">\n {{ error }}\n </div>\n\n <!-- Empty -->\n <EmptyState\n v-else-if=\"experiments.length === 0\"\n title=\"No experiments found\"\n description=\"Try adjusting your search or filters.\"\n size=\"sm\"\n />\n\n <!-- Experiment list -->\n <div v-else ref=\"listRef\" class=\"mld-experiment-selector__list\">\n <div\n v-for=\"(exp, idx) in experiments\"\n :key=\"exp.id\"\n class=\"mld-experiment-selector__row\"\n :class=\"{\n 'mld-experiment-selector__row--active': exp.id === currentExperimentId,\n 'mld-experiment-selector__row--focused': idx === activeIndex,\n }\"\n @click=\"handleSelect(exp)\"\n @mouseenter=\"activeIndex = idx\"\n >\n <div class=\"mld-experiment-selector__row-content\">\n <div class=\"mld-experiment-selector__name\">\n {{ exp.name }}\n <ExperimentCodeBadge\n v-if=\"exp.experiment_code\"\n :code=\"exp.experiment_code\"\n size=\"sm\"\n :copyable=\"false\"\n />\n </div>\n <div class=\"mld-experiment-selector__meta\">\n <span v-if=\"exp.project\">{{ exp.project }}</span>\n <span>{{ formatExperimentDate(exp.created_at) }}</span>\n </div>\n </div>\n <BasePill :variant=\"EXPERIMENT_STATUS_VARIANT_MAP[exp.status]\" size=\"sm\">\n {{ exp.status }}\n </BasePill>\n </div>\n </div>\n </div>\n </BaseModal>\n</template>\n\n<style>\n@import '../styles/components/experiment-selector-modal.css';\n</style>\n"],"names":["_createBlock","BaseModal","_createElementVNode","_createVNode","BaseInput","_unref","BaseSelect","_openBlock","_createElementBlock","_Fragment","_renderList","Skeleton","_toDisplayString","EmptyState","ExperimentCodeBadge","BasePill","_createTextVNode"],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import { ExperimentStatus, SelectOption, PillVariant } from '../types';
2
+ export declare function formatExperimentDate(dateStr: string): string;
3
+ export declare const EXPERIMENT_STATUS_OPTIONS: SelectOption<string>[];
4
+ export declare const EXPERIMENT_STATUS_VARIANT_MAP: Record<ExperimentStatus, PillVariant>;
5
+ export declare const EXPERIMENT_STATUS_LABELS: Record<ExperimentStatus, string>;
@@ -0,0 +1,34 @@
1
+ function formatExperimentDate(dateStr) {
2
+ try {
3
+ return new Date(dateStr).toLocaleDateString(void 0, {
4
+ year: "numeric",
5
+ month: "short",
6
+ day: "numeric"
7
+ });
8
+ } catch {
9
+ return dateStr;
10
+ }
11
+ }
12
+ const EXPERIMENT_STATUS_OPTIONS = [
13
+ { value: "", label: "All statuses" },
14
+ { value: "planned", label: "Planned" },
15
+ { value: "ongoing", label: "Ongoing" },
16
+ { value: "completed", label: "Completed" }
17
+ ];
18
+ const EXPERIMENT_STATUS_VARIANT_MAP = {
19
+ planned: "default",
20
+ ongoing: "primary",
21
+ completed: "success"
22
+ };
23
+ const EXPERIMENT_STATUS_LABELS = {
24
+ planned: "Planned",
25
+ ongoing: "Ongoing",
26
+ completed: "Completed"
27
+ };
28
+ export {
29
+ EXPERIMENT_STATUS_LABELS,
30
+ EXPERIMENT_STATUS_OPTIONS,
31
+ EXPERIMENT_STATUS_VARIANT_MAP,
32
+ formatExperimentDate
33
+ };
34
+ //# sourceMappingURL=experiment-utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"experiment-utils.js","sources":["../../src/composables/experiment-utils.ts"],"sourcesContent":["import type { ExperimentStatus, SelectOption, PillVariant } from '../types'\n\nexport function formatExperimentDate(dateStr: string): string {\n try {\n return new Date(dateStr).toLocaleDateString(undefined, {\n year: 'numeric',\n month: 'short',\n day: 'numeric',\n })\n } catch {\n return dateStr\n }\n}\n\nexport const EXPERIMENT_STATUS_OPTIONS: SelectOption<string>[] = [\n { value: '', label: 'All statuses' },\n { value: 'planned', label: 'Planned' },\n { value: 'ongoing', label: 'Ongoing' },\n { value: 'completed', label: 'Completed' },\n]\n\nexport const EXPERIMENT_STATUS_VARIANT_MAP: Record<ExperimentStatus, PillVariant> = {\n planned: 'default',\n ongoing: 'primary',\n completed: 'success',\n}\n\nexport const EXPERIMENT_STATUS_LABELS: Record<ExperimentStatus, string> = {\n planned: 'Planned',\n ongoing: 'Ongoing',\n completed: 'Completed',\n}\n"],"names":[],"mappings":"AAEO,SAAS,qBAAqB,SAAyB;AAC5D,MAAI;AACF,WAAO,IAAI,KAAK,OAAO,EAAE,mBAAmB,QAAW;AAAA,MACrD,MAAM;AAAA,MACN,OAAO;AAAA,MACP,KAAK;AAAA,IAAA,CACN;AAAA,EACH,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,MAAM,4BAAoD;AAAA,EAC/D,EAAE,OAAO,IAAI,OAAO,eAAA;AAAA,EACpB,EAAE,OAAO,WAAW,OAAO,UAAA;AAAA,EAC3B,EAAE,OAAO,WAAW,OAAO,UAAA;AAAA,EAC3B,EAAE,OAAO,aAAa,OAAO,YAAA;AAC/B;AAEO,MAAM,gCAAuE;AAAA,EAClF,SAAS;AAAA,EACT,SAAS;AAAA,EACT,WAAW;AACb;AAEO,MAAM,2BAA6D;AAAA,EACxE,SAAS;AAAA,EACT,SAAS;AAAA,EACT,WAAW;AACb;"}
@@ -19,4 +19,6 @@ export { useFormBuilder, evaluateCondition } from './useFormBuilder';
19
19
  export { useAutoGroup, DEFAULT_COLORS } from './useAutoGroup';
20
20
  export { usePluginConfig, type UsePluginConfigReturn } from './usePluginConfig';
21
21
  export { useExperimentSelector, type UseExperimentSelectorOptions, type UseExperimentSelectorReturn, } from './useExperimentSelector';
22
+ export { formatExperimentDate, EXPERIMENT_STATUS_OPTIONS, EXPERIMENT_STATUS_VARIANT_MAP, EXPERIMENT_STATUS_LABELS, } from './experiment-utils';
23
+ export { useExperimentData, type UseExperimentDataOptions, type UseExperimentDataReturn, } from './useExperimentData';
22
24
  export { getFieldRegistryEntry, getTypeDefault, type RegistryEntry, } from './formBuilderRegistry';
@@ -19,16 +19,22 @@ import { evaluateCondition, useFormBuilder } from "./useFormBuilder.js";
19
19
  import { DEFAULT_COLORS, useAutoGroup } from "./useAutoGroup.js";
20
20
  import { usePluginConfig } from "./usePluginConfig.js";
21
21
  import { useExperimentSelector } from "./useExperimentSelector.js";
22
+ import { EXPERIMENT_STATUS_LABELS, EXPERIMENT_STATUS_OPTIONS, EXPERIMENT_STATUS_VARIANT_MAP, formatExperimentDate } from "./experiment-utils.js";
23
+ import { useExperimentData } from "./useExperimentData.js";
22
24
  import { getFieldRegistryEntry, getTypeDefault } from "./formBuilderRegistry.js";
23
25
  export {
24
26
  ATOMIC_WEIGHTS,
25
27
  DEFAULT_COLORS,
28
+ EXPERIMENT_STATUS_LABELS,
29
+ EXPERIMENT_STATUS_OPTIONS,
30
+ EXPERIMENT_STATUS_VARIANT_MAP,
26
31
  addMinutes,
27
32
  compareTime,
28
33
  durationMinutes,
29
34
  evaluateCondition,
30
35
  findAvailableSlots,
31
36
  formatDuration,
37
+ formatExperimentDate,
32
38
  formatTime,
33
39
  generateTimeSlots,
34
40
  getFieldRegistryEntry,
@@ -45,6 +51,7 @@ export {
45
51
  useChemicalFormula,
46
52
  useConcentrationUnits,
47
53
  useDoseCalculator,
54
+ useExperimentData,
48
55
  useExperimentSelector,
49
56
  useForm,
50
57
  useFormBuilder,
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -0,0 +1,17 @@
1
+ import { Ref, ComputedRef } from 'vue';
2
+ import { TreeNode, SummaryData } from '../types';
3
+ export interface UseExperimentDataOptions {
4
+ apiBaseUrl?: string;
5
+ immediate?: boolean;
6
+ }
7
+ export interface UseExperimentDataReturn {
8
+ data: Ref<Record<string, unknown> | null>;
9
+ treeData: ComputedRef<TreeNode[]>;
10
+ tableData: ComputedRef<Record<string, unknown>[]>;
11
+ summaryData: ComputedRef<SummaryData | null>;
12
+ isLoading: Ref<boolean>;
13
+ error: Ref<string | null>;
14
+ fetch: (experimentId: number) => Promise<void>;
15
+ refresh: () => Promise<void>;
16
+ }
17
+ export declare function useExperimentData(options?: UseExperimentDataOptions): UseExperimentDataReturn;
@@ -0,0 +1,62 @@
1
+ import { ref, computed } from "vue";
2
+ import { useApi } from "./useApi.js";
3
+ function useExperimentData(options = {}) {
4
+ const api = useApi({ baseUrl: options.apiBaseUrl });
5
+ const data = ref(null);
6
+ const isLoading = ref(false);
7
+ const error = ref(null);
8
+ let lastExperimentId = null;
9
+ const treeData = computed(() => {
10
+ if (!data.value) return [];
11
+ const tree = data.value.tree_data ?? data.value.treeData;
12
+ return Array.isArray(tree) ? tree : [];
13
+ });
14
+ const tableData = computed(() => {
15
+ if (!data.value) return [];
16
+ const table = data.value.table_data ?? data.value.tableData;
17
+ return Array.isArray(table) ? table : [];
18
+ });
19
+ const summaryData = computed(() => {
20
+ if (!data.value) return null;
21
+ const summary = data.value.summary_data ?? data.value.summaryData;
22
+ if (summary && typeof summary === "object" && "metadata" in summary) {
23
+ return summary;
24
+ }
25
+ return null;
26
+ });
27
+ async function fetchData(experimentId) {
28
+ lastExperimentId = experimentId;
29
+ isLoading.value = true;
30
+ error.value = null;
31
+ try {
32
+ const result = await api.get(
33
+ `/api/experiments/${experimentId}/data`
34
+ );
35
+ data.value = result;
36
+ } catch (e) {
37
+ error.value = e instanceof Error ? e.message : "Failed to fetch experiment data";
38
+ data.value = null;
39
+ } finally {
40
+ isLoading.value = false;
41
+ }
42
+ }
43
+ async function refresh() {
44
+ if (lastExperimentId !== null) {
45
+ await fetchData(lastExperimentId);
46
+ }
47
+ }
48
+ return {
49
+ data,
50
+ treeData,
51
+ tableData,
52
+ summaryData,
53
+ isLoading,
54
+ error,
55
+ fetch: fetchData,
56
+ refresh
57
+ };
58
+ }
59
+ export {
60
+ useExperimentData
61
+ };
62
+ //# sourceMappingURL=useExperimentData.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useExperimentData.js","sources":["../../src/composables/useExperimentData.ts"],"sourcesContent":["import { ref, computed, type Ref, type ComputedRef } from 'vue'\nimport { useApi } from './useApi'\nimport type { TreeNode, SummaryData } from '../types'\n\nexport interface UseExperimentDataOptions {\n apiBaseUrl?: string\n immediate?: boolean\n}\n\nexport interface UseExperimentDataReturn {\n data: Ref<Record<string, unknown> | null>\n treeData: ComputedRef<TreeNode[]>\n tableData: ComputedRef<Record<string, unknown>[]>\n summaryData: ComputedRef<SummaryData | null>\n isLoading: Ref<boolean>\n error: Ref<string | null>\n fetch: (experimentId: number) => Promise<void>\n refresh: () => Promise<void>\n}\n\nexport function useExperimentData(\n options: UseExperimentDataOptions = {},\n): UseExperimentDataReturn {\n const api = useApi({ baseUrl: options.apiBaseUrl })\n\n const data = ref<Record<string, unknown> | null>(null)\n const isLoading = ref(false)\n const error = ref<string | null>(null)\n let lastExperimentId: number | null = null\n\n const treeData = computed<TreeNode[]>(() => {\n if (!data.value) return []\n const tree = data.value.tree_data ?? data.value.treeData\n return Array.isArray(tree) ? tree as TreeNode[] : []\n })\n\n const tableData = computed<Record<string, unknown>[]>(() => {\n if (!data.value) return []\n const table = data.value.table_data ?? data.value.tableData\n return Array.isArray(table) ? table as Record<string, unknown>[] : []\n })\n\n const summaryData = computed<SummaryData | null>(() => {\n if (!data.value) return null\n const summary = data.value.summary_data ?? data.value.summaryData\n if (summary && typeof summary === 'object' && 'metadata' in (summary as Record<string, unknown>)) {\n return summary as SummaryData\n }\n return null\n })\n\n async function fetchData(experimentId: number): Promise<void> {\n lastExperimentId = experimentId\n isLoading.value = true\n error.value = null\n try {\n const result = await api.get<Record<string, unknown>>(\n `/api/experiments/${experimentId}/data`,\n )\n data.value = result\n } catch (e) {\n error.value = e instanceof Error ? e.message : 'Failed to fetch experiment data'\n data.value = null\n } finally {\n isLoading.value = false\n }\n }\n\n async function refresh(): Promise<void> {\n if (lastExperimentId !== null) {\n await fetchData(lastExperimentId)\n }\n }\n\n return {\n data,\n treeData,\n tableData,\n summaryData,\n isLoading,\n error,\n fetch: fetchData,\n refresh,\n }\n}\n"],"names":[],"mappings":";;AAoBO,SAAS,kBACd,UAAoC,IACX;AACzB,QAAM,MAAM,OAAO,EAAE,SAAS,QAAQ,YAAY;AAElD,QAAM,OAAO,IAAoC,IAAI;AACrD,QAAM,YAAY,IAAI,KAAK;AAC3B,QAAM,QAAQ,IAAmB,IAAI;AACrC,MAAI,mBAAkC;AAEtC,QAAM,WAAW,SAAqB,MAAM;AAC1C,QAAI,CAAC,KAAK,MAAO,QAAO,CAAA;AACxB,UAAM,OAAO,KAAK,MAAM,aAAa,KAAK,MAAM;AAChD,WAAO,MAAM,QAAQ,IAAI,IAAI,OAAqB,CAAA;AAAA,EACpD,CAAC;AAED,QAAM,YAAY,SAAoC,MAAM;AAC1D,QAAI,CAAC,KAAK,MAAO,QAAO,CAAA;AACxB,UAAM,QAAQ,KAAK,MAAM,cAAc,KAAK,MAAM;AAClD,WAAO,MAAM,QAAQ,KAAK,IAAI,QAAqC,CAAA;AAAA,EACrE,CAAC;AAED,QAAM,cAAc,SAA6B,MAAM;AACrD,QAAI,CAAC,KAAK,MAAO,QAAO;AACxB,UAAM,UAAU,KAAK,MAAM,gBAAgB,KAAK,MAAM;AACtD,QAAI,WAAW,OAAO,YAAY,YAAY,cAAe,SAAqC;AAChG,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT,CAAC;AAED,iBAAe,UAAU,cAAqC;AAC5D,uBAAmB;AACnB,cAAU,QAAQ;AAClB,UAAM,QAAQ;AACd,QAAI;AACF,YAAM,SAAS,MAAM,IAAI;AAAA,QACvB,oBAAoB,YAAY;AAAA,MAAA;AAElC,WAAK,QAAQ;AAAA,IACf,SAAS,GAAG;AACV,YAAM,QAAQ,aAAa,QAAQ,EAAE,UAAU;AAC/C,WAAK,QAAQ;AAAA,IACf,UAAA;AACE,gBAAU,QAAQ;AAAA,IACpB;AAAA,EACF;AAEA,iBAAe,UAAyB;AACtC,QAAI,qBAAqB,MAAM;AAC7B,YAAM,UAAU,gBAAgB;AAAA,IAClC;AAAA,EACF;AAEA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO;AAAA,IACP;AAAA,EAAA;AAEJ;"}
@@ -1,4 +1,4 @@
1
- import { Ref } from 'vue';
1
+ import { Ref, ComputedRef } from 'vue';
2
2
  import { ExperimentSummary, ExperimentFilters } from '../types';
3
3
  export interface UseExperimentSelectorOptions {
4
4
  experimentType?: string;
@@ -13,7 +13,11 @@ export interface UseExperimentSelectorReturn {
13
13
  filters: ExperimentFilters;
14
14
  isLoading: Ref<boolean>;
15
15
  error: Ref<string | null>;
16
+ page: Ref<number>;
17
+ hasMore: ComputedRef<boolean>;
16
18
  fetch: () => Promise<void>;
19
+ loadMore: () => Promise<void>;
20
+ reset: () => void;
17
21
  select: (experiment: ExperimentSummary) => void;
18
22
  clear: () => void;
19
23
  }
@@ -1,4 +1,4 @@
1
- import { ref, reactive, watch } from "vue";
1
+ import { ref, computed, reactive, watch, onScopeDispose } from "vue";
2
2
  import { useApi } from "./useApi.js";
3
3
  function useExperimentSelector(options = {}) {
4
4
  const { limit = 100, immediate = false, experimentType, apiBaseUrl } = options;
@@ -8,11 +8,8 @@ function useExperimentSelector(options = {}) {
8
8
  const selectedExperiment = ref(null);
9
9
  const isLoading = ref(false);
10
10
  const error = ref(null);
11
- const filters = reactive({
12
- search: void 0,
13
- status: void 0,
14
- project: void 0
15
- });
11
+ const page = ref(0);
12
+ const hasMore = computed(() => experiments.value.length < total.value);
16
13
  async function fetchExperiments() {
17
14
  isLoading.value = true;
18
15
  error.value = null;
@@ -23,19 +20,37 @@ function useExperimentSelector(options = {}) {
23
20
  if (filters.search) params.set("search", filters.search);
24
21
  if (filters.project) params.set("project", filters.project);
25
22
  params.set("limit", String(limit));
23
+ params.set("skip", String(page.value * limit));
26
24
  const query = params.toString();
27
25
  const url = `/api/experiments${query ? `?${query}` : ""}`;
28
26
  const data = await api.get(url);
29
- experiments.value = data.experiments;
27
+ if (page.value === 0) {
28
+ experiments.value = data.experiments;
29
+ } else {
30
+ experiments.value = [...experiments.value, ...data.experiments];
31
+ }
30
32
  total.value = data.total;
31
33
  } catch (e) {
32
34
  error.value = e instanceof Error ? e.message : "Failed to fetch experiments";
33
- experiments.value = [];
34
- total.value = 0;
35
+ if (page.value === 0) {
36
+ experiments.value = [];
37
+ total.value = 0;
38
+ }
35
39
  } finally {
36
40
  isLoading.value = false;
37
41
  }
38
42
  }
43
+ async function loadMore() {
44
+ if (!hasMore.value || isLoading.value) return;
45
+ page.value++;
46
+ await fetchExperiments();
47
+ }
48
+ function reset() {
49
+ page.value = 0;
50
+ experiments.value = [];
51
+ total.value = 0;
52
+ fetchExperiments();
53
+ }
39
54
  function select(experiment) {
40
55
  selectedExperiment.value = experiment;
41
56
  }
@@ -44,13 +59,20 @@ function useExperimentSelector(options = {}) {
44
59
  filters.search = void 0;
45
60
  filters.status = void 0;
46
61
  filters.project = void 0;
62
+ page.value = 0;
47
63
  }
64
+ const filters = reactive({
65
+ search: void 0,
66
+ status: void 0,
67
+ project: void 0
68
+ });
48
69
  let debounceTimer = null;
49
70
  watch(
50
71
  () => filters.search,
51
72
  () => {
52
73
  if (debounceTimer) clearTimeout(debounceTimer);
53
74
  debounceTimer = setTimeout(() => {
75
+ page.value = 0;
54
76
  fetchExperiments();
55
77
  }, 300);
56
78
  }
@@ -58,9 +80,13 @@ function useExperimentSelector(options = {}) {
58
80
  watch(
59
81
  () => [filters.status, filters.project],
60
82
  () => {
83
+ page.value = 0;
61
84
  fetchExperiments();
62
85
  }
63
86
  );
87
+ onScopeDispose(() => {
88
+ if (debounceTimer) clearTimeout(debounceTimer);
89
+ });
64
90
  if (immediate) {
65
91
  fetchExperiments();
66
92
  }
@@ -71,7 +97,11 @@ function useExperimentSelector(options = {}) {
71
97
  filters,
72
98
  isLoading,
73
99
  error,
100
+ page,
101
+ hasMore,
74
102
  fetch: fetchExperiments,
103
+ loadMore,
104
+ reset,
75
105
  select,
76
106
  clear
77
107
  };