easy_ml 0.1.4 → 0.2.0.pre.rc1
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.
- checksums.yaml +4 -4
- data/README.md +234 -26
- data/Rakefile +45 -0
- data/app/controllers/easy_ml/application_controller.rb +67 -0
- data/app/controllers/easy_ml/columns_controller.rb +38 -0
- data/app/controllers/easy_ml/datasets_controller.rb +156 -0
- data/app/controllers/easy_ml/datasources_controller.rb +88 -0
- data/app/controllers/easy_ml/deploys_controller.rb +20 -0
- data/app/controllers/easy_ml/models_controller.rb +151 -0
- data/app/controllers/easy_ml/retraining_runs_controller.rb +19 -0
- data/app/controllers/easy_ml/settings_controller.rb +59 -0
- data/app/frontend/components/AlertProvider.tsx +108 -0
- data/app/frontend/components/DatasetPreview.tsx +161 -0
- data/app/frontend/components/EmptyState.tsx +28 -0
- data/app/frontend/components/ModelCard.tsx +255 -0
- data/app/frontend/components/ModelDetails.tsx +334 -0
- data/app/frontend/components/ModelForm.tsx +384 -0
- data/app/frontend/components/Navigation.tsx +300 -0
- data/app/frontend/components/Pagination.tsx +72 -0
- data/app/frontend/components/Popover.tsx +55 -0
- data/app/frontend/components/PredictionStream.tsx +105 -0
- data/app/frontend/components/ScheduleModal.tsx +726 -0
- data/app/frontend/components/SearchInput.tsx +23 -0
- data/app/frontend/components/SearchableSelect.tsx +132 -0
- data/app/frontend/components/dataset/AutosaveIndicator.tsx +39 -0
- data/app/frontend/components/dataset/ColumnConfigModal.tsx +431 -0
- data/app/frontend/components/dataset/ColumnFilters.tsx +256 -0
- data/app/frontend/components/dataset/ColumnList.tsx +101 -0
- data/app/frontend/components/dataset/FeatureConfigPopover.tsx +57 -0
- data/app/frontend/components/dataset/FeaturePicker.tsx +205 -0
- data/app/frontend/components/dataset/PreprocessingConfig.tsx +704 -0
- data/app/frontend/components/dataset/SplitConfigurator.tsx +120 -0
- data/app/frontend/components/dataset/splitters/DateSplitter.tsx +58 -0
- data/app/frontend/components/dataset/splitters/KFoldSplitter.tsx +68 -0
- data/app/frontend/components/dataset/splitters/LeavePOutSplitter.tsx +29 -0
- data/app/frontend/components/dataset/splitters/PredefinedSplitter.tsx +146 -0
- data/app/frontend/components/dataset/splitters/RandomSplitter.tsx +85 -0
- data/app/frontend/components/dataset/splitters/StratifiedSplitter.tsx +79 -0
- data/app/frontend/components/dataset/splitters/constants.ts +77 -0
- data/app/frontend/components/dataset/splitters/types.ts +168 -0
- data/app/frontend/components/dataset/splitters/utils.ts +53 -0
- data/app/frontend/components/features/CodeEditor.tsx +46 -0
- data/app/frontend/components/features/DataPreview.tsx +150 -0
- data/app/frontend/components/features/FeatureCard.tsx +88 -0
- data/app/frontend/components/features/FeatureForm.tsx +235 -0
- data/app/frontend/components/features/FeatureGroupCard.tsx +54 -0
- data/app/frontend/components/settings/PluginSettings.tsx +81 -0
- data/app/frontend/components/ui/badge.tsx +44 -0
- data/app/frontend/components/ui/collapsible.tsx +9 -0
- data/app/frontend/components/ui/scroll-area.tsx +46 -0
- data/app/frontend/components/ui/separator.tsx +29 -0
- data/app/frontend/entrypoints/App.tsx +40 -0
- data/app/frontend/entrypoints/Application.tsx +24 -0
- data/app/frontend/hooks/useAutosave.ts +61 -0
- data/app/frontend/layouts/Layout.tsx +38 -0
- data/app/frontend/lib/utils.ts +6 -0
- data/app/frontend/mockData.ts +272 -0
- data/app/frontend/pages/DatasetDetailsPage.tsx +103 -0
- data/app/frontend/pages/DatasetsPage.tsx +261 -0
- data/app/frontend/pages/DatasourceFormPage.tsx +147 -0
- data/app/frontend/pages/DatasourcesPage.tsx +261 -0
- data/app/frontend/pages/EditModelPage.tsx +45 -0
- data/app/frontend/pages/EditTransformationPage.tsx +56 -0
- data/app/frontend/pages/ModelsPage.tsx +115 -0
- data/app/frontend/pages/NewDatasetPage.tsx +366 -0
- data/app/frontend/pages/NewModelPage.tsx +45 -0
- data/app/frontend/pages/NewTransformationPage.tsx +43 -0
- data/app/frontend/pages/SettingsPage.tsx +272 -0
- data/app/frontend/pages/ShowModelPage.tsx +30 -0
- data/app/frontend/pages/TransformationsPage.tsx +95 -0
- data/app/frontend/styles/application.css +100 -0
- data/app/frontend/types/dataset.ts +146 -0
- data/app/frontend/types/datasource.ts +33 -0
- data/app/frontend/types/preprocessing.ts +1 -0
- data/app/frontend/types.ts +113 -0
- data/app/helpers/easy_ml/application_helper.rb +10 -0
- data/app/jobs/easy_ml/application_job.rb +21 -0
- data/app/jobs/easy_ml/batch_job.rb +46 -0
- data/app/jobs/easy_ml/compute_feature_job.rb +19 -0
- data/app/jobs/easy_ml/deploy_job.rb +13 -0
- data/app/jobs/easy_ml/finalize_feature_job.rb +15 -0
- data/app/jobs/easy_ml/refresh_dataset_job.rb +32 -0
- data/app/jobs/easy_ml/schedule_retraining_job.rb +11 -0
- data/app/jobs/easy_ml/sync_datasource_job.rb +17 -0
- data/app/jobs/easy_ml/training_job.rb +62 -0
- data/app/models/easy_ml/adapters/base_adapter.rb +45 -0
- data/app/models/easy_ml/adapters/polars_adapter.rb +77 -0
- data/app/models/easy_ml/cleaner.rb +82 -0
- data/app/models/easy_ml/column.rb +124 -0
- data/app/models/easy_ml/column_history.rb +30 -0
- data/app/models/easy_ml/column_list.rb +122 -0
- data/app/models/easy_ml/concerns/configurable.rb +61 -0
- data/app/models/easy_ml/concerns/versionable.rb +19 -0
- data/app/models/easy_ml/dataset.rb +767 -0
- data/app/models/easy_ml/dataset_history.rb +56 -0
- data/app/models/easy_ml/datasource.rb +182 -0
- data/app/models/easy_ml/datasource_history.rb +24 -0
- data/app/models/easy_ml/datasources/base_datasource.rb +54 -0
- data/app/models/easy_ml/datasources/file_datasource.rb +58 -0
- data/app/models/easy_ml/datasources/polars_datasource.rb +89 -0
- data/app/models/easy_ml/datasources/s3_datasource.rb +97 -0
- data/app/models/easy_ml/deploy.rb +114 -0
- data/app/models/easy_ml/event.rb +79 -0
- data/app/models/easy_ml/feature.rb +437 -0
- data/app/models/easy_ml/feature_history.rb +38 -0
- data/app/models/easy_ml/model.rb +575 -41
- data/app/models/easy_ml/model_file.rb +133 -0
- data/app/models/easy_ml/model_file_history.rb +24 -0
- data/app/models/easy_ml/model_history.rb +51 -0
- data/app/models/easy_ml/models/base_model.rb +58 -0
- data/app/models/easy_ml/models/hyperparameters/base.rb +99 -0
- data/app/models/easy_ml/models/hyperparameters/xgboost/dart.rb +82 -0
- data/app/models/easy_ml/models/hyperparameters/xgboost/gblinear.rb +82 -0
- data/app/models/easy_ml/models/hyperparameters/xgboost/gbtree.rb +97 -0
- data/app/models/easy_ml/models/hyperparameters/xgboost.rb +71 -0
- data/app/models/easy_ml/models/xgboost/evals_callback.rb +138 -0
- data/app/models/easy_ml/models/xgboost/progress_callback.rb +39 -0
- data/app/models/easy_ml/models/xgboost.rb +544 -5
- data/app/models/easy_ml/prediction.rb +44 -0
- data/app/models/easy_ml/retraining_job.rb +278 -0
- data/app/models/easy_ml/retraining_run.rb +184 -0
- data/app/models/easy_ml/settings.rb +37 -0
- data/app/models/easy_ml/splitter.rb +90 -0
- data/app/models/easy_ml/splitters/base_splitter.rb +28 -0
- data/app/models/easy_ml/splitters/date_splitter.rb +91 -0
- data/app/models/easy_ml/splitters/predefined_splitter.rb +74 -0
- data/app/models/easy_ml/splitters/random_splitter.rb +82 -0
- data/app/models/easy_ml/tuner_job.rb +56 -0
- data/app/models/easy_ml/tuner_run.rb +31 -0
- data/app/models/splitter_history.rb +6 -0
- data/app/serializers/easy_ml/column_serializer.rb +27 -0
- data/app/serializers/easy_ml/dataset_serializer.rb +73 -0
- data/app/serializers/easy_ml/datasource_serializer.rb +64 -0
- data/app/serializers/easy_ml/feature_serializer.rb +27 -0
- data/app/serializers/easy_ml/model_serializer.rb +90 -0
- data/app/serializers/easy_ml/retraining_job_serializer.rb +22 -0
- data/app/serializers/easy_ml/retraining_run_serializer.rb +39 -0
- data/app/serializers/easy_ml/settings_serializer.rb +9 -0
- data/app/views/layouts/easy_ml/application.html.erb +15 -0
- data/config/initializers/resque.rb +3 -0
- data/config/resque-pool.yml +6 -0
- data/config/routes.rb +39 -0
- data/config/spring.rb +1 -0
- data/config/vite.json +15 -0
- data/lib/easy_ml/configuration.rb +64 -0
- data/lib/easy_ml/core/evaluators/base_evaluator.rb +53 -0
- data/lib/easy_ml/core/evaluators/classification_evaluators.rb +126 -0
- data/lib/easy_ml/core/evaluators/regression_evaluators.rb +66 -0
- data/lib/easy_ml/core/model_evaluator.rb +161 -89
- data/lib/easy_ml/core/tuner/adapters/base_adapter.rb +28 -18
- data/lib/easy_ml/core/tuner/adapters/xgboost_adapter.rb +4 -25
- data/lib/easy_ml/core/tuner.rb +123 -62
- data/lib/easy_ml/core.rb +0 -3
- data/lib/easy_ml/core_ext/hash.rb +24 -0
- data/lib/easy_ml/core_ext/pathname.rb +11 -5
- data/lib/easy_ml/data/date_converter.rb +90 -0
- data/lib/easy_ml/data/filter_extensions.rb +31 -0
- data/lib/easy_ml/data/polars_column.rb +126 -0
- data/lib/easy_ml/data/polars_reader.rb +297 -0
- data/lib/easy_ml/data/preprocessor.rb +280 -142
- data/lib/easy_ml/data/simple_imputer.rb +255 -0
- data/lib/easy_ml/data/splits/file_split.rb +252 -0
- data/lib/easy_ml/data/splits/in_memory_split.rb +54 -0
- data/lib/easy_ml/data/splits/split.rb +95 -0
- data/lib/easy_ml/data/splits.rb +9 -0
- data/lib/easy_ml/data/statistics_learner.rb +93 -0
- data/lib/easy_ml/data/synced_directory.rb +341 -0
- data/lib/easy_ml/data.rb +6 -2
- data/lib/easy_ml/engine.rb +105 -6
- data/lib/easy_ml/feature_store.rb +227 -0
- data/lib/easy_ml/features.rb +61 -0
- data/lib/easy_ml/initializers/inflections.rb +17 -3
- data/lib/easy_ml/logging.rb +2 -2
- data/lib/easy_ml/predict.rb +74 -0
- data/lib/easy_ml/railtie/generators/migration/migration_generator.rb +192 -36
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_column_histories.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_columns.rb.tt +25 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_dataset_histories.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasets.rb.tt +31 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasource_histories.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasources.rb.tt +16 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_deploys.rb.tt +24 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_events.rb.tt +20 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_feature_histories.rb.tt +14 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_features.rb.tt +32 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_file_histories.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_files.rb.tt +17 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_histories.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_models.rb.tt +20 -9
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_predictions.rb.tt +17 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_retraining_jobs.rb.tt +77 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_settings.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_splitter_histories.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_splitters.rb.tt +15 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_tuner_jobs.rb.tt +40 -0
- data/lib/easy_ml/support/est.rb +5 -1
- data/lib/easy_ml/support/file_rotate.rb +79 -15
- data/lib/easy_ml/support/file_support.rb +9 -0
- data/lib/easy_ml/support/local_file.rb +24 -0
- data/lib/easy_ml/support/lockable.rb +62 -0
- data/lib/easy_ml/support/synced_file.rb +103 -0
- data/lib/easy_ml/support/utc.rb +5 -1
- data/lib/easy_ml/support.rb +6 -3
- data/lib/easy_ml/version.rb +4 -1
- data/lib/easy_ml.rb +7 -2
- metadata +355 -72
- data/app/models/easy_ml/models.rb +0 -5
- data/lib/easy_ml/core/model.rb +0 -30
- data/lib/easy_ml/core/model_core.rb +0 -181
- data/lib/easy_ml/core/models/hyperparameters/base.rb +0 -34
- data/lib/easy_ml/core/models/hyperparameters/xgboost.rb +0 -19
- data/lib/easy_ml/core/models/xgboost.rb +0 -10
- data/lib/easy_ml/core/models/xgboost_core.rb +0 -220
- data/lib/easy_ml/core/models.rb +0 -10
- data/lib/easy_ml/core/uploaders/model_uploader.rb +0 -24
- data/lib/easy_ml/core/uploaders.rb +0 -7
- data/lib/easy_ml/data/dataloader.rb +0 -6
- data/lib/easy_ml/data/dataset/data/preprocessor/statistics.json +0 -31
- data/lib/easy_ml/data/dataset/data/sample_info.json +0 -1
- data/lib/easy_ml/data/dataset/dataset/files/sample_info.json +0 -1
- data/lib/easy_ml/data/dataset/splits/file_split.rb +0 -140
- data/lib/easy_ml/data/dataset/splits/in_memory_split.rb +0 -49
- data/lib/easy_ml/data/dataset/splits/split.rb +0 -98
- data/lib/easy_ml/data/dataset/splits.rb +0 -11
- data/lib/easy_ml/data/dataset/splitters/date_splitter.rb +0 -43
- data/lib/easy_ml/data/dataset/splitters.rb +0 -9
- data/lib/easy_ml/data/dataset.rb +0 -430
- data/lib/easy_ml/data/datasource/datasource_factory.rb +0 -60
- data/lib/easy_ml/data/datasource/file_datasource.rb +0 -40
- data/lib/easy_ml/data/datasource/merged_datasource.rb +0 -64
- data/lib/easy_ml/data/datasource/polars_datasource.rb +0 -41
- data/lib/easy_ml/data/datasource/s3_datasource.rb +0 -89
- data/lib/easy_ml/data/datasource.rb +0 -33
- data/lib/easy_ml/data/preprocessor/preprocessor.rb +0 -205
- data/lib/easy_ml/data/preprocessor/simple_imputer.rb +0 -402
- data/lib/easy_ml/deployment.rb +0 -5
- data/lib/easy_ml/support/synced_directory.rb +0 -134
- data/lib/easy_ml/transforms.rb +0 -29
- /data/{lib/easy_ml/core → app/models/easy_ml}/models/hyperparameters.rb +0 -0
@@ -0,0 +1,23 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Search } from 'lucide-react';
|
3
|
+
|
4
|
+
interface SearchInputProps {
|
5
|
+
value: string;
|
6
|
+
onChange: (value: string) => void;
|
7
|
+
placeholder?: string;
|
8
|
+
}
|
9
|
+
|
10
|
+
export function SearchInput({ value, onChange, placeholder = 'Search...' }: SearchInputProps) {
|
11
|
+
return (
|
12
|
+
<div className="relative">
|
13
|
+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
14
|
+
<input
|
15
|
+
type="text"
|
16
|
+
value={value}
|
17
|
+
onChange={(e) => onChange(e.target.value)}
|
18
|
+
placeholder={placeholder}
|
19
|
+
className="pl-9 pr-4 py-2 w-64 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
|
20
|
+
/>
|
21
|
+
</div>
|
22
|
+
);
|
23
|
+
}
|
@@ -0,0 +1,132 @@
|
|
1
|
+
import React, { useState, useRef, useEffect, forwardRef } from 'react';
|
2
|
+
import { Search, Check } from 'lucide-react';
|
3
|
+
|
4
|
+
interface Option {
|
5
|
+
value: string | number;
|
6
|
+
label: string;
|
7
|
+
description?: string;
|
8
|
+
metadata?: Record<string, any>;
|
9
|
+
}
|
10
|
+
|
11
|
+
interface SearchableSelectProps {
|
12
|
+
options: Option[];
|
13
|
+
value: Option['value'] | null;
|
14
|
+
onChange: (value: Option['value']) => void;
|
15
|
+
placeholder?: string;
|
16
|
+
renderOption?: (option: Option) => React.ReactNode;
|
17
|
+
}
|
18
|
+
|
19
|
+
export const SearchableSelect = forwardRef<HTMLButtonElement, SearchableSelectProps>(
|
20
|
+
({ options, value, onChange, placeholder = 'Search...', renderOption }, ref) => {
|
21
|
+
const [isOpen, setIsOpen] = useState(false);
|
22
|
+
const [searchQuery, setSearchQuery] = useState('');
|
23
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
24
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
25
|
+
|
26
|
+
const selectedOption = options.find(opt => opt.value === value);
|
27
|
+
|
28
|
+
const filteredOptions = options.filter(option =>
|
29
|
+
option.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
30
|
+
option.description?.toLowerCase().includes(searchQuery.toLowerCase())
|
31
|
+
);
|
32
|
+
|
33
|
+
useEffect(() => {
|
34
|
+
function handleClickOutside(event: MouseEvent) {
|
35
|
+
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
36
|
+
setIsOpen(false);
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
document.addEventListener('mousedown', handleClickOutside);
|
41
|
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
42
|
+
}, []);
|
43
|
+
|
44
|
+
useEffect(() => {
|
45
|
+
if (isOpen && inputRef.current) {
|
46
|
+
inputRef.current.focus();
|
47
|
+
}
|
48
|
+
}, [isOpen]);
|
49
|
+
|
50
|
+
return (
|
51
|
+
<div className="relative" ref={containerRef}>
|
52
|
+
<button
|
53
|
+
type="button"
|
54
|
+
onClick={() => setIsOpen(!isOpen)}
|
55
|
+
className="w-full bg-white relative border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-pointer focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
56
|
+
ref={ref}
|
57
|
+
>
|
58
|
+
{selectedOption ? (
|
59
|
+
<span className="block truncate">{selectedOption.label}</span>
|
60
|
+
) : (
|
61
|
+
<span className="block truncate text-gray-500">{placeholder}</span>
|
62
|
+
)}
|
63
|
+
</button>
|
64
|
+
|
65
|
+
{isOpen && (
|
66
|
+
<div className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-96 rounded-md overflow-hidden">
|
67
|
+
<div className="p-2 border-b">
|
68
|
+
<div className="relative">
|
69
|
+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
70
|
+
<input
|
71
|
+
ref={inputRef}
|
72
|
+
type="text"
|
73
|
+
className="w-full pl-9 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
74
|
+
placeholder="Search..."
|
75
|
+
value={searchQuery}
|
76
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
77
|
+
onClick={(e) => e.stopPropagation()}
|
78
|
+
/>
|
79
|
+
</div>
|
80
|
+
</div>
|
81
|
+
|
82
|
+
<div className="max-h-60 overflow-y-auto">
|
83
|
+
{filteredOptions.length === 0 ? (
|
84
|
+
<div className="text-center py-4 text-sm text-gray-500">
|
85
|
+
No results found
|
86
|
+
</div>
|
87
|
+
) : (
|
88
|
+
<ul className="py-1">
|
89
|
+
{filteredOptions.map((option) => (
|
90
|
+
<li key={option.value}>
|
91
|
+
<button
|
92
|
+
type="button"
|
93
|
+
className={`w-full text-left px-4 py-2 hover:bg-gray-100 ${
|
94
|
+
option.value === value ? 'bg-blue-50' : ''
|
95
|
+
}`}
|
96
|
+
onClick={() => {
|
97
|
+
onChange(option.value);
|
98
|
+
setIsOpen(false);
|
99
|
+
setSearchQuery('');
|
100
|
+
}}
|
101
|
+
>
|
102
|
+
{renderOption ? (
|
103
|
+
renderOption(option)
|
104
|
+
) : (
|
105
|
+
<div className="flex items-center justify-between">
|
106
|
+
<div>
|
107
|
+
<div className="font-medium">{option.label}</div>
|
108
|
+
{option.description && (
|
109
|
+
<div className="text-sm text-gray-500">
|
110
|
+
{option.description}
|
111
|
+
</div>
|
112
|
+
)}
|
113
|
+
</div>
|
114
|
+
{option.value === value && (
|
115
|
+
<Check className="w-4 h-4 text-blue-600" />
|
116
|
+
)}
|
117
|
+
</div>
|
118
|
+
)}
|
119
|
+
</button>
|
120
|
+
</li>
|
121
|
+
))}
|
122
|
+
</ul>
|
123
|
+
)}
|
124
|
+
</div>
|
125
|
+
</div>
|
126
|
+
)}
|
127
|
+
</div>
|
128
|
+
);
|
129
|
+
}
|
130
|
+
);
|
131
|
+
|
132
|
+
SearchableSelect.displayName = 'SearchableSelect';
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Save, AlertCircle, Loader2 } from 'lucide-react';
|
3
|
+
|
4
|
+
interface AutosaveIndicatorProps {
|
5
|
+
saving: boolean;
|
6
|
+
saved: boolean;
|
7
|
+
error: string | null;
|
8
|
+
}
|
9
|
+
|
10
|
+
export function AutosaveIndicator({ saving, saved, error }: AutosaveIndicatorProps) {
|
11
|
+
if (error) {
|
12
|
+
return (
|
13
|
+
<div className="flex items-center gap-2 text-red-600">
|
14
|
+
<AlertCircle className="w-4 h-4" />
|
15
|
+
<span className="text-sm font-medium">{error}</span>
|
16
|
+
</div>
|
17
|
+
);
|
18
|
+
}
|
19
|
+
|
20
|
+
if (saving) {
|
21
|
+
return (
|
22
|
+
<div className="flex items-center gap-2 text-blue-600">
|
23
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
24
|
+
<span className="text-sm font-medium">Saving changes...</span>
|
25
|
+
</div>
|
26
|
+
);
|
27
|
+
}
|
28
|
+
|
29
|
+
if (saved) {
|
30
|
+
return (
|
31
|
+
<div className="flex items-center gap-2 text-green-600">
|
32
|
+
<Save className="w-4 h-4" />
|
33
|
+
<span className="text-sm font-medium">Changes saved</span>
|
34
|
+
</div>
|
35
|
+
);
|
36
|
+
}
|
37
|
+
|
38
|
+
return null;
|
39
|
+
}
|
@@ -0,0 +1,431 @@
|
|
1
|
+
import React, { useState, useMemo, useCallback, useEffect } from "react";
|
2
|
+
import {
|
3
|
+
X,
|
4
|
+
Settings2,
|
5
|
+
AlertCircle,
|
6
|
+
Target,
|
7
|
+
EyeOff,
|
8
|
+
Search,
|
9
|
+
Wand2,
|
10
|
+
Play,
|
11
|
+
Loader2,
|
12
|
+
Sparkles,
|
13
|
+
} from "lucide-react";
|
14
|
+
import { PreprocessingConfig } from "./PreprocessingConfig";
|
15
|
+
import { ColumnList } from "./ColumnList";
|
16
|
+
import { ColumnFilters } from "./ColumnFilters";
|
17
|
+
import { AutosaveIndicator } from "./AutosaveIndicator";
|
18
|
+
import { SearchableSelect } from "../SearchableSelect";
|
19
|
+
import { useAutosave } from "../../hooks/useAutosave";
|
20
|
+
import { Dataset, Column, Feature } from "../../types/dataset";
|
21
|
+
import type { PreprocessingStep } from "../../types/dataset";
|
22
|
+
import { FeaturePicker } from "./FeaturePicker";
|
23
|
+
import { router } from "@inertiajs/react";
|
24
|
+
|
25
|
+
interface ColumnConfig {
|
26
|
+
targetColumn?: string;
|
27
|
+
}
|
28
|
+
|
29
|
+
interface ColumnConfigModalProps {
|
30
|
+
isOpen: boolean;
|
31
|
+
onClose: () => void;
|
32
|
+
initialDataset: Dataset;
|
33
|
+
onSave: (dataset: Dataset) => Promise<void>;
|
34
|
+
constants: any;
|
35
|
+
}
|
36
|
+
|
37
|
+
export function ColumnConfigModal({
|
38
|
+
isOpen,
|
39
|
+
onClose,
|
40
|
+
initialDataset,
|
41
|
+
onSave,
|
42
|
+
constants,
|
43
|
+
}: ColumnConfigModalProps) {
|
44
|
+
const [dataset, setDataset] = useState<Dataset>(initialDataset);
|
45
|
+
const [activeTab, setActiveTab] = useState<"columns" | "features">(
|
46
|
+
"columns"
|
47
|
+
);
|
48
|
+
const [isApplying, setIsApplying] = useState(false);
|
49
|
+
const [config, setConfig] = useState<ColumnConfig>({
|
50
|
+
targetColumn: dataset.target,
|
51
|
+
});
|
52
|
+
const [selectedColumn, setSelectedColumn] = useState<string | null>(null);
|
53
|
+
const [searchQuery, setSearchQuery] = useState("");
|
54
|
+
const [activeFilters, setActiveFilters] = useState<{
|
55
|
+
view: "all" | "training" | "hidden" | "preprocessed" | "nulls";
|
56
|
+
types: string[];
|
57
|
+
}>({
|
58
|
+
view: "all",
|
59
|
+
types: [],
|
60
|
+
});
|
61
|
+
const [needsRefresh, setNeedsRefresh] = useState(
|
62
|
+
initialDataset.needs_refresh || false
|
63
|
+
);
|
64
|
+
|
65
|
+
const handleSave = useCallback(
|
66
|
+
async (data: Dataset) => {
|
67
|
+
await onSave(data);
|
68
|
+
},
|
69
|
+
[onSave]
|
70
|
+
);
|
71
|
+
|
72
|
+
const { saving, saved, error } = useAutosave(dataset, handleSave, 2000);
|
73
|
+
|
74
|
+
const colHasPreprocessingSteps = (col: Column) => {
|
75
|
+
return (
|
76
|
+
col.preprocessing_steps?.training != null &&
|
77
|
+
col.preprocessing_steps?.training?.method !== "none"
|
78
|
+
);
|
79
|
+
};
|
80
|
+
|
81
|
+
const filteredColumns = useMemo(() => {
|
82
|
+
return dataset.columns.filter((column) => {
|
83
|
+
const matchesSearch = column.name
|
84
|
+
.toLowerCase()
|
85
|
+
.includes(searchQuery.toLowerCase());
|
86
|
+
const matchesType =
|
87
|
+
activeFilters.types.length === 0 ||
|
88
|
+
activeFilters.types.includes(column.datatype);
|
89
|
+
|
90
|
+
const matchesView = (() => {
|
91
|
+
switch (activeFilters.view) {
|
92
|
+
case "training":
|
93
|
+
return !column.hidden && !column.drop_if_null;
|
94
|
+
case "hidden":
|
95
|
+
return column.hidden;
|
96
|
+
case "preprocessed":
|
97
|
+
return colHasPreprocessingSteps(column);
|
98
|
+
case "nulls":
|
99
|
+
return (column.statistics?.processed?.null_count || 0) > 0;
|
100
|
+
default:
|
101
|
+
return true;
|
102
|
+
}
|
103
|
+
})();
|
104
|
+
|
105
|
+
return matchesSearch && matchesType && matchesView;
|
106
|
+
});
|
107
|
+
}, [dataset.columns, searchQuery, activeFilters]);
|
108
|
+
|
109
|
+
const columnStats = useMemo(
|
110
|
+
() => ({
|
111
|
+
total: dataset.columns.length,
|
112
|
+
filtered: filteredColumns.length,
|
113
|
+
training: dataset.columns.filter((c) => !c.hidden && !c.drop_if_null)
|
114
|
+
.length,
|
115
|
+
hidden: dataset.columns.filter((c) => c.hidden).length,
|
116
|
+
withPreprocessing: dataset.columns.filter(colHasPreprocessingSteps)
|
117
|
+
.length,
|
118
|
+
withNulls: dataset.columns.filter(
|
119
|
+
(c) => (c.statistics?.processed?.null_count || 0) > 0
|
120
|
+
).length,
|
121
|
+
}),
|
122
|
+
[dataset.columns, filteredColumns]
|
123
|
+
);
|
124
|
+
|
125
|
+
const columnTypes = useMemo(
|
126
|
+
() => Array.from(new Set(dataset.columns.map((c) => c.datatype))),
|
127
|
+
[dataset.columns]
|
128
|
+
);
|
129
|
+
|
130
|
+
const handleColumnSelect = (columnName: string) => {
|
131
|
+
setSelectedColumn(columnName);
|
132
|
+
};
|
133
|
+
|
134
|
+
const toggleHiddenColumn = (columnName: string) => {
|
135
|
+
const updatedColumns = dataset.columns.map((c) => ({
|
136
|
+
...c,
|
137
|
+
hidden: c.name === columnName ? !c.hidden : c.hidden,
|
138
|
+
}));
|
139
|
+
|
140
|
+
setDataset({
|
141
|
+
...dataset,
|
142
|
+
columns: updatedColumns,
|
143
|
+
});
|
144
|
+
setNeedsRefresh(true);
|
145
|
+
};
|
146
|
+
|
147
|
+
const setTargetColumn = (columnName: string) => {
|
148
|
+
const name = String(columnName);
|
149
|
+
setConfig({ targetColumn: columnName });
|
150
|
+
const updatedColumns = dataset.columns.map((c) => ({
|
151
|
+
...c,
|
152
|
+
is_target: c.name === name,
|
153
|
+
}));
|
154
|
+
|
155
|
+
setDataset({
|
156
|
+
...dataset,
|
157
|
+
columns: updatedColumns,
|
158
|
+
});
|
159
|
+
setNeedsRefresh(true);
|
160
|
+
};
|
161
|
+
|
162
|
+
const setColumnType = (columnName: string, datatype: string) => {
|
163
|
+
const updatedColumns = dataset.columns.map((c) => ({
|
164
|
+
...c,
|
165
|
+
datatype: c.name === columnName ? datatype : c.datatype,
|
166
|
+
}));
|
167
|
+
|
168
|
+
setDataset({
|
169
|
+
...dataset,
|
170
|
+
columns: updatedColumns,
|
171
|
+
});
|
172
|
+
setNeedsRefresh(true);
|
173
|
+
};
|
174
|
+
|
175
|
+
const handlePreprocessingUpdate = (
|
176
|
+
columnName: string,
|
177
|
+
training: PreprocessingStep,
|
178
|
+
inference: PreprocessingStep | undefined,
|
179
|
+
useDistinctInference: boolean
|
180
|
+
) => {
|
181
|
+
const column = dataset.columns.find((c) => c.name === columnName);
|
182
|
+
if (!column) return;
|
183
|
+
|
184
|
+
const updatedColumns = dataset.columns.map((c) => {
|
185
|
+
if (c.name !== columnName) return c;
|
186
|
+
|
187
|
+
return {
|
188
|
+
...c,
|
189
|
+
preprocessing_steps: {
|
190
|
+
training,
|
191
|
+
...(useDistinctInference && inference ? { inference } : {}),
|
192
|
+
},
|
193
|
+
};
|
194
|
+
});
|
195
|
+
|
196
|
+
setDataset({
|
197
|
+
...dataset,
|
198
|
+
columns: updatedColumns,
|
199
|
+
});
|
200
|
+
setNeedsRefresh(true);
|
201
|
+
};
|
202
|
+
|
203
|
+
const handleFeaturesChange = (newFeatures: Feature[]) => {
|
204
|
+
const existingFeatures = dataset.features || [];
|
205
|
+
|
206
|
+
const removedFeatures = existingFeatures
|
207
|
+
.filter(
|
208
|
+
(existing) => !newFeatures.find((t) => t.name === existing.name)
|
209
|
+
)
|
210
|
+
.map((feature) => ({ ...feature, _destroy: true }));
|
211
|
+
|
212
|
+
const featuresWithDatasetId = [
|
213
|
+
...newFeatures,
|
214
|
+
...removedFeatures,
|
215
|
+
].map((feature, index) => ({
|
216
|
+
...feature,
|
217
|
+
dataset_id: dataset.id,
|
218
|
+
feature_position: index,
|
219
|
+
}));
|
220
|
+
|
221
|
+
setDataset((prevDataset) => ({
|
222
|
+
...prevDataset,
|
223
|
+
features: featuresWithDatasetId,
|
224
|
+
}));
|
225
|
+
setNeedsRefresh(true);
|
226
|
+
};
|
227
|
+
|
228
|
+
const handleApplyChanges = async () => {
|
229
|
+
setIsApplying(true);
|
230
|
+
try {
|
231
|
+
await onSave(dataset);
|
232
|
+
router.post(
|
233
|
+
`/easy_ml/datasets/${dataset.id}/refresh`,
|
234
|
+
{},
|
235
|
+
{
|
236
|
+
onSuccess: () => {
|
237
|
+
setIsApplying(false);
|
238
|
+
},
|
239
|
+
onError: () => {
|
240
|
+
console.error("Error refreshing dataset");
|
241
|
+
setIsApplying(false);
|
242
|
+
},
|
243
|
+
}
|
244
|
+
);
|
245
|
+
} catch (error) {
|
246
|
+
console.error("Error refreshing dataset:", error);
|
247
|
+
setIsApplying(false);
|
248
|
+
}
|
249
|
+
};
|
250
|
+
|
251
|
+
if (!isOpen) return null;
|
252
|
+
|
253
|
+
const selectedColumnData = selectedColumn
|
254
|
+
? dataset.columns.find((c) => c.name === selectedColumn)
|
255
|
+
: null;
|
256
|
+
|
257
|
+
return (
|
258
|
+
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
259
|
+
<div className="bg-white rounded-lg w-full max-w-6xl max-h-[90vh] overflow-hidden flex flex-col">
|
260
|
+
<div className="flex justify-between items-center p-4 border-b shrink-0">
|
261
|
+
<h2 className="text-lg font-semibold">Column Configuration</h2>
|
262
|
+
<div className="flex items-center gap-4">
|
263
|
+
<div className="min-w-[0px]">
|
264
|
+
<AutosaveIndicator saving={saving} saved={saved} error={error} />
|
265
|
+
</div>
|
266
|
+
<div className="relative">
|
267
|
+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
268
|
+
<input
|
269
|
+
type="text"
|
270
|
+
placeholder="Search columns..."
|
271
|
+
value={searchQuery}
|
272
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
273
|
+
className="pl-9 pr-4 py-2 w-64 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
|
274
|
+
/>
|
275
|
+
</div>
|
276
|
+
<button
|
277
|
+
onClick={onClose}
|
278
|
+
className="text-gray-500 hover:text-gray-700"
|
279
|
+
>
|
280
|
+
<X className="w-5 h-5" />
|
281
|
+
</button>
|
282
|
+
</div>
|
283
|
+
</div>
|
284
|
+
|
285
|
+
<div className="flex border-b shrink-0">
|
286
|
+
<button
|
287
|
+
onClick={() => setActiveTab("columns")}
|
288
|
+
className={`px-4 py-2 text-sm font-medium border-b-2 ${
|
289
|
+
activeTab === "columns"
|
290
|
+
? "border-blue-500 text-blue-600"
|
291
|
+
: "border-transparent text-gray-500 hover:text-gray-700"
|
292
|
+
}`}
|
293
|
+
>
|
294
|
+
<div className="flex items-center gap-2">
|
295
|
+
<Settings2 className="w-4 h-4" />
|
296
|
+
Column Configuration
|
297
|
+
</div>
|
298
|
+
</button>
|
299
|
+
<button
|
300
|
+
onClick={() => setActiveTab("features")}
|
301
|
+
className={`px-4 py-2 text-sm font-medium border-b-2 ${
|
302
|
+
activeTab === "features"
|
303
|
+
? "border-blue-500 text-blue-600"
|
304
|
+
: "border-transparent text-gray-500 hover:text-gray-700"
|
305
|
+
}`}
|
306
|
+
>
|
307
|
+
<div className="flex items-center gap-2">
|
308
|
+
<Wand2 className="w-4 h-4" />
|
309
|
+
Features
|
310
|
+
<span className="px-1.5 py-0.5 text-xs font-medium bg-blue-100 text-blue-600 rounded-full">
|
311
|
+
{constants.feature_options.length}
|
312
|
+
</span>
|
313
|
+
</div>
|
314
|
+
</button>
|
315
|
+
|
316
|
+
{needsRefresh && (
|
317
|
+
<div className="ml-auto px-4 flex items-center">
|
318
|
+
<button
|
319
|
+
onClick={handleApplyChanges}
|
320
|
+
disabled={isApplying}
|
321
|
+
className="group relative inline-flex items-center gap-2 px-6 py-2.5 bg-gradient-to-r from-blue-600 to-indigo-600 text-white text-sm font-medium rounded-md hover:from-blue-700 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 transition-all duration-200 shadow-md hover:shadow-lg"
|
322
|
+
>
|
323
|
+
<div className="absolute inset-0 bg-white/10 rounded-md opacity-0 group-hover:opacity-100 transition-opacity duration-200" />
|
324
|
+
{isApplying ? (
|
325
|
+
<>
|
326
|
+
<Loader2 className="w-4 h-4 animate-spin" />
|
327
|
+
Applying Preprocessing...
|
328
|
+
</>
|
329
|
+
) : (
|
330
|
+
<>
|
331
|
+
<Sparkles className="w-4 h-4" />
|
332
|
+
Apply Preprocessing
|
333
|
+
</>
|
334
|
+
)}
|
335
|
+
</button>
|
336
|
+
</div>
|
337
|
+
)}
|
338
|
+
</div>
|
339
|
+
|
340
|
+
{activeTab === "columns" ? (
|
341
|
+
<React.Fragment>
|
342
|
+
<div className="grid grid-cols-7 flex-1 min-h-0">
|
343
|
+
<div className="col-span-3 border-r overflow-hidden flex flex-col">
|
344
|
+
<div className="p-4 border-b shrink-0">
|
345
|
+
<label className="block text-sm font-medium text-gray-700">
|
346
|
+
Target Column
|
347
|
+
</label>
|
348
|
+
<SearchableSelect
|
349
|
+
options={dataset.columns.map((column) => ({
|
350
|
+
value: column.name,
|
351
|
+
label: column.name,
|
352
|
+
}))}
|
353
|
+
value={config.targetColumn || ""}
|
354
|
+
onChange={(value) =>
|
355
|
+
value && setTargetColumn(String(value))
|
356
|
+
}
|
357
|
+
/>
|
358
|
+
</div>
|
359
|
+
<div className="shrink-0">
|
360
|
+
<ColumnFilters
|
361
|
+
types={columnTypes}
|
362
|
+
activeFilters={activeFilters}
|
363
|
+
onFilterChange={setActiveFilters}
|
364
|
+
columnStats={columnStats}
|
365
|
+
columns={dataset.columns}
|
366
|
+
colHasPreprocessingSteps={colHasPreprocessingSteps}
|
367
|
+
/>
|
368
|
+
</div>
|
369
|
+
|
370
|
+
<div className="flex-1 overflow-y-auto p-4 min-h-0">
|
371
|
+
<ColumnList
|
372
|
+
columns={filteredColumns}
|
373
|
+
selectedColumn={selectedColumn}
|
374
|
+
onColumnSelect={handleColumnSelect}
|
375
|
+
onToggleHidden={toggleHiddenColumn}
|
376
|
+
/>
|
377
|
+
</div>
|
378
|
+
</div>
|
379
|
+
|
380
|
+
<div className="col-span-4 overflow-y-auto p-4">
|
381
|
+
{selectedColumnData ? (
|
382
|
+
<PreprocessingConfig
|
383
|
+
column={selectedColumnData}
|
384
|
+
dataset={dataset}
|
385
|
+
setColumnType={setColumnType}
|
386
|
+
setDataset={setDataset}
|
387
|
+
constants={constants}
|
388
|
+
onUpdate={(training, inference, useDistinctInference) =>
|
389
|
+
handlePreprocessingUpdate(
|
390
|
+
selectedColumnData.name,
|
391
|
+
training,
|
392
|
+
inference,
|
393
|
+
useDistinctInference
|
394
|
+
)
|
395
|
+
}
|
396
|
+
/>
|
397
|
+
) : (
|
398
|
+
<div className="h-full flex items-center justify-center text-gray-500">
|
399
|
+
Select a column to configure preprocessing
|
400
|
+
</div>
|
401
|
+
)}
|
402
|
+
</div>
|
403
|
+
</div>
|
404
|
+
<div className="border-t p-4 flex justify-between items-center shrink-0">
|
405
|
+
<div className="text-sm text-gray-600">
|
406
|
+
{dataset.columns.filter((c) => !c.hidden).length} columns
|
407
|
+
selected for training
|
408
|
+
</div>
|
409
|
+
<div className="flex gap-3">
|
410
|
+
<button
|
411
|
+
onClick={onClose}
|
412
|
+
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
|
413
|
+
>
|
414
|
+
Close
|
415
|
+
</button>
|
416
|
+
</div>
|
417
|
+
</div>
|
418
|
+
</React.Fragment>
|
419
|
+
) : (
|
420
|
+
<div className="p-6 h-[calc(90vh-8rem)] overflow-y-auto">
|
421
|
+
<FeaturePicker
|
422
|
+
options={constants.feature_options}
|
423
|
+
initialFeatures={dataset.features}
|
424
|
+
onFeaturesChange={handleFeaturesChange}
|
425
|
+
/>
|
426
|
+
</div>
|
427
|
+
)}
|
428
|
+
</div>
|
429
|
+
</div>
|
430
|
+
);
|
431
|
+
}
|