@morscherlab/mint-sdk 1.0.0-alpha.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/README.md +326 -0
- package/dist/__stories__/experiment-helpers.d.ts +25 -0
- package/dist/__tests__/components/AppLayout.test.d.ts +1 -0
- package/dist/__tests__/components/AppSidebar.test.d.ts +1 -0
- package/dist/__tests__/components/AppTopBar.test.d.ts +1 -0
- package/dist/__tests__/components/BaseInput.test.d.ts +1 -0
- package/dist/__tests__/components/BasePill.test.d.ts +1 -0
- package/dist/__tests__/components/Calendar.test.d.ts +1 -0
- package/dist/__tests__/components/CollapsibleCard.test.d.ts +1 -0
- package/dist/__tests__/components/DataFrame.test.d.ts +1 -0
- package/dist/__tests__/components/DropdownButton.test.d.ts +1 -0
- package/dist/__tests__/composables/formBuilderRegistry.test.d.ts +1 -0
- package/dist/__tests__/composables/useAppExperiment.test.d.ts +1 -0
- package/dist/__tests__/composables/useAuth.test.d.ts +1 -0
- package/dist/__tests__/composables/useAutoGroup.test.d.ts +1 -0
- package/dist/__tests__/composables/useExperimentData.test.d.ts +13 -0
- package/dist/__tests__/composables/useExperimentSave.test.d.ts +1 -0
- package/dist/__tests__/composables/useForm.test.d.ts +1 -0
- package/dist/__tests__/composables/useFormBuilder.test.d.ts +1 -0
- package/dist/__tests__/composables/usePlatformContext.test.d.ts +1 -0
- package/dist/__tests__/composables/usePluginApi.test.d.ts +13 -0
- package/dist/__tests__/composables/usePluginConfig.test.d.ts +14 -0
- package/dist/__tests__/utils/color.test.d.ts +1 -0
- package/dist/auth-BYmxZdJl.js +297 -0
- package/dist/auth-BYmxZdJl.js.map +1 -0
- package/dist/components/AlertBox.vue.d.ts +34 -0
- package/dist/components/AppAvatarMenu.vue.d.ts +58 -0
- package/dist/components/AppContainer.vue.d.ts +28 -0
- package/dist/components/AppLayout.vue.d.ts +31 -0
- package/dist/components/AppPageSelector.vue.d.ts +43 -0
- package/dist/components/AppPillNav.vue.d.ts +11 -0
- package/dist/components/AppPluginSwitcher.vue.d.ts +38 -0
- package/dist/components/AppSidebar.vue.d.ts +47 -0
- package/dist/components/AppTopBar.vue.d.ts +111 -0
- package/dist/components/AuditTrail.vue.d.ts +38 -0
- package/dist/components/AutoGroupModal.vue.d.ts +124 -0
- package/dist/components/Avatar.vue.d.ts +14 -0
- package/dist/components/BaseButton.vue.d.ts +37 -0
- package/dist/components/BaseCheckbox.vue.d.ts +17 -0
- package/dist/components/BaseInput.vue.d.ts +34 -0
- package/dist/components/BaseModal.vue.d.ts +46 -0
- package/dist/components/BasePill.vue.d.ts +57 -0
- package/dist/components/BaseRadioGroup.vue.d.ts +21 -0
- package/dist/components/BaseSelect.vue.d.ts +20 -0
- package/dist/components/BaseSlider.vue.d.ts +22 -0
- package/dist/components/BaseTabs.vue.d.ts +14 -0
- package/dist/components/BaseTextarea.vue.d.ts +30 -0
- package/dist/components/BaseToggle.vue.d.ts +19 -0
- package/dist/components/BatchProgressList.vue.d.ts +43 -0
- package/dist/components/Breadcrumb.vue.d.ts +33 -0
- package/dist/components/Calendar.vue.d.ts +107 -0
- package/dist/components/ChartContainer.vue.d.ts +31 -0
- package/dist/components/ChemicalFormula.vue.d.ts +8 -0
- package/dist/components/CollapsibleCard.vue.d.ts +41 -0
- package/dist/components/ColorSlider.vue.d.ts +34 -0
- package/dist/components/ConcentrationInput.vue.d.ts +25 -0
- package/dist/components/ConfirmDialog.vue.d.ts +42 -0
- package/dist/components/DataFrame.vue.d.ts +107 -0
- package/dist/components/DatePicker.vue.d.ts +25 -0
- package/dist/components/DateTimePicker.vue.d.ts +30 -0
- package/dist/components/Divider.vue.d.ts +14 -0
- package/dist/components/DoseCalculator.vue.d.ts +19 -0
- package/dist/components/DropdownButton.vue.d.ts +47 -0
- package/dist/components/EmptyState.vue.d.ts +36 -0
- package/dist/components/ExperimentCodeBadge.vue.d.ts +14 -0
- package/dist/components/ExperimentDataViewer.vue.d.ts +29 -0
- package/dist/components/ExperimentPopover.vue.d.ts +32 -0
- package/dist/components/ExperimentSelectorModal.vue.d.ts +28 -0
- package/dist/components/ExperimentTimeline.vue.d.ts +44 -0
- package/dist/components/FileUploader.vue.d.ts +40 -0
- package/dist/components/FitPanel.vue.d.ts +46 -0
- package/dist/components/FormActions.vue.d.ts +33 -0
- package/dist/components/FormBuilder.vue.d.ts +287 -0
- package/dist/components/FormField.vue.d.ts +28 -0
- package/dist/components/FormFieldRenderer.vue.d.ts +31 -0
- package/dist/components/FormSection.vue.d.ts +43 -0
- package/dist/components/FormulaInput.vue.d.ts +25 -0
- package/dist/components/GroupAssigner.vue.d.ts +25 -0
- package/dist/components/GroupingModal.vue.d.ts +12 -0
- package/dist/components/IconButton.vue.d.ts +34 -0
- package/dist/components/LoadingSpinner.vue.d.ts +12 -0
- package/dist/components/MoleculeInput.vue.d.ts +27 -0
- package/dist/components/MultiSelect.vue.d.ts +19 -0
- package/dist/components/NumberInput.vue.d.ts +22 -0
- package/dist/components/PlateMapEditor.vue.d.ts +50 -0
- package/dist/components/ProgressBar.vue.d.ts +23 -0
- package/dist/components/ProtocolStepEditor.vue.d.ts +24 -0
- package/dist/components/RackEditor.vue.d.ts +40 -0
- package/dist/components/ReagentEditor.vue.d.ts +30 -0
- package/dist/components/ReagentList.vue.d.ts +32 -0
- package/dist/components/ResourceCard.vue.d.ts +50 -0
- package/dist/components/SampleHierarchyTree.vue.d.ts +26 -0
- package/dist/components/SampleLegend.vue.d.ts +32 -0
- package/dist/components/SampleSelector.vue.d.ts +29 -0
- package/dist/components/ScheduleCalendar.vue.d.ts +110 -0
- package/dist/components/ScientificNumber.vue.d.ts +14 -0
- package/dist/components/SegmentedControl.vue.d.ts +20 -0
- package/dist/components/SequenceInput.vue.d.ts +54 -0
- package/dist/components/SettingsButton.vue.d.ts +30 -0
- package/dist/components/SettingsModal.vue.d.ts +36 -0
- package/dist/components/Skeleton.vue.d.ts +11 -0
- package/dist/components/StatusIndicator.vue.d.ts +13 -0
- package/dist/components/StepWizard.vue.d.ts +65 -0
- package/dist/components/TagsInput.vue.d.ts +39 -0
- package/dist/components/ThemeToggle.vue.d.ts +7 -0
- package/dist/components/TimePicker.vue.d.ts +29 -0
- package/dist/components/TimeRangeInput.vue.d.ts +27 -0
- package/dist/components/ToastNotification.vue.d.ts +2 -0
- package/dist/components/Tooltip.vue.d.ts +35 -0
- package/dist/components/UnitInput.vue.d.ts +39 -0
- package/dist/components/WellEditPopup.vue.d.ts +25 -0
- package/dist/components/WellPlate.vue.d.ts +73 -0
- package/dist/components/index.d.ts +87 -0
- package/dist/components/index.js +3 -0
- package/dist/components-CKf-UpGi.js +15089 -0
- package/dist/components-CKf-UpGi.js.map +1 -0
- package/dist/composables/experiment-utils.d.ts +8 -0
- package/dist/composables/formBuilderRegistry.d.ts +13 -0
- package/dist/composables/index.d.ts +28 -0
- package/dist/composables/index.js +3 -0
- package/dist/composables/useApi.d.ts +20 -0
- package/dist/composables/useAppExperiment.d.ts +37 -0
- package/dist/composables/useAsync.d.ts +128 -0
- package/dist/composables/useAuth.d.ts +47 -0
- package/dist/composables/useAutoGroup.d.ts +106 -0
- package/dist/composables/useChemicalFormula.d.ts +21 -0
- package/dist/composables/useConcentrationUnits.d.ts +29 -0
- package/dist/composables/useDoseCalculator.d.ts +58 -0
- package/dist/composables/useExperimentData.d.ts +18 -0
- package/dist/composables/useExperimentSave.d.ts +36 -0
- package/dist/composables/useExperimentSelector.d.ts +30 -0
- package/dist/composables/useForm.d.ts +92 -0
- package/dist/composables/useFormBuilder.d.ts +24 -0
- package/dist/composables/usePasskey.d.ts +10 -0
- package/dist/composables/usePlatformContext.d.ts +131 -0
- package/dist/composables/usePluginApi.d.ts +29 -0
- package/dist/composables/usePluginConfig.d.ts +13 -0
- package/dist/composables/useProtocolTemplates.d.ts +44 -0
- package/dist/composables/useRackEditor.d.ts +31 -0
- package/dist/composables/useReagentSeries.d.ts +23 -0
- package/dist/composables/useScheduleDrag.d.ts +78 -0
- package/dist/composables/useSequenceUtils.d.ts +14 -0
- package/dist/composables/useTheme.d.ts +8 -0
- package/dist/composables/useTimeUtils.d.ts +29 -0
- package/dist/composables/useToast.d.ts +22 -0
- package/dist/composables/useWellPlateEditor.d.ts +33 -0
- package/dist/composables-D0QfFzq1.js +805 -0
- package/dist/composables-D0QfFzq1.js.map +1 -0
- package/dist/histoire.setup.d.ts +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/install.d.ts +16 -0
- package/dist/install.js +23 -0
- package/dist/install.js.map +1 -0
- package/dist/stores/auth.d.ts +146 -0
- package/dist/stores/index.d.ts +2 -0
- package/dist/stores/index.js +2 -0
- package/dist/stores/settings.d.ts +75 -0
- package/dist/styles.css +29728 -0
- package/dist/tailwind.preset.d.ts +58 -0
- package/dist/tailwind.preset.js +66 -0
- package/dist/tailwind.preset.js.map +1 -0
- package/dist/types/auth.d.ts +42 -0
- package/dist/types/auto-group.d.ts +34 -0
- package/dist/types/components.d.ts +528 -0
- package/dist/types/form-builder.d.ts +167 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/index.js +0 -0
- package/dist/types/platform.d.ts +75 -0
- package/dist/useScheduleDrag-DAJueTbK.js +7181 -0
- package/dist/useScheduleDrag-DAJueTbK.js.map +1 -0
- package/dist/utils/color.d.ts +24 -0
- package/package.json +114 -0
- package/src/__stories__/experiment-helpers.ts +83 -0
- package/src/__tests__/components/AppLayout.test.ts +163 -0
- package/src/__tests__/components/AppSidebar.test.ts +292 -0
- package/src/__tests__/components/AppTopBar.test.ts +683 -0
- package/src/__tests__/components/BaseInput.test.ts +99 -0
- package/src/__tests__/components/BasePill.test.ts +291 -0
- package/src/__tests__/components/Calendar.test.ts +566 -0
- package/src/__tests__/components/CollapsibleCard.test.ts +524 -0
- package/src/__tests__/components/DataFrame.test.ts +767 -0
- package/src/__tests__/components/DropdownButton.test.ts +471 -0
- package/src/__tests__/composables/formBuilderRegistry.test.ts +187 -0
- package/src/__tests__/composables/useAppExperiment.test.ts +560 -0
- package/src/__tests__/composables/useAuth.test.ts +188 -0
- package/src/__tests__/composables/useAutoGroup.test.ts +860 -0
- package/src/__tests__/composables/useExperimentData.test.ts +127 -0
- package/src/__tests__/composables/useExperimentSave.test.ts +347 -0
- package/src/__tests__/composables/useForm.test.ts +205 -0
- package/src/__tests__/composables/useFormBuilder.test.ts +917 -0
- package/src/__tests__/composables/usePlatformContext.test.ts +116 -0
- package/src/__tests__/composables/usePluginApi.test.ts +81 -0
- package/src/__tests__/composables/usePluginConfig.test.ts +176 -0
- package/src/__tests__/utils/color.test.ts +96 -0
- package/src/components/AlertBox.story.vue +204 -0
- package/src/components/AlertBox.vue +88 -0
- package/src/components/AppAvatarMenu.story.vue +155 -0
- package/src/components/AppAvatarMenu.vue +184 -0
- package/src/components/AppContainer.story.vue +104 -0
- package/src/components/AppContainer.vue +34 -0
- package/src/components/AppLayout.story.vue +292 -0
- package/src/components/AppLayout.vue +75 -0
- package/src/components/AppPageSelector.vue +159 -0
- package/src/components/AppPillNav.vue +66 -0
- package/src/components/AppPluginSwitcher.vue +241 -0
- package/src/components/AppSidebar.story.vue +309 -0
- package/src/components/AppSidebar.vue +119 -0
- package/src/components/AppTopBar.story.vue +304 -0
- package/src/components/AppTopBar.vue +661 -0
- package/src/components/AuditTrail.story.vue +163 -0
- package/src/components/AuditTrail.vue +151 -0
- package/src/components/AutoGroupModal.story.vue +273 -0
- package/src/components/AutoGroupModal.vue +566 -0
- package/src/components/Avatar.story.vue +115 -0
- package/src/components/Avatar.vue +79 -0
- package/src/components/BaseButton.story.vue +96 -0
- package/src/components/BaseButton.vue +73 -0
- package/src/components/BaseCheckbox.story.vue +73 -0
- package/src/components/BaseCheckbox.vue +69 -0
- package/src/components/BaseInput.story.vue +98 -0
- package/src/components/BaseInput.vue +74 -0
- package/src/components/BaseModal.story.vue +237 -0
- package/src/components/BaseModal.vue +182 -0
- package/src/components/BasePill.story.vue +142 -0
- package/src/components/BasePill.vue +89 -0
- package/src/components/BaseRadioGroup.story.vue +145 -0
- package/src/components/BaseRadioGroup.vue +124 -0
- package/src/components/BaseSelect.story.vue +120 -0
- package/src/components/BaseSelect.vue +71 -0
- package/src/components/BaseSlider.story.vue +122 -0
- package/src/components/BaseSlider.vue +126 -0
- package/src/components/BaseTabs.story.vue +127 -0
- package/src/components/BaseTabs.vue +59 -0
- package/src/components/BaseTextarea.story.vue +91 -0
- package/src/components/BaseTextarea.vue +62 -0
- package/src/components/BaseToggle.story.vue +81 -0
- package/src/components/BaseToggle.vue +76 -0
- package/src/components/BatchProgressList.story.vue +92 -0
- package/src/components/BatchProgressList.vue +184 -0
- package/src/components/Breadcrumb.story.vue +106 -0
- package/src/components/Breadcrumb.vue +75 -0
- package/src/components/Calendar.story.vue +106 -0
- package/src/components/Calendar.vue +363 -0
- package/src/components/ChartContainer.story.vue +113 -0
- package/src/components/ChartContainer.vue +64 -0
- package/src/components/ChemicalFormula.story.vue +102 -0
- package/src/components/ChemicalFormula.vue +39 -0
- package/src/components/CollapsibleCard.story.vue +135 -0
- package/src/components/CollapsibleCard.vue +167 -0
- package/src/components/ColorSlider.story.vue +120 -0
- package/src/components/ColorSlider.vue +164 -0
- package/src/components/ConcentrationInput.story.vue +77 -0
- package/src/components/ConcentrationInput.vue +185 -0
- package/src/components/ConfirmDialog.story.vue +248 -0
- package/src/components/ConfirmDialog.vue +93 -0
- package/src/components/DataFrame.story.vue +148 -0
- package/src/components/DataFrame.vue +419 -0
- package/src/components/DatePicker.story.vue +119 -0
- package/src/components/DatePicker.vue +330 -0
- package/src/components/DateTimePicker.story.vue +112 -0
- package/src/components/DateTimePicker.vue +392 -0
- package/src/components/Divider.story.vue +80 -0
- package/src/components/Divider.vue +49 -0
- package/src/components/DoseCalculator.story.vue +68 -0
- package/src/components/DoseCalculator.vue +476 -0
- package/src/components/DropdownButton.story.vue +102 -0
- package/src/components/DropdownButton.vue +181 -0
- package/src/components/EmptyState.story.vue +135 -0
- package/src/components/EmptyState.vue +69 -0
- package/src/components/ExperimentCodeBadge.story.vue +77 -0
- package/src/components/ExperimentCodeBadge.vue +64 -0
- package/src/components/ExperimentDataViewer.story.vue +174 -0
- package/src/components/ExperimentDataViewer.vue +288 -0
- package/src/components/ExperimentPopover.story.vue +384 -0
- package/src/components/ExperimentPopover.vue +241 -0
- package/src/components/ExperimentSelectorModal.story.vue +391 -0
- package/src/components/ExperimentSelectorModal.vue +387 -0
- package/src/components/ExperimentTimeline.story.vue +161 -0
- package/src/components/ExperimentTimeline.vue +382 -0
- package/src/components/FileUploader.story.vue +107 -0
- package/src/components/FileUploader.vue +386 -0
- package/src/components/FitPanel.story.vue +125 -0
- package/src/components/FitPanel.vue +120 -0
- package/src/components/FormActions.vue +92 -0
- package/src/components/FormBuilder.vue +214 -0
- package/src/components/FormField.story.vue +132 -0
- package/src/components/FormField.vue +59 -0
- package/src/components/FormFieldRenderer.vue +58 -0
- package/src/components/FormSection.vue +90 -0
- package/src/components/FormulaInput.story.vue +96 -0
- package/src/components/FormulaInput.vue +125 -0
- package/src/components/GroupAssigner.story.vue +83 -0
- package/src/components/GroupAssigner.vue +284 -0
- package/src/components/GroupingModal.story.vue +52 -0
- package/src/components/GroupingModal.vue +422 -0
- package/src/components/IconButton.story.vue +135 -0
- package/src/components/IconButton.vue +73 -0
- package/src/components/LoadingSpinner.story.vue +70 -0
- package/src/components/LoadingSpinner.vue +50 -0
- package/src/components/MoleculeInput.story.vue +66 -0
- package/src/components/MoleculeInput.vue +426 -0
- package/src/components/MultiSelect.story.vue +132 -0
- package/src/components/MultiSelect.vue +118 -0
- package/src/components/NumberInput.story.vue +122 -0
- package/src/components/NumberInput.vue +160 -0
- package/src/components/PlateMapEditor.story.vue +92 -0
- package/src/components/PlateMapEditor.vue +513 -0
- package/src/components/ProgressBar.story.vue +148 -0
- package/src/components/ProgressBar.vue +114 -0
- package/src/components/ProtocolStepEditor.story.vue +69 -0
- package/src/components/ProtocolStepEditor.vue +522 -0
- package/src/components/RackEditor.story.vue +100 -0
- package/src/components/RackEditor.vue +371 -0
- package/src/components/ReagentEditor.story.vue +153 -0
- package/src/components/ReagentEditor.vue +418 -0
- package/src/components/ReagentList.story.vue +137 -0
- package/src/components/ReagentList.vue +463 -0
- package/src/components/ResourceCard.story.vue +150 -0
- package/src/components/ResourceCard.vue +161 -0
- package/src/components/SampleHierarchyTree.story.vue +161 -0
- package/src/components/SampleHierarchyTree.vue +256 -0
- package/src/components/SampleLegend.story.vue +91 -0
- package/src/components/SampleLegend.vue +119 -0
- package/src/components/SampleSelector.story.vue +111 -0
- package/src/components/SampleSelector.vue +1033 -0
- package/src/components/ScheduleCalendar.story.vue +195 -0
- package/src/components/ScheduleCalendar.vue +569 -0
- package/src/components/ScientificNumber.story.vue +127 -0
- package/src/components/ScientificNumber.vue +197 -0
- package/src/components/SegmentedControl.story.vue +132 -0
- package/src/components/SegmentedControl.vue +79 -0
- package/src/components/SequenceInput.story.vue +119 -0
- package/src/components/SequenceInput.vue +209 -0
- package/src/components/SettingsButton.story.vue +58 -0
- package/src/components/SettingsButton.vue +76 -0
- package/src/components/SettingsModal.story.vue +145 -0
- package/src/components/SettingsModal.vue +146 -0
- package/src/components/Skeleton.story.vue +141 -0
- package/src/components/Skeleton.vue +74 -0
- package/src/components/StatusIndicator.story.vue +99 -0
- package/src/components/StatusIndicator.vue +40 -0
- package/src/components/StepWizard.story.vue +155 -0
- package/src/components/StepWizard.vue +223 -0
- package/src/components/TagsInput.story.vue +155 -0
- package/src/components/TagsInput.vue +265 -0
- package/src/components/ThemeToggle.story.vue +36 -0
- package/src/components/ThemeToggle.vue +54 -0
- package/src/components/TimePicker.story.vue +96 -0
- package/src/components/TimePicker.vue +273 -0
- package/src/components/TimeRangeInput.story.vue +104 -0
- package/src/components/TimeRangeInput.vue +122 -0
- package/src/components/ToastNotification.story.vue +157 -0
- package/src/components/ToastNotification.vue +62 -0
- package/src/components/Tooltip.story.vue +138 -0
- package/src/components/Tooltip.vue +119 -0
- package/src/components/UnitInput.story.vue +194 -0
- package/src/components/UnitInput.vue +213 -0
- package/src/components/WellEditPopup.vue +234 -0
- package/src/components/WellPlate.story.vue +282 -0
- package/src/components/WellPlate.vue +830 -0
- package/src/components/index.ts +118 -0
- package/src/composables/experiment-utils.ts +57 -0
- package/src/composables/formBuilderRegistry.ts +79 -0
- package/src/composables/index.ts +140 -0
- package/src/composables/useApi.ts +167 -0
- package/src/composables/useAppExperiment.ts +159 -0
- package/src/composables/useAsync.ts +323 -0
- package/src/composables/useAuth.ts +445 -0
- package/src/composables/useAutoGroup.ts +641 -0
- package/src/composables/useChemicalFormula.ts +275 -0
- package/src/composables/useConcentrationUnits.ts +246 -0
- package/src/composables/useDoseCalculator.ts +370 -0
- package/src/composables/useExperimentData.ts +86 -0
- package/src/composables/useExperimentSave.ts +192 -0
- package/src/composables/useExperimentSelector.ts +292 -0
- package/src/composables/useForm.ts +416 -0
- package/src/composables/useFormBuilder.ts +383 -0
- package/src/composables/usePasskey.ts +216 -0
- package/src/composables/usePlatformContext.ts +299 -0
- package/src/composables/usePluginApi.ts +39 -0
- package/src/composables/usePluginConfig.ts +93 -0
- package/src/composables/useProtocolTemplates.ts +518 -0
- package/src/composables/useRackEditor.ts +222 -0
- package/src/composables/useReagentSeries.ts +91 -0
- package/src/composables/useScheduleDrag.ts +245 -0
- package/src/composables/useSequenceUtils.ts +105 -0
- package/src/composables/useTheme.ts +58 -0
- package/src/composables/useTimeUtils.ts +131 -0
- package/src/composables/useToast.ts +40 -0
- package/src/composables/useWellPlateEditor.ts +421 -0
- package/src/histoire.setup.ts +17 -0
- package/src/index.ts +367 -0
- package/src/install.ts +32 -0
- package/src/stores/auth.ts +152 -0
- package/src/stores/index.ts +2 -0
- package/src/stores/settings.ts +218 -0
- package/src/styles/components/alert-box.css +150 -0
- package/src/styles/components/app-avatar-menu.css +155 -0
- package/src/styles/components/app-container.css +33 -0
- package/src/styles/components/app-layout.css +98 -0
- package/src/styles/components/app-page-selector.css +191 -0
- package/src/styles/components/app-pill-nav.css +57 -0
- package/src/styles/components/app-plugin-switcher.css +209 -0
- package/src/styles/components/app-sidebar.css +145 -0
- package/src/styles/components/app-top-bar.css +492 -0
- package/src/styles/components/audit-trail.css +143 -0
- package/src/styles/components/auto-group-modal.css +644 -0
- package/src/styles/components/avatar.css +73 -0
- package/src/styles/components/batch-progress-list.css +196 -0
- package/src/styles/components/breadcrumb.css +64 -0
- package/src/styles/components/button.css +188 -0
- package/src/styles/components/calendar.css +192 -0
- package/src/styles/components/chart-container.css +69 -0
- package/src/styles/components/checkbox.css +123 -0
- package/src/styles/components/chemical-formula.css +46 -0
- package/src/styles/components/collapsible-card.css +253 -0
- package/src/styles/components/color-slider.css +110 -0
- package/src/styles/components/concentration-input.css +156 -0
- package/src/styles/components/confirm-dialog.css +183 -0
- package/src/styles/components/dataframe.css +382 -0
- package/src/styles/components/date-picker.css +243 -0
- package/src/styles/components/datetime-picker.css +229 -0
- package/src/styles/components/divider.css +63 -0
- package/src/styles/components/dose-calculator.css +301 -0
- package/src/styles/components/dropdown-button.css +280 -0
- package/src/styles/components/empty-state.css +151 -0
- package/src/styles/components/experiment-code-badge.css +33 -0
- package/src/styles/components/experiment-data-viewer.css +138 -0
- package/src/styles/components/experiment-popover.css +562 -0
- package/src/styles/components/experiment-selector-modal.css +285 -0
- package/src/styles/components/experiment-timeline.css +529 -0
- package/src/styles/components/file-uploader.css +310 -0
- package/src/styles/components/fit-panel.css +67 -0
- package/src/styles/components/form-builder.css +69 -0
- package/src/styles/components/form-field.css +48 -0
- package/src/styles/components/formula-input.css +103 -0
- package/src/styles/components/group-assigner.css +200 -0
- package/src/styles/components/grouping-modal.css +323 -0
- package/src/styles/components/icon-button.css +192 -0
- package/src/styles/components/input.css +66 -0
- package/src/styles/components/loading-spinner.css +67 -0
- package/src/styles/components/modal.css +350 -0
- package/src/styles/components/molecule-input.css +186 -0
- package/src/styles/components/multi-select.css +131 -0
- package/src/styles/components/number-input.css +199 -0
- package/src/styles/components/pill.css +188 -0
- package/src/styles/components/plate-map-editor.css +464 -0
- package/src/styles/components/progress-bar.css +133 -0
- package/src/styles/components/protocol-step-editor.css +449 -0
- package/src/styles/components/rack-editor.css +265 -0
- package/src/styles/components/radio-group.css +240 -0
- package/src/styles/components/reagent-editor.css +510 -0
- package/src/styles/components/reagent-list.css +407 -0
- package/src/styles/components/resource-card.css +360 -0
- package/src/styles/components/sample-hierarchy-tree.css +314 -0
- package/src/styles/components/sample-legend.css +201 -0
- package/src/styles/components/sample-selector.css +751 -0
- package/src/styles/components/schedule-calendar.css +478 -0
- package/src/styles/components/scientific-number.css +63 -0
- package/src/styles/components/segmented-control.css +197 -0
- package/src/styles/components/select.css +77 -0
- package/src/styles/components/sequence-input.css +184 -0
- package/src/styles/components/settings-button.css +94 -0
- package/src/styles/components/settings-modal.css +95 -0
- package/src/styles/components/skeleton.css +49 -0
- package/src/styles/components/slider.css +74 -0
- package/src/styles/components/status-indicator.css +66 -0
- package/src/styles/components/step-wizard.css +192 -0
- package/src/styles/components/tabs.css +95 -0
- package/src/styles/components/tags-input.css +195 -0
- package/src/styles/components/textarea.css +82 -0
- package/src/styles/components/theme-toggle.css +69 -0
- package/src/styles/components/time-picker.css +171 -0
- package/src/styles/components/time-range-input.css +42 -0
- package/src/styles/components/toast.css +91 -0
- package/src/styles/components/toggle.css +146 -0
- package/src/styles/components/tooltip.css +91 -0
- package/src/styles/components/unit-input.css +123 -0
- package/src/styles/components/well-edit-popup.css +252 -0
- package/src/styles/components/well-plate.css +307 -0
- package/src/styles/index.css +87 -0
- package/src/styles/variables.css +1117 -0
- package/src/tailwind.preset.ts +61 -0
- package/src/types/auth.ts +55 -0
- package/src/types/auto-group.ts +40 -0
- package/src/types/components.ts +710 -0
- package/src/types/form-builder.ts +197 -0
- package/src/types/index.ts +207 -0
- package/src/types/platform.ts +116 -0
- package/src/utils/color.ts +96 -0
|
@@ -0,0 +1,641 @@
|
|
|
1
|
+
import { ref, computed } from 'vue'
|
|
2
|
+
import type {
|
|
3
|
+
InputMode,
|
|
4
|
+
OutlierAction,
|
|
5
|
+
OutlierInfo,
|
|
6
|
+
ColumnInfo,
|
|
7
|
+
MetadataRow,
|
|
8
|
+
AutoGroupResult,
|
|
9
|
+
ParsedCsvData,
|
|
10
|
+
} from '../types/auto-group'
|
|
11
|
+
import type { SampleGroup } from '../types/components'
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_COLORS = [
|
|
14
|
+
'#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6',
|
|
15
|
+
'#EC4899', '#06B6D4', '#84CC16', '#F97316', '#6366F1',
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const DELIMITER_CANDIDATES = ['_', '-', '.'] as const
|
|
19
|
+
|
|
20
|
+
// --- Pure functions (exported for testing) ---
|
|
21
|
+
|
|
22
|
+
export function analyzeDelimiter(lines: string[]): {
|
|
23
|
+
delimiter: string
|
|
24
|
+
dominantFieldCount: number
|
|
25
|
+
minFieldCount: number
|
|
26
|
+
consistency: number
|
|
27
|
+
} {
|
|
28
|
+
if (lines.length === 0) {
|
|
29
|
+
return { delimiter: '_', dominantFieldCount: 1, minFieldCount: 1, consistency: 0 }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let bestDelimiter = '_'
|
|
33
|
+
let bestConsistency = -1
|
|
34
|
+
let bestFieldCount = 1
|
|
35
|
+
|
|
36
|
+
for (const candidate of DELIMITER_CANDIDATES) {
|
|
37
|
+
const fieldCounts = lines.map(line => line.split(candidate).length)
|
|
38
|
+
const countFrequency = new Map<number, number>()
|
|
39
|
+
|
|
40
|
+
for (const count of fieldCounts) {
|
|
41
|
+
countFrequency.set(count, (countFrequency.get(count) ?? 0) + 1)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Find mode (most frequent field count)
|
|
45
|
+
let modeCount = 1
|
|
46
|
+
let modeFrequency = 0
|
|
47
|
+
for (const [count, freq] of countFrequency) {
|
|
48
|
+
if (freq > modeFrequency || (freq === modeFrequency && count > modeCount)) {
|
|
49
|
+
modeCount = count
|
|
50
|
+
modeFrequency = freq
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const rawConsistency = modeFrequency / lines.length
|
|
55
|
+
// A delimiter that produces field count 1 didn't actually split anything
|
|
56
|
+
const consistency = modeCount > 1 ? rawConsistency : 0
|
|
57
|
+
|
|
58
|
+
if (
|
|
59
|
+
consistency > bestConsistency ||
|
|
60
|
+
(consistency === bestConsistency &&
|
|
61
|
+
DELIMITER_CANDIDATES.indexOf(candidate) < DELIMITER_CANDIDATES.indexOf(bestDelimiter as typeof candidate))
|
|
62
|
+
) {
|
|
63
|
+
bestDelimiter = candidate
|
|
64
|
+
bestConsistency = consistency
|
|
65
|
+
bestFieldCount = modeCount
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const bestFieldCounts = lines.map(line => line.split(bestDelimiter).length)
|
|
70
|
+
const multiFieldCounts = bestFieldCounts.filter(c => c >= 2)
|
|
71
|
+
const minFieldCount = multiFieldCounts.length > 0 ? Math.min(...multiFieldCounts) : 1
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
delimiter: bestDelimiter,
|
|
75
|
+
dominantFieldCount: bestFieldCount,
|
|
76
|
+
minFieldCount,
|
|
77
|
+
consistency: bestConsistency,
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function detectOutliers(
|
|
82
|
+
lines: string[],
|
|
83
|
+
delimiter: string,
|
|
84
|
+
minFieldCount: number,
|
|
85
|
+
): OutlierInfo[] {
|
|
86
|
+
const outliers: OutlierInfo[] = []
|
|
87
|
+
|
|
88
|
+
for (let i = 0; i < lines.length; i++) {
|
|
89
|
+
const fieldCount = lines[i].split(delimiter).length
|
|
90
|
+
if (fieldCount < minFieldCount) {
|
|
91
|
+
outliers.push({
|
|
92
|
+
sample: lines[i],
|
|
93
|
+
index: i,
|
|
94
|
+
fieldCount,
|
|
95
|
+
action: 'include',
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return outliers
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const QC_KEYWORDS = new Set([
|
|
104
|
+
'eqc', 'iqc', 'qc', 'blank', 'std', 'standard', 'test',
|
|
105
|
+
])
|
|
106
|
+
|
|
107
|
+
export function classifyOutlierAction(
|
|
108
|
+
sample: string,
|
|
109
|
+
delimiter: string,
|
|
110
|
+
): OutlierAction {
|
|
111
|
+
const segments = sample.split(delimiter)
|
|
112
|
+
return segments.some(seg => QC_KEYWORDS.has(seg.toLowerCase()))
|
|
113
|
+
? 'qc'
|
|
114
|
+
: 'include'
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// A column is useful for grouping when it has more than one distinct value
|
|
118
|
+
// AND it does not produce a unique value per row (which would create N
|
|
119
|
+
// singleton groups instead of meaningful aggregation).
|
|
120
|
+
export function isUsefulField(field: ColumnInfo, rowCount: number): boolean {
|
|
121
|
+
return field.cardinality > 1 && !(rowCount > 1 && field.cardinality === rowCount)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function extractColumns(
|
|
125
|
+
samples: string[],
|
|
126
|
+
delimiter: string,
|
|
127
|
+
minFieldCount: number,
|
|
128
|
+
): ColumnInfo[] {
|
|
129
|
+
if (samples.length === 0) return []
|
|
130
|
+
|
|
131
|
+
const suffixCount = minFieldCount - 1
|
|
132
|
+
const rows = samples.map(s => {
|
|
133
|
+
const parts = s.split(delimiter)
|
|
134
|
+
const splitAt = parts.length - suffixCount
|
|
135
|
+
return [
|
|
136
|
+
parts.slice(0, splitAt).join(delimiter),
|
|
137
|
+
...parts.slice(splitAt),
|
|
138
|
+
]
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
const columnCount = minFieldCount
|
|
142
|
+
const columns: ColumnInfo[] = []
|
|
143
|
+
for (let col = 0; col < columnCount; col++) {
|
|
144
|
+
const values = rows.map(row => row[col])
|
|
145
|
+
const unique = [...new Set(values)]
|
|
146
|
+
columns.push({
|
|
147
|
+
index: col,
|
|
148
|
+
name: col === 0 ? 'Condition' : `Field ${col + 1}`,
|
|
149
|
+
uniqueValues: unique,
|
|
150
|
+
cardinality: unique.length,
|
|
151
|
+
type: col === 0 ? 'prefix' : 'suffix',
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return columns
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function parseCSVLine(line: string, delimiter: string = ','): string[] {
|
|
159
|
+
const result: string[] = []
|
|
160
|
+
let current = ''
|
|
161
|
+
let inQuotes = false
|
|
162
|
+
|
|
163
|
+
for (let i = 0; i < line.length; i++) {
|
|
164
|
+
const char = line[i]
|
|
165
|
+
if (char === '"') {
|
|
166
|
+
inQuotes = !inQuotes
|
|
167
|
+
} else if (char === delimiter && !inQuotes) {
|
|
168
|
+
result.push(current.trim())
|
|
169
|
+
current = ''
|
|
170
|
+
} else {
|
|
171
|
+
current += char
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
result.push(current.trim())
|
|
175
|
+
|
|
176
|
+
return result
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function parseCSV(text: string): ParsedCsvData {
|
|
180
|
+
const lines = text.trim().split('\n')
|
|
181
|
+
if (lines.length < 2) {
|
|
182
|
+
throw new Error('CSV must have at least a header and one data row')
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Auto-detect delimiter: tab takes precedence if present in header
|
|
186
|
+
const firstLine = lines[0]
|
|
187
|
+
const csvDelimiter = firstLine.includes('\t') ? '\t' : ','
|
|
188
|
+
|
|
189
|
+
const headers = parseCSVLine(firstLine, csvDelimiter)
|
|
190
|
+
const rows: Record<string, string>[] = []
|
|
191
|
+
|
|
192
|
+
for (let i = 1; i < lines.length; i++) {
|
|
193
|
+
const values = parseCSVLine(lines[i], csvDelimiter)
|
|
194
|
+
if (values.length !== headers.length) continue
|
|
195
|
+
const row: Record<string, string> = {}
|
|
196
|
+
headers.forEach((header, idx) => {
|
|
197
|
+
row[header] = values[idx]
|
|
198
|
+
})
|
|
199
|
+
rows.push(row)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Auto-detect sample column
|
|
203
|
+
const sampleKeywords = ['sample', 'name', 'id', 'sample_name', 'samplename', 'file name', 'filename', 'file_name']
|
|
204
|
+
const sampleColumn =
|
|
205
|
+
headers.find(h => sampleKeywords.includes(h.toLowerCase())) ?? headers[0]
|
|
206
|
+
|
|
207
|
+
return { columns: headers, rows, sampleColumn, delimiter: csvDelimiter }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function computeGroups(
|
|
211
|
+
allSamples: string[],
|
|
212
|
+
columns: ColumnInfo[],
|
|
213
|
+
enabledFields: Set<number>,
|
|
214
|
+
outlierActions: Map<number, OutlierAction>,
|
|
215
|
+
delimiter: string,
|
|
216
|
+
minFieldCount: number,
|
|
217
|
+
): { groups: SampleGroup[]; metadata: MetadataRow[]; excludedSamples: string[] } {
|
|
218
|
+
const excludedSamples: string[] = []
|
|
219
|
+
const qcSamples: string[] = []
|
|
220
|
+
const conformingSamples: string[] = []
|
|
221
|
+
|
|
222
|
+
for (let i = 0; i < allSamples.length; i++) {
|
|
223
|
+
const action = outlierActions.get(i)
|
|
224
|
+
if (action === 'exclude') {
|
|
225
|
+
excludedSamples.push(allSamples[i])
|
|
226
|
+
} else if (action === 'qc') {
|
|
227
|
+
qcSamples.push(allSamples[i])
|
|
228
|
+
} else {
|
|
229
|
+
conformingSamples.push(allSamples[i])
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Build group map
|
|
234
|
+
const groupMap = new Map<string, string[]>()
|
|
235
|
+
const metadata: MetadataRow[] = []
|
|
236
|
+
const enabledIndices = [...enabledFields].sort((a, b) => a - b)
|
|
237
|
+
|
|
238
|
+
const suffixCount = minFieldCount - 1
|
|
239
|
+
|
|
240
|
+
for (const sample of conformingSamples) {
|
|
241
|
+
const parts = sample.split(delimiter)
|
|
242
|
+
const splitAt = Math.max(1, parts.length - suffixCount)
|
|
243
|
+
const row = [
|
|
244
|
+
parts.slice(0, splitAt).join(delimiter),
|
|
245
|
+
...parts.slice(splitAt),
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
// Build group key from enabled columns
|
|
249
|
+
const keyParts: string[] = []
|
|
250
|
+
for (const idx of enabledIndices) {
|
|
251
|
+
if (idx < row.length && idx < columns.length) {
|
|
252
|
+
keyParts.push(row[idx])
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const groupKey = keyParts.join(' / ')
|
|
256
|
+
|
|
257
|
+
if (!groupMap.has(groupKey)) {
|
|
258
|
+
groupMap.set(groupKey, [])
|
|
259
|
+
}
|
|
260
|
+
groupMap.get(groupKey)!.push(sample)
|
|
261
|
+
|
|
262
|
+
// Build metadata row with ALL columns
|
|
263
|
+
const fields: Record<string, string> = {}
|
|
264
|
+
for (const col of columns) {
|
|
265
|
+
if (col.index < row.length) {
|
|
266
|
+
fields[col.name] = row[col.index]
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
metadata.push({ sampleName: sample, fields, group: groupKey })
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Convert to SampleGroup[]
|
|
273
|
+
const groups: SampleGroup[] = []
|
|
274
|
+
let colorIdx = 0
|
|
275
|
+
for (const [name, samples] of groupMap) {
|
|
276
|
+
groups.push({
|
|
277
|
+
name,
|
|
278
|
+
color: DEFAULT_COLORS[colorIdx % DEFAULT_COLORS.length],
|
|
279
|
+
samples,
|
|
280
|
+
})
|
|
281
|
+
colorIdx++
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// QC group
|
|
285
|
+
if (qcSamples.length > 0) {
|
|
286
|
+
groups.push({
|
|
287
|
+
name: 'QC',
|
|
288
|
+
color: '#6B7280',
|
|
289
|
+
samples: qcSamples,
|
|
290
|
+
})
|
|
291
|
+
for (const sample of qcSamples) {
|
|
292
|
+
metadata.push({ sampleName: sample, fields: {}, group: 'QC' })
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return { groups, metadata, excludedSamples }
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Extract sample metadata from raw design_data into ParsedCsvData format.
|
|
301
|
+
*
|
|
302
|
+
* Looks for a `samples` array in the design data. For each sample, merges
|
|
303
|
+
* the `conditions` dict (the metadata table) with the `sample_name` to
|
|
304
|
+
* produce a flat tabular row. QC and blank samples are filtered out by
|
|
305
|
+
* their explicit `sample_type` field.
|
|
306
|
+
*
|
|
307
|
+
* Returns null if no samples with conditions are found.
|
|
308
|
+
*/
|
|
309
|
+
export function extractSamplesFromDesignData(
|
|
310
|
+
rawData: Record<string, unknown>,
|
|
311
|
+
): ParsedCsvData | null {
|
|
312
|
+
const samples = rawData.samples
|
|
313
|
+
if (!Array.isArray(samples) || samples.length === 0) return null
|
|
314
|
+
|
|
315
|
+
// Single pass: filter QC/blank and collect all condition keys
|
|
316
|
+
const allConditionKeys: string[] = []
|
|
317
|
+
const keySet = new Set<string>()
|
|
318
|
+
const filteredSamples: Record<string, unknown>[] = []
|
|
319
|
+
|
|
320
|
+
for (const sample of samples) {
|
|
321
|
+
const sampleType = String((sample as Record<string, unknown>).sample_type ?? 'sample').toLowerCase()
|
|
322
|
+
if (sampleType === 'qc' || sampleType === 'blank') continue
|
|
323
|
+
|
|
324
|
+
filteredSamples.push(sample as Record<string, unknown>)
|
|
325
|
+
const conditions = (sample as Record<string, unknown>).conditions as Record<string, string> | undefined
|
|
326
|
+
if (conditions && typeof conditions === 'object') {
|
|
327
|
+
for (const key of Object.keys(conditions)) {
|
|
328
|
+
if (!keySet.has(key)) {
|
|
329
|
+
keySet.add(key)
|
|
330
|
+
allConditionKeys.push(key)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (filteredSamples.length === 0 || allConditionKeys.length === 0) return null
|
|
337
|
+
|
|
338
|
+
const columns = ['sample_name', ...allConditionKeys]
|
|
339
|
+
const rows: Record<string, string>[] = filteredSamples.map((sample) => {
|
|
340
|
+
const conditions = (sample.conditions as Record<string, string>) ?? {}
|
|
341
|
+
const row: Record<string, string> = {
|
|
342
|
+
sample_name: String(sample.sample_name ?? ''),
|
|
343
|
+
}
|
|
344
|
+
for (const key of allConditionKeys) {
|
|
345
|
+
row[key] = conditions[key] ?? ''
|
|
346
|
+
}
|
|
347
|
+
return row
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
return { columns, rows, sampleColumn: 'sample_name', delimiter: ',' }
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function computeGroupsFromCsv(
|
|
354
|
+
csvData: ParsedCsvData,
|
|
355
|
+
columns: ColumnInfo[],
|
|
356
|
+
enabledFields: Set<number>,
|
|
357
|
+
): { groups: SampleGroup[]; metadata: MetadataRow[]; excludedSamples: string[] } {
|
|
358
|
+
const groupMap = new Map<string, string[]>()
|
|
359
|
+
const metadata: MetadataRow[] = []
|
|
360
|
+
const enabledCols = columns
|
|
361
|
+
.filter(c => enabledFields.has(c.index))
|
|
362
|
+
.sort((a, b) => a.index - b.index)
|
|
363
|
+
|
|
364
|
+
for (const row of csvData.rows) {
|
|
365
|
+
const sampleName = row[csvData.sampleColumn]
|
|
366
|
+
|
|
367
|
+
// Build group key from enabled CSV column values
|
|
368
|
+
// Use originalName for CSV row lookup (survives user renames), display name for group key
|
|
369
|
+
const keyParts = enabledCols.map(col => row[col.originalName ?? col.name])
|
|
370
|
+
const groupKey = keyParts.join(' / ')
|
|
371
|
+
|
|
372
|
+
if (!groupMap.has(groupKey)) {
|
|
373
|
+
groupMap.set(groupKey, [])
|
|
374
|
+
}
|
|
375
|
+
groupMap.get(groupKey)!.push(sampleName)
|
|
376
|
+
|
|
377
|
+
// Build metadata row with ALL columns — use display name as key, original for lookup
|
|
378
|
+
const fields: Record<string, string> = {}
|
|
379
|
+
for (const col of columns) {
|
|
380
|
+
fields[col.name] = row[col.originalName ?? col.name]
|
|
381
|
+
}
|
|
382
|
+
metadata.push({ sampleName, fields, group: groupKey })
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Convert to SampleGroup[]
|
|
386
|
+
const groups: SampleGroup[] = []
|
|
387
|
+
let colorIdx = 0
|
|
388
|
+
for (const [name, samples] of groupMap) {
|
|
389
|
+
groups.push({
|
|
390
|
+
name,
|
|
391
|
+
color: DEFAULT_COLORS[colorIdx % DEFAULT_COLORS.length],
|
|
392
|
+
samples,
|
|
393
|
+
})
|
|
394
|
+
colorIdx++
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return { groups, metadata, excludedSamples: [] }
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// --- Reactive composable ---
|
|
401
|
+
|
|
402
|
+
/** Parses sample names or CSV data to propose group assignments with outlier detection and preview. */
|
|
403
|
+
export function useAutoGroup() {
|
|
404
|
+
const inputMode = ref<InputMode>('paste')
|
|
405
|
+
const rawText = ref('')
|
|
406
|
+
const csvData = ref<ParsedCsvData | null>(null)
|
|
407
|
+
const delimiter = ref('_')
|
|
408
|
+
const dominantFieldCount = ref(1)
|
|
409
|
+
const minFieldCount = ref(1)
|
|
410
|
+
const outliers = ref<OutlierInfo[]>([])
|
|
411
|
+
const fields = ref<ColumnInfo[]>([])
|
|
412
|
+
const fieldNames = ref<Record<number, string>>({})
|
|
413
|
+
const enabledFields = ref(new Set<number>())
|
|
414
|
+
|
|
415
|
+
const isTabularMode = computed(() =>
|
|
416
|
+
(inputMode.value === 'csv' || inputMode.value === 'experiment') && csvData.value !== null,
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
const samples = computed(() => {
|
|
420
|
+
if (isTabularMode.value && csvData.value) {
|
|
421
|
+
return csvData.value.rows.map(r => r[csvData.value!.sampleColumn])
|
|
422
|
+
}
|
|
423
|
+
return rawText.value
|
|
424
|
+
.split('\n')
|
|
425
|
+
.map(l => l.trim())
|
|
426
|
+
.filter(l => l.length > 0)
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
const hasOutliers = computed(() => outliers.value.length > 0)
|
|
430
|
+
|
|
431
|
+
const conformingSamples = computed(() => {
|
|
432
|
+
const outlierIndices = new Set(outliers.value.map(o => o.index))
|
|
433
|
+
return samples.value.filter((_, i) => !outlierIndices.has(i))
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
const outlierActions = computed(() => {
|
|
437
|
+
const map = new Map<number, OutlierAction>()
|
|
438
|
+
for (const o of outliers.value) {
|
|
439
|
+
map.set(o.index, o.action)
|
|
440
|
+
}
|
|
441
|
+
return map
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
const effectiveColumns = computed(() => {
|
|
445
|
+
return fields.value.map(col => ({
|
|
446
|
+
...col,
|
|
447
|
+
name: fieldNames.value[col.index] ?? col.name,
|
|
448
|
+
}))
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
const _computedResult = computed((): AutoGroupResult => {
|
|
452
|
+
if (effectiveColumns.value.length === 0 || enabledFields.value.size === 0) {
|
|
453
|
+
return { groups: [], metadata: [], excludedSamples: [] }
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (isTabularMode.value && csvData.value) {
|
|
457
|
+
return computeGroupsFromCsv(
|
|
458
|
+
csvData.value,
|
|
459
|
+
effectiveColumns.value,
|
|
460
|
+
enabledFields.value,
|
|
461
|
+
)
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return computeGroups(
|
|
465
|
+
samples.value,
|
|
466
|
+
effectiveColumns.value,
|
|
467
|
+
enabledFields.value,
|
|
468
|
+
outlierActions.value,
|
|
469
|
+
delimiter.value,
|
|
470
|
+
minFieldCount.value,
|
|
471
|
+
)
|
|
472
|
+
})
|
|
473
|
+
|
|
474
|
+
const groups = computed(() => _computedResult.value.groups)
|
|
475
|
+
const metadata = computed(() => _computedResult.value.metadata)
|
|
476
|
+
const excludedSamples = computed(() => _computedResult.value.excludedSamples)
|
|
477
|
+
|
|
478
|
+
const allSingletons = computed(() =>
|
|
479
|
+
groups.value.length > 1 && groups.value.every(g => g.samples.length === 1),
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
const result = _computedResult
|
|
483
|
+
|
|
484
|
+
function parseInput() {
|
|
485
|
+
if (isTabularMode.value) {
|
|
486
|
+
parseCsvInput()
|
|
487
|
+
} else {
|
|
488
|
+
parsePasteInput()
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function parsePasteInput() {
|
|
493
|
+
const lines = samples.value
|
|
494
|
+
if (lines.length === 0) return
|
|
495
|
+
|
|
496
|
+
const analysis = analyzeDelimiter(lines)
|
|
497
|
+
delimiter.value = analysis.delimiter
|
|
498
|
+
dominantFieldCount.value = analysis.dominantFieldCount
|
|
499
|
+
|
|
500
|
+
// Use dominantFieldCount as outlier threshold so QC/test samples with
|
|
501
|
+
// fewer fields than the majority are correctly flagged
|
|
502
|
+
outliers.value = detectOutliers(lines, analysis.delimiter, analysis.dominantFieldCount)
|
|
503
|
+
|
|
504
|
+
// Apply smart default actions: auto-classify QC/test samples
|
|
505
|
+
for (const outlier of outliers.value) {
|
|
506
|
+
outlier.action = classifyOutlierAction(outlier.sample, analysis.delimiter)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const conforming = lines.filter(
|
|
510
|
+
(_, i) => !outliers.value.some(o => o.index === i)
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
// Recompute minFieldCount from conforming samples only
|
|
514
|
+
const conformingFieldCounts = conforming.map(s => s.split(analysis.delimiter).length)
|
|
515
|
+
minFieldCount.value = conformingFieldCounts.length > 0
|
|
516
|
+
? Math.min(...conformingFieldCounts)
|
|
517
|
+
: analysis.dominantFieldCount
|
|
518
|
+
|
|
519
|
+
fields.value = extractColumns(conforming, analysis.delimiter, minFieldCount.value)
|
|
520
|
+
|
|
521
|
+
fieldNames.value = {}
|
|
522
|
+
const rowCount = conforming.length
|
|
523
|
+
enabledFields.value = new Set(
|
|
524
|
+
fields.value.filter(f => isUsefulField(f, rowCount)).map(f => f.index),
|
|
525
|
+
)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function parseCsvInput() {
|
|
529
|
+
if (!csvData.value) return
|
|
530
|
+
|
|
531
|
+
const csv = csvData.value
|
|
532
|
+
const nonSampleCols = csv.columns.filter(c => c !== csv.sampleColumn)
|
|
533
|
+
|
|
534
|
+
fields.value = nonSampleCols.map((col, i) => {
|
|
535
|
+
const values = csv.rows.map(r => r[col])
|
|
536
|
+
const unique = [...new Set(values)]
|
|
537
|
+
return {
|
|
538
|
+
index: i,
|
|
539
|
+
name: col,
|
|
540
|
+
originalName: col,
|
|
541
|
+
uniqueValues: unique,
|
|
542
|
+
cardinality: unique.length,
|
|
543
|
+
}
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
// For CSV, no outliers
|
|
547
|
+
outliers.value = []
|
|
548
|
+
delimiter.value = csv.delimiter
|
|
549
|
+
dominantFieldCount.value = csv.columns.length
|
|
550
|
+
|
|
551
|
+
fieldNames.value = Object.fromEntries(fields.value.map(f => [f.index, f.name]))
|
|
552
|
+
const rowCount = csv.rows.length
|
|
553
|
+
enabledFields.value = new Set(
|
|
554
|
+
fields.value.filter(f => isUsefulField(f, rowCount)).map(f => f.index),
|
|
555
|
+
)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function setOutlierAction(index: number, action: OutlierAction) {
|
|
559
|
+
const outlier = outliers.value.find(o => o.index === index)
|
|
560
|
+
if (outlier) {
|
|
561
|
+
outlier.action = action
|
|
562
|
+
// Trigger reactivity
|
|
563
|
+
outliers.value = [...outliers.value]
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function setAllOutlierActions(action: OutlierAction) {
|
|
568
|
+
for (const outlier of outliers.value) {
|
|
569
|
+
outlier.action = action
|
|
570
|
+
}
|
|
571
|
+
outliers.value = [...outliers.value]
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function toggleField(index: number) {
|
|
575
|
+
const newSet = new Set(enabledFields.value)
|
|
576
|
+
if (newSet.has(index)) {
|
|
577
|
+
newSet.delete(index)
|
|
578
|
+
} else {
|
|
579
|
+
newSet.add(index)
|
|
580
|
+
}
|
|
581
|
+
enabledFields.value = newSet
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function renameField(index: number, name: string) {
|
|
585
|
+
fieldNames.value = { ...fieldNames.value, [index]: name }
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function loadExperimentData(rawData: Record<string, unknown>): boolean {
|
|
589
|
+
const parsed = extractSamplesFromDesignData(rawData)
|
|
590
|
+
if (!parsed) return false
|
|
591
|
+
|
|
592
|
+
inputMode.value = 'experiment'
|
|
593
|
+
csvData.value = parsed
|
|
594
|
+
parseCsvInput()
|
|
595
|
+
return true
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function reset() {
|
|
599
|
+
rawText.value = ''
|
|
600
|
+
csvData.value = null
|
|
601
|
+
delimiter.value = '_'
|
|
602
|
+
dominantFieldCount.value = 1
|
|
603
|
+
minFieldCount.value = 1
|
|
604
|
+
outliers.value = []
|
|
605
|
+
fields.value = []
|
|
606
|
+
fieldNames.value = {}
|
|
607
|
+
enabledFields.value = new Set()
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return {
|
|
611
|
+
// State
|
|
612
|
+
inputMode,
|
|
613
|
+
rawText,
|
|
614
|
+
csvData,
|
|
615
|
+
delimiter,
|
|
616
|
+
dominantFieldCount,
|
|
617
|
+
minFieldCount,
|
|
618
|
+
outliers,
|
|
619
|
+
fields,
|
|
620
|
+
fieldNames,
|
|
621
|
+
enabledFields,
|
|
622
|
+
// Computed
|
|
623
|
+
samples,
|
|
624
|
+
hasOutliers,
|
|
625
|
+
conformingSamples,
|
|
626
|
+
groups,
|
|
627
|
+
metadata,
|
|
628
|
+
excludedSamples,
|
|
629
|
+
allSingletons,
|
|
630
|
+
result,
|
|
631
|
+
effectiveColumns,
|
|
632
|
+
// Actions
|
|
633
|
+
parseInput,
|
|
634
|
+
loadExperimentData,
|
|
635
|
+
setOutlierAction,
|
|
636
|
+
setAllOutlierActions,
|
|
637
|
+
toggleField,
|
|
638
|
+
renameField,
|
|
639
|
+
reset,
|
|
640
|
+
}
|
|
641
|
+
}
|