easy_ml 0.1.4 → 0.2.0.pre.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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,384 @@
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
2
|
+
// import { useNavigate } from 'react-router-dom';
|
3
|
+
import { TrainFront, Lock, AlertCircle } from 'lucide-react';
|
4
|
+
import { SearchableSelect } from './SearchableSelect';
|
5
|
+
import { ScheduleModal } from './ScheduleModal';
|
6
|
+
import { router } from '@inertiajs/react';
|
7
|
+
import { useInertiaForm } from 'use-inertia-form';
|
8
|
+
import { usePage } from '@inertiajs/react';
|
9
|
+
import type { Dataset } from '../types';
|
10
|
+
|
11
|
+
interface ModelFormProps {
|
12
|
+
initialData?: {
|
13
|
+
id: number;
|
14
|
+
name: string;
|
15
|
+
modelType: string;
|
16
|
+
datasetId: number;
|
17
|
+
task: string;
|
18
|
+
objective?: string;
|
19
|
+
metrics?: string[];
|
20
|
+
retraining_job?: {
|
21
|
+
frequency: string;
|
22
|
+
at: {
|
23
|
+
hour: number;
|
24
|
+
day_of_week?: string;
|
25
|
+
day_of_month?: number;
|
26
|
+
};
|
27
|
+
batch_mode?: string;
|
28
|
+
batch_size?: number;
|
29
|
+
batch_overlap?: number;
|
30
|
+
batch_key?: string;
|
31
|
+
tuning_frequency?: string;
|
32
|
+
active: boolean;
|
33
|
+
metric?: string;
|
34
|
+
threshold?: number;
|
35
|
+
tuner_config?: {
|
36
|
+
n_trials: number;
|
37
|
+
objective: string;
|
38
|
+
config: Record<string, any>;
|
39
|
+
};
|
40
|
+
tuning_enabled?: boolean;
|
41
|
+
};
|
42
|
+
};
|
43
|
+
datasets: Array<Dataset>;
|
44
|
+
constants: {
|
45
|
+
tasks: { value: string; label: string }[];
|
46
|
+
objectives: Record<string, { value: string; label: string; description?: string }[]>;
|
47
|
+
metrics: Record<string, { value: string; label: string; direction: string }[]>;
|
48
|
+
timezone: string;
|
49
|
+
retraining_job_constants: any;
|
50
|
+
tuner_job_constants: any;
|
51
|
+
};
|
52
|
+
isEditing?: boolean;
|
53
|
+
errors?: any;
|
54
|
+
}
|
55
|
+
|
56
|
+
const ErrorDisplay = ({ error }: { error?: string }) => (
|
57
|
+
error ? (
|
58
|
+
<div className="mt-1 flex items-center gap-1 text-sm text-red-600">
|
59
|
+
<AlertCircle className="w-4 h-4" />
|
60
|
+
{error}
|
61
|
+
</div>
|
62
|
+
) : null
|
63
|
+
);
|
64
|
+
|
65
|
+
export function ModelForm({ initialData, datasets, constants, isEditing, errors: initialErrors }: ModelFormProps) {
|
66
|
+
const { rootPath } = usePage().props;
|
67
|
+
const [showScheduleModal, setShowScheduleModal] = useState(false);
|
68
|
+
const [isDataSet, setIsDataSet] = useState(false);
|
69
|
+
|
70
|
+
const form = useInertiaForm({
|
71
|
+
model: {
|
72
|
+
id: initialData?.id,
|
73
|
+
name: initialData?.name || '',
|
74
|
+
model_type: initialData?.model_type || 'xgboost',
|
75
|
+
dataset_id: initialData?.dataset_id || '',
|
76
|
+
task: initialData?.task || 'classification',
|
77
|
+
objective: initialData?.objective || 'binary:logistic',
|
78
|
+
metrics: initialData?.metrics || ['accuracy'],
|
79
|
+
retraining_job_attributes: initialData?.retraining_job ? {
|
80
|
+
id: initialData.retraining_job.id,
|
81
|
+
frequency: initialData.retraining_job.frequency,
|
82
|
+
tuning_frequency: initialData.retraining_job.tuning_frequency || 'month',
|
83
|
+
batch_mode: initialData.retraining_job.batch_mode,
|
84
|
+
batch_size: initialData.retraining_job.batch_size,
|
85
|
+
batch_overlap: initialData.retraining_job.batch_overlap,
|
86
|
+
batch_key: initialData.retraining_job.batch_key,
|
87
|
+
at: {
|
88
|
+
hour: initialData.retraining_job.at?.hour ?? 2,
|
89
|
+
day_of_week: initialData.retraining_job.at?.day_of_week ?? 1,
|
90
|
+
day_of_month: initialData.retraining_job.at?.day_of_month ?? 1
|
91
|
+
},
|
92
|
+
active: initialData.retraining_job.active,
|
93
|
+
metric: initialData.retraining_job.metric,
|
94
|
+
threshold: initialData.retraining_job.threshold,
|
95
|
+
tuner_config: initialData.retraining_job.tuner_config,
|
96
|
+
tuning_enabled: initialData.retraining_job.tuning_enabled || false,
|
97
|
+
} : undefined
|
98
|
+
}
|
99
|
+
});
|
100
|
+
|
101
|
+
const { data, setData, post, patch, processing, errors: formErrors } = form;
|
102
|
+
const errors = { ...initialErrors, ...formErrors };
|
103
|
+
|
104
|
+
const objectives: { value: string; label: string; description?: string }[] =
|
105
|
+
constants.objectives[data.model.model_type]?.[data.model.task] || [];
|
106
|
+
|
107
|
+
useEffect(() => {
|
108
|
+
// Only set default metrics if none were provided from the backend
|
109
|
+
if (!initialData?.metrics) {
|
110
|
+
const availableMetrics = constants.metrics[data.model.task]?.map(metric => metric.value) || [];
|
111
|
+
setData({
|
112
|
+
...data,
|
113
|
+
model: {
|
114
|
+
...data.model,
|
115
|
+
objective: data.model.task === 'classification' ? 'binary:logistic' : 'reg:squarederror',
|
116
|
+
metrics: availableMetrics
|
117
|
+
}
|
118
|
+
});
|
119
|
+
} else {
|
120
|
+
setData({
|
121
|
+
...data,
|
122
|
+
model: {
|
123
|
+
...data.model,
|
124
|
+
objective: data.model.task === 'classification' ? 'binary:logistic' : 'reg:squarederror'
|
125
|
+
}
|
126
|
+
});
|
127
|
+
}
|
128
|
+
}, [data.model.task]);
|
129
|
+
|
130
|
+
useEffect(() => {
|
131
|
+
if (isDataSet) {
|
132
|
+
save();
|
133
|
+
setIsDataSet(false); // Reset the flag
|
134
|
+
}
|
135
|
+
}, [isDataSet]);
|
136
|
+
|
137
|
+
const handleScheduleSave = (scheduleData: any) => {
|
138
|
+
setData({
|
139
|
+
...data,
|
140
|
+
model: {
|
141
|
+
...data.model,
|
142
|
+
retraining_job_attributes: scheduleData.retraining_job_attributes
|
143
|
+
}
|
144
|
+
});
|
145
|
+
setIsDataSet(true);
|
146
|
+
};
|
147
|
+
|
148
|
+
const save = () => {
|
149
|
+
if (data.model.retraining_job_attributes) {
|
150
|
+
const at: any = { hour: data.model.retraining_job_attributes.at.hour };
|
151
|
+
|
152
|
+
// Only include relevant date attributes based on frequency
|
153
|
+
switch (data.model.retraining_job_attributes.frequency) {
|
154
|
+
case 'day':
|
155
|
+
// For daily frequency, only include hour
|
156
|
+
break;
|
157
|
+
case 'week':
|
158
|
+
// For weekly frequency, include hour and day_of_week
|
159
|
+
at.day_of_week = data.model.retraining_job_attributes.at.day_of_week;
|
160
|
+
break;
|
161
|
+
case 'month':
|
162
|
+
// For monthly frequency, include hour and day_of_month
|
163
|
+
at.day_of_month = data.model.retraining_job_attributes.at.day_of_month;
|
164
|
+
break;
|
165
|
+
}
|
166
|
+
|
167
|
+
// Update the form data with the cleaned at object
|
168
|
+
setData('model.retraining_job_attributes.at', at);
|
169
|
+
}
|
170
|
+
|
171
|
+
if (data.model.id) {
|
172
|
+
patch(`${rootPath}/models/${data.model.id}`, {
|
173
|
+
onSuccess: () => {
|
174
|
+
router.visit(`${rootPath}/models`);
|
175
|
+
},
|
176
|
+
});
|
177
|
+
} else {
|
178
|
+
post(`${rootPath}/models`, {
|
179
|
+
onSuccess: () => {
|
180
|
+
router.visit(`${rootPath}/models`);
|
181
|
+
},
|
182
|
+
});
|
183
|
+
}
|
184
|
+
}
|
185
|
+
|
186
|
+
const handleSubmit = (e: React.FormEvent) => {
|
187
|
+
e.preventDefault();
|
188
|
+
save();
|
189
|
+
};
|
190
|
+
|
191
|
+
console.log(data.model)
|
192
|
+
const selectedDataset = datasets.find(d => d.id === data.model.dataset_id);
|
193
|
+
|
194
|
+
const filteredTunerJobConstants = constants.tuner_job_constants[data.model.model_type] || {};
|
195
|
+
|
196
|
+
return (
|
197
|
+
<form onSubmit={handleSubmit} className="space-y-8">
|
198
|
+
<div className="flex justify-between items-center border-b pb-4">
|
199
|
+
<h3 className="text-lg font-medium text-gray-900">Model Configuration</h3>
|
200
|
+
<button
|
201
|
+
type="button"
|
202
|
+
onClick={() => setShowScheduleModal(true)}
|
203
|
+
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
204
|
+
>
|
205
|
+
<TrainFront className="w-4 h-4" />
|
206
|
+
Configure Training
|
207
|
+
</button>
|
208
|
+
</div>
|
209
|
+
|
210
|
+
<div className="space-y-6">
|
211
|
+
<div className="grid grid-cols-2 gap-6">
|
212
|
+
<div>
|
213
|
+
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
|
214
|
+
Model Name
|
215
|
+
</label>
|
216
|
+
<input
|
217
|
+
type="text"
|
218
|
+
id="name"
|
219
|
+
value={data.model.name}
|
220
|
+
onChange={(e) => setData('model.name', e.target.value)}
|
221
|
+
className="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 py-2 px-4 shadow-sm border-gray-300 border"
|
222
|
+
/>
|
223
|
+
<ErrorDisplay error={errors.name} />
|
224
|
+
</div>
|
225
|
+
|
226
|
+
<div>
|
227
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
228
|
+
Model Type
|
229
|
+
</label>
|
230
|
+
<SearchableSelect
|
231
|
+
options={[{ value: 'xgboost', label: 'XGBoost', description: 'Gradient boosting framework' }]}
|
232
|
+
value={data.model.model_type}
|
233
|
+
onChange={(value) => setData('model.model_type', value as string)}
|
234
|
+
placeholder="Select model type"
|
235
|
+
/>
|
236
|
+
<ErrorDisplay error={errors.model_type} />
|
237
|
+
</div>
|
238
|
+
|
239
|
+
<div>
|
240
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
241
|
+
Dataset
|
242
|
+
</label>
|
243
|
+
{isEditing ? (
|
244
|
+
<div className="flex items-center gap-2 p-2 bg-gray-50 rounded-md border border-gray-200">
|
245
|
+
<Lock className="w-4 h-4 text-gray-400" />
|
246
|
+
<span className="text-gray-700">{selectedDataset?.name}</span>
|
247
|
+
</div>
|
248
|
+
) : (
|
249
|
+
<SearchableSelect
|
250
|
+
options={datasets.map(dataset => ({
|
251
|
+
value: dataset.id,
|
252
|
+
label: dataset.name,
|
253
|
+
description: `${dataset.num_rows.toLocaleString()} rows`
|
254
|
+
}))}
|
255
|
+
value={data.model.dataset_id}
|
256
|
+
onChange={(value) => setData('model.dataset_id', value)}
|
257
|
+
placeholder="Select dataset"
|
258
|
+
/>
|
259
|
+
)}
|
260
|
+
<ErrorDisplay error={errors.dataset_id} />
|
261
|
+
</div>
|
262
|
+
|
263
|
+
<div>
|
264
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
265
|
+
Task
|
266
|
+
</label>
|
267
|
+
<SearchableSelect
|
268
|
+
options={constants.tasks}
|
269
|
+
value={data.model.task}
|
270
|
+
onChange={(value) => setData('model.task', value as string)}
|
271
|
+
placeholder="Select task"
|
272
|
+
/>
|
273
|
+
<ErrorDisplay error={errors.task} />
|
274
|
+
</div>
|
275
|
+
|
276
|
+
<div>
|
277
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
278
|
+
Objective
|
279
|
+
</label>
|
280
|
+
<SearchableSelect
|
281
|
+
options={objectives || []}
|
282
|
+
value={data.model.objective}
|
283
|
+
onChange={(value) => setData('model.objective', value as string)}
|
284
|
+
placeholder="Select objective"
|
285
|
+
/>
|
286
|
+
<ErrorDisplay error={errors.objective} />
|
287
|
+
</div>
|
288
|
+
</div>
|
289
|
+
|
290
|
+
<div>
|
291
|
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
292
|
+
Metrics
|
293
|
+
</label>
|
294
|
+
<div className="grid grid-cols-2 gap-4">
|
295
|
+
{constants.metrics[data.model.task]?.map(metric => (
|
296
|
+
<label
|
297
|
+
key={metric.value}
|
298
|
+
className="relative flex items-center px-4 py-3 bg-white border rounded-lg hover:bg-gray-50 cursor-pointer"
|
299
|
+
>
|
300
|
+
<input
|
301
|
+
type="checkbox"
|
302
|
+
checked={data.model.metrics.includes(metric.value)}
|
303
|
+
onChange={(e) => {
|
304
|
+
const metrics = e.target.checked
|
305
|
+
? [...data.model.metrics, metric.value]
|
306
|
+
: data.model.metrics.filter(m => m !== metric.value);
|
307
|
+
setData('model.metrics', metrics);
|
308
|
+
}}
|
309
|
+
className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
310
|
+
/>
|
311
|
+
<div className="ml-3">
|
312
|
+
<span className="block text-sm font-medium text-gray-900">
|
313
|
+
{metric.label}
|
314
|
+
</span>
|
315
|
+
<span className="block text-xs text-gray-500">
|
316
|
+
{metric.direction === 'maximize' ? 'Higher is better' : 'Lower is better'}
|
317
|
+
</span>
|
318
|
+
</div>
|
319
|
+
</label>
|
320
|
+
))}
|
321
|
+
</div>
|
322
|
+
</div>
|
323
|
+
</div>
|
324
|
+
|
325
|
+
{data.model.retraining_job_attributes && data.model.retraining_job_attributes.batch_mode && (
|
326
|
+
<>
|
327
|
+
<div className="mt-4">
|
328
|
+
<label className="block text-sm font-medium text-gray-700">
|
329
|
+
Batch Key
|
330
|
+
</label>
|
331
|
+
<SearchableSelect
|
332
|
+
value={data.model.retraining_job_attributes.batch_key || ''}
|
333
|
+
onChange={(value) => setData('model', {
|
334
|
+
...data.model,
|
335
|
+
retraining_job_attributes: {
|
336
|
+
...data.model.retraining_job_attributes,
|
337
|
+
batch_key: value
|
338
|
+
}
|
339
|
+
})}
|
340
|
+
options={selectedDataset?.columns?.map(column => ({
|
341
|
+
value: column.name,
|
342
|
+
label: column.name
|
343
|
+
})) || []}
|
344
|
+
placeholder="Select a column for batch key"
|
345
|
+
/>
|
346
|
+
<ErrorDisplay error={errors['model.retraining_job_attributes.batch_key']} />
|
347
|
+
</div>
|
348
|
+
</>
|
349
|
+
)}
|
350
|
+
|
351
|
+
<div className="flex justify-end gap-3 pt-4 border-t">
|
352
|
+
<button
|
353
|
+
type="button"
|
354
|
+
onClick={() => router.visit(`${rootPath}/models`)}
|
355
|
+
className="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900"
|
356
|
+
>
|
357
|
+
Cancel
|
358
|
+
</button>
|
359
|
+
<button
|
360
|
+
type="submit"
|
361
|
+
className="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
362
|
+
>
|
363
|
+
{isEditing ? 'Save Changes' : 'Create Model'}
|
364
|
+
</button>
|
365
|
+
</div>
|
366
|
+
|
367
|
+
<ScheduleModal
|
368
|
+
isOpen={showScheduleModal}
|
369
|
+
onClose={() => setShowScheduleModal(false)}
|
370
|
+
onSave={handleScheduleSave}
|
371
|
+
initialData={{
|
372
|
+
task: data.model.task,
|
373
|
+
metrics: data.model.metrics,
|
374
|
+
modelType: data.model.model_type,
|
375
|
+
dataset: selectedDataset,
|
376
|
+
retraining_job: data.model.retraining_job_attributes
|
377
|
+
}}
|
378
|
+
tunerJobConstants={filteredTunerJobConstants}
|
379
|
+
timezone={constants.timezone}
|
380
|
+
retrainingJobConstants={constants.retraining_job_constants}
|
381
|
+
/>
|
382
|
+
</form>
|
383
|
+
);
|
384
|
+
}
|
@@ -0,0 +1,300 @@
|
|
1
|
+
import React, { useState } from 'react';
|
2
|
+
import { AlertContainer } from './AlertProvider';
|
3
|
+
import { Link, router, usePage } from "@inertiajs/react";
|
4
|
+
import { Brain, Database, HardDrive, ChevronRight, ChevronDown, Menu, Settings2 } from 'lucide-react';
|
5
|
+
import { ScrollArea } from './ui/scroll-area';
|
6
|
+
import { Separator } from './ui/separator';
|
7
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible';
|
8
|
+
import { cn } from '@/lib/utils';
|
9
|
+
import { mockDatasets, mockModels } from '../mockData';
|
10
|
+
|
11
|
+
export function NavLink({
|
12
|
+
href,
|
13
|
+
className = (isActive: boolean) => "", // Add type annotation for isActive
|
14
|
+
activeClassName = 'active',
|
15
|
+
children,
|
16
|
+
...props
|
17
|
+
}: {
|
18
|
+
href: string;
|
19
|
+
className?: (isActive: boolean) => string;
|
20
|
+
activeClassName?: string;
|
21
|
+
children: React.ReactNode;
|
22
|
+
[key: string]: any;
|
23
|
+
}) {
|
24
|
+
// Get the current URL path from Inertia's page object
|
25
|
+
const { rootPath, url } = usePage().props;
|
26
|
+
|
27
|
+
// Check if the current URL matches the `href` to apply the active class
|
28
|
+
const isActive = url === href;
|
29
|
+
let classes = className(isActive);
|
30
|
+
|
31
|
+
return (
|
32
|
+
<Link
|
33
|
+
href={`${rootPath}${href}`}
|
34
|
+
className={cn(classes, isActive && activeClassName)}
|
35
|
+
{...props}
|
36
|
+
>
|
37
|
+
{children}
|
38
|
+
</Link>
|
39
|
+
);
|
40
|
+
}
|
41
|
+
|
42
|
+
interface NavItem {
|
43
|
+
title: string;
|
44
|
+
icon: React.ElementType;
|
45
|
+
href: string;
|
46
|
+
children?: NavItem[];
|
47
|
+
}
|
48
|
+
|
49
|
+
const navItems: NavItem[] = [
|
50
|
+
{
|
51
|
+
title: 'Models',
|
52
|
+
icon: Brain,
|
53
|
+
href: '/',
|
54
|
+
children: [
|
55
|
+
{ title: 'All Models', icon: Brain, href: '/models' },
|
56
|
+
{ title: 'New Model', icon: Brain, href: '/models/new' }
|
57
|
+
]
|
58
|
+
},
|
59
|
+
{
|
60
|
+
title: 'Datasources',
|
61
|
+
icon: HardDrive,
|
62
|
+
href: '/datasources',
|
63
|
+
children: [
|
64
|
+
{ title: 'All Datasources', icon: HardDrive, href: '/datasources' },
|
65
|
+
{ title: 'New Datasource', icon: HardDrive, href: '/datasources/new' }
|
66
|
+
]
|
67
|
+
},
|
68
|
+
{
|
69
|
+
title: 'Datasets',
|
70
|
+
icon: Database,
|
71
|
+
href: '/datasets',
|
72
|
+
children: [
|
73
|
+
{ title: 'All Datasets', icon: Database, href: '/datasets' },
|
74
|
+
{ title: 'New Dataset', icon: Database, href: '/datasets/new' }
|
75
|
+
]
|
76
|
+
}
|
77
|
+
];
|
78
|
+
|
79
|
+
function getBreadcrumbs(pathname: string): { title: string; href: string }[] {
|
80
|
+
const { rootPath } = usePage().props; // Inject rootPath
|
81
|
+
const paths = pathname.split('/').filter(Boolean);
|
82
|
+
const breadcrumbs = [];
|
83
|
+
let currentPath = rootPath; // Start with rootPath
|
84
|
+
|
85
|
+
// Determine the root breadcrumb based on the first path segment
|
86
|
+
if (paths.length === 0) {
|
87
|
+
return [];
|
88
|
+
}
|
89
|
+
|
90
|
+
let firstSegment;
|
91
|
+
let rootCrumb;
|
92
|
+
if (['datasources', 'datasets', 'models', 'settings'].includes(paths[0])) {
|
93
|
+
firstSegment = paths[0];
|
94
|
+
rootCrumb = 0;
|
95
|
+
} else {
|
96
|
+
firstSegment = paths[1];
|
97
|
+
rootCrumb = 1;
|
98
|
+
}
|
99
|
+
switch (firstSegment) {
|
100
|
+
case 'models':
|
101
|
+
breadcrumbs.push({ title: 'Models', href: `${rootPath}/models` });
|
102
|
+
break;
|
103
|
+
case 'datasources':
|
104
|
+
breadcrumbs.push({ title: 'Datasources', href: `${rootPath}/datasources` });
|
105
|
+
break;
|
106
|
+
case 'datasets':
|
107
|
+
breadcrumbs.push({ title: 'Datasets', href: `${rootPath}/datasets` });
|
108
|
+
break;
|
109
|
+
case 'settings':
|
110
|
+
breadcrumbs.push({ title: 'Settings', href: `${rootPath}/settings` });
|
111
|
+
break;
|
112
|
+
default:
|
113
|
+
breadcrumbs.push({ title: 'Models', href: `${rootPath}/models` });
|
114
|
+
}
|
115
|
+
|
116
|
+
// Add remaining breadcrumbs only if there are more segments
|
117
|
+
for (let i = rootCrumb + 1; i < paths.length; i++) {
|
118
|
+
const path = paths[i];
|
119
|
+
currentPath += `/${paths[i]}`;
|
120
|
+
|
121
|
+
// Handle special cases for IDs
|
122
|
+
if (paths[i-1] === 'datasets' && path !== 'new') {
|
123
|
+
breadcrumbs.push({
|
124
|
+
title: 'Details',
|
125
|
+
href: currentPath
|
126
|
+
});
|
127
|
+
} else if (paths[i-1] === 'models' && path !== 'new') {
|
128
|
+
breadcrumbs.push({
|
129
|
+
title: 'Details',
|
130
|
+
href: currentPath
|
131
|
+
});
|
132
|
+
} else {
|
133
|
+
const title = path === 'new'
|
134
|
+
? 'New'
|
135
|
+
: path === 'edit'
|
136
|
+
? 'Edit'
|
137
|
+
: path.charAt(0).toUpperCase() + path.slice(1);
|
138
|
+
breadcrumbs.push({ title, href: currentPath });
|
139
|
+
}
|
140
|
+
}
|
141
|
+
|
142
|
+
return breadcrumbs;
|
143
|
+
}
|
144
|
+
|
145
|
+
interface NavigationProps {
|
146
|
+
children: React.ReactNode;
|
147
|
+
}
|
148
|
+
|
149
|
+
export function Navigation({ children }: NavigationProps) {
|
150
|
+
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
151
|
+
const [openSections, setOpenSections] = useState<string[]>(['Models']);
|
152
|
+
const breadcrumbs = getBreadcrumbs(location.pathname);
|
153
|
+
|
154
|
+
const toggleSection = (title: string) => {
|
155
|
+
setOpenSections(prev =>
|
156
|
+
prev.includes(title)
|
157
|
+
? prev.filter(t => t !== title)
|
158
|
+
: [...prev, title]
|
159
|
+
);
|
160
|
+
};
|
161
|
+
|
162
|
+
return (
|
163
|
+
<div className="min-h-screen bg-gray-50">
|
164
|
+
{/* Sidebar */}
|
165
|
+
<div
|
166
|
+
className={cn(
|
167
|
+
"fixed left-0 top-0 z-40 h-screen bg-white border-r transition-all duration-300",
|
168
|
+
isSidebarOpen ? "w-64" : "w-16"
|
169
|
+
)}
|
170
|
+
>
|
171
|
+
<div className="flex h-16 items-center border-b px-4">
|
172
|
+
{isSidebarOpen ? (
|
173
|
+
<>
|
174
|
+
<Brain className="w-8 h-8 text-blue-600" />
|
175
|
+
<h1 className="text-xl font-bold text-gray-900 ml-2">EasyML</h1>
|
176
|
+
</>
|
177
|
+
) : (
|
178
|
+
<Brain className="w-8 h-8 text-blue-600" />
|
179
|
+
)}
|
180
|
+
<button
|
181
|
+
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
182
|
+
className="ml-auto p-2 hover:bg-gray-100 rounded-md"
|
183
|
+
>
|
184
|
+
<Menu className="w-4 h-4" />
|
185
|
+
</button>
|
186
|
+
</div>
|
187
|
+
|
188
|
+
<ScrollArea className="h-[calc(100vh-4rem)] px-3">
|
189
|
+
<div className="space-y-2 py-4">
|
190
|
+
{navItems.map((section) => (
|
191
|
+
<Collapsible
|
192
|
+
key={section.title}
|
193
|
+
open={openSections.includes(section.title)}
|
194
|
+
onOpenChange={() => toggleSection(section.title)}
|
195
|
+
>
|
196
|
+
<CollapsibleTrigger className="flex items-center w-full p-2 hover:bg-gray-100 rounded-md">
|
197
|
+
<section.icon className="w-4 h-4" />
|
198
|
+
{isSidebarOpen && (
|
199
|
+
<>
|
200
|
+
<span className="ml-2 text-sm font-medium flex-1 text-left">
|
201
|
+
{section.title}
|
202
|
+
</span>
|
203
|
+
{openSections.includes(section.title) ? (
|
204
|
+
<ChevronDown className="w-4 h-4" />
|
205
|
+
) : (
|
206
|
+
<ChevronRight className="w-4 h-4" />
|
207
|
+
)}
|
208
|
+
</>
|
209
|
+
)}
|
210
|
+
</CollapsibleTrigger>
|
211
|
+
<CollapsibleContent>
|
212
|
+
{isSidebarOpen && section.children?.map((item) => (
|
213
|
+
<NavLink
|
214
|
+
key={item.href}
|
215
|
+
href={item.href}
|
216
|
+
className={({ isActive }) =>
|
217
|
+
cn(
|
218
|
+
"flex items-center pl-8 pr-2 py-2 text-sm rounded-md",
|
219
|
+
isActive
|
220
|
+
? "bg-blue-50 text-blue-600"
|
221
|
+
: "text-gray-600 hover:bg-gray-50"
|
222
|
+
)
|
223
|
+
}
|
224
|
+
>
|
225
|
+
<item.icon className="w-4 h-4" />
|
226
|
+
<span className="ml-2">{item.title}</span>
|
227
|
+
</NavLink>
|
228
|
+
))}
|
229
|
+
</CollapsibleContent>
|
230
|
+
</Collapsible>
|
231
|
+
))}
|
232
|
+
|
233
|
+
<Separator className="my-4" />
|
234
|
+
|
235
|
+
{/* Settings Link */}
|
236
|
+
<NavLink
|
237
|
+
href="/settings"
|
238
|
+
className={({ isActive }) =>
|
239
|
+
cn(
|
240
|
+
"flex items-center w-full p-2 rounded-md",
|
241
|
+
isActive
|
242
|
+
? "bg-blue-50 text-blue-600"
|
243
|
+
: "text-gray-600 hover:bg-gray-50"
|
244
|
+
)
|
245
|
+
}
|
246
|
+
>
|
247
|
+
<Settings2 className="w-4 h-4" />
|
248
|
+
{isSidebarOpen && (
|
249
|
+
<span className="ml-2 text-sm font-medium">Settings</span>
|
250
|
+
)}
|
251
|
+
</NavLink>
|
252
|
+
</div>
|
253
|
+
</ScrollArea>
|
254
|
+
</div>
|
255
|
+
|
256
|
+
{/* Main content */}
|
257
|
+
<div
|
258
|
+
className={cn(
|
259
|
+
"transition-all duration-300",
|
260
|
+
isSidebarOpen ? "ml-64" : "ml-16"
|
261
|
+
)}
|
262
|
+
>
|
263
|
+
<AlertContainer />
|
264
|
+
|
265
|
+
{/* Breadcrumbs */}
|
266
|
+
<div className="h-16 border-b bg-white flex items-center px-4">
|
267
|
+
<nav className="flex" aria-label="Breadcrumb">
|
268
|
+
<ol className="flex items-center space-x-2">
|
269
|
+
{breadcrumbs.map((crumb, index) => (
|
270
|
+
<React.Fragment key={crumb.href}>
|
271
|
+
{index > 0 && (
|
272
|
+
<ChevronRight className="w-4 h-4 text-gray-400" />
|
273
|
+
)}
|
274
|
+
<li>
|
275
|
+
<Link
|
276
|
+
href={crumb.href}
|
277
|
+
className={cn(
|
278
|
+
"text-sm",
|
279
|
+
index === breadcrumbs.length - 1
|
280
|
+
? "text-blue-600 font-medium"
|
281
|
+
: "text-gray-500 hover:text-gray-700"
|
282
|
+
)}
|
283
|
+
>
|
284
|
+
{crumb.title}
|
285
|
+
</Link>
|
286
|
+
</li>
|
287
|
+
</React.Fragment>
|
288
|
+
))}
|
289
|
+
</ol>
|
290
|
+
</nav>
|
291
|
+
</div>
|
292
|
+
|
293
|
+
{/* Page content */}
|
294
|
+
<main className="min-h-[calc(100vh-4rem)]">
|
295
|
+
{children}
|
296
|
+
</main>
|
297
|
+
</div>
|
298
|
+
</div>
|
299
|
+
);
|
300
|
+
}
|