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.
Files changed (239) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -26
  3. data/Rakefile +45 -0
  4. data/app/controllers/easy_ml/application_controller.rb +67 -0
  5. data/app/controllers/easy_ml/columns_controller.rb +38 -0
  6. data/app/controllers/easy_ml/datasets_controller.rb +156 -0
  7. data/app/controllers/easy_ml/datasources_controller.rb +88 -0
  8. data/app/controllers/easy_ml/deploys_controller.rb +20 -0
  9. data/app/controllers/easy_ml/models_controller.rb +151 -0
  10. data/app/controllers/easy_ml/retraining_runs_controller.rb +19 -0
  11. data/app/controllers/easy_ml/settings_controller.rb +59 -0
  12. data/app/frontend/components/AlertProvider.tsx +108 -0
  13. data/app/frontend/components/DatasetPreview.tsx +161 -0
  14. data/app/frontend/components/EmptyState.tsx +28 -0
  15. data/app/frontend/components/ModelCard.tsx +255 -0
  16. data/app/frontend/components/ModelDetails.tsx +334 -0
  17. data/app/frontend/components/ModelForm.tsx +384 -0
  18. data/app/frontend/components/Navigation.tsx +300 -0
  19. data/app/frontend/components/Pagination.tsx +72 -0
  20. data/app/frontend/components/Popover.tsx +55 -0
  21. data/app/frontend/components/PredictionStream.tsx +105 -0
  22. data/app/frontend/components/ScheduleModal.tsx +726 -0
  23. data/app/frontend/components/SearchInput.tsx +23 -0
  24. data/app/frontend/components/SearchableSelect.tsx +132 -0
  25. data/app/frontend/components/dataset/AutosaveIndicator.tsx +39 -0
  26. data/app/frontend/components/dataset/ColumnConfigModal.tsx +431 -0
  27. data/app/frontend/components/dataset/ColumnFilters.tsx +256 -0
  28. data/app/frontend/components/dataset/ColumnList.tsx +101 -0
  29. data/app/frontend/components/dataset/FeatureConfigPopover.tsx +57 -0
  30. data/app/frontend/components/dataset/FeaturePicker.tsx +205 -0
  31. data/app/frontend/components/dataset/PreprocessingConfig.tsx +704 -0
  32. data/app/frontend/components/dataset/SplitConfigurator.tsx +120 -0
  33. data/app/frontend/components/dataset/splitters/DateSplitter.tsx +58 -0
  34. data/app/frontend/components/dataset/splitters/KFoldSplitter.tsx +68 -0
  35. data/app/frontend/components/dataset/splitters/LeavePOutSplitter.tsx +29 -0
  36. data/app/frontend/components/dataset/splitters/PredefinedSplitter.tsx +146 -0
  37. data/app/frontend/components/dataset/splitters/RandomSplitter.tsx +85 -0
  38. data/app/frontend/components/dataset/splitters/StratifiedSplitter.tsx +79 -0
  39. data/app/frontend/components/dataset/splitters/constants.ts +77 -0
  40. data/app/frontend/components/dataset/splitters/types.ts +168 -0
  41. data/app/frontend/components/dataset/splitters/utils.ts +53 -0
  42. data/app/frontend/components/features/CodeEditor.tsx +46 -0
  43. data/app/frontend/components/features/DataPreview.tsx +150 -0
  44. data/app/frontend/components/features/FeatureCard.tsx +88 -0
  45. data/app/frontend/components/features/FeatureForm.tsx +235 -0
  46. data/app/frontend/components/features/FeatureGroupCard.tsx +54 -0
  47. data/app/frontend/components/settings/PluginSettings.tsx +81 -0
  48. data/app/frontend/components/ui/badge.tsx +44 -0
  49. data/app/frontend/components/ui/collapsible.tsx +9 -0
  50. data/app/frontend/components/ui/scroll-area.tsx +46 -0
  51. data/app/frontend/components/ui/separator.tsx +29 -0
  52. data/app/frontend/entrypoints/App.tsx +40 -0
  53. data/app/frontend/entrypoints/Application.tsx +24 -0
  54. data/app/frontend/hooks/useAutosave.ts +61 -0
  55. data/app/frontend/layouts/Layout.tsx +38 -0
  56. data/app/frontend/lib/utils.ts +6 -0
  57. data/app/frontend/mockData.ts +272 -0
  58. data/app/frontend/pages/DatasetDetailsPage.tsx +103 -0
  59. data/app/frontend/pages/DatasetsPage.tsx +261 -0
  60. data/app/frontend/pages/DatasourceFormPage.tsx +147 -0
  61. data/app/frontend/pages/DatasourcesPage.tsx +261 -0
  62. data/app/frontend/pages/EditModelPage.tsx +45 -0
  63. data/app/frontend/pages/EditTransformationPage.tsx +56 -0
  64. data/app/frontend/pages/ModelsPage.tsx +115 -0
  65. data/app/frontend/pages/NewDatasetPage.tsx +366 -0
  66. data/app/frontend/pages/NewModelPage.tsx +45 -0
  67. data/app/frontend/pages/NewTransformationPage.tsx +43 -0
  68. data/app/frontend/pages/SettingsPage.tsx +272 -0
  69. data/app/frontend/pages/ShowModelPage.tsx +30 -0
  70. data/app/frontend/pages/TransformationsPage.tsx +95 -0
  71. data/app/frontend/styles/application.css +100 -0
  72. data/app/frontend/types/dataset.ts +146 -0
  73. data/app/frontend/types/datasource.ts +33 -0
  74. data/app/frontend/types/preprocessing.ts +1 -0
  75. data/app/frontend/types.ts +113 -0
  76. data/app/helpers/easy_ml/application_helper.rb +10 -0
  77. data/app/jobs/easy_ml/application_job.rb +21 -0
  78. data/app/jobs/easy_ml/batch_job.rb +46 -0
  79. data/app/jobs/easy_ml/compute_feature_job.rb +19 -0
  80. data/app/jobs/easy_ml/deploy_job.rb +13 -0
  81. data/app/jobs/easy_ml/finalize_feature_job.rb +15 -0
  82. data/app/jobs/easy_ml/refresh_dataset_job.rb +32 -0
  83. data/app/jobs/easy_ml/schedule_retraining_job.rb +11 -0
  84. data/app/jobs/easy_ml/sync_datasource_job.rb +17 -0
  85. data/app/jobs/easy_ml/training_job.rb +62 -0
  86. data/app/models/easy_ml/adapters/base_adapter.rb +45 -0
  87. data/app/models/easy_ml/adapters/polars_adapter.rb +77 -0
  88. data/app/models/easy_ml/cleaner.rb +82 -0
  89. data/app/models/easy_ml/column.rb +124 -0
  90. data/app/models/easy_ml/column_history.rb +30 -0
  91. data/app/models/easy_ml/column_list.rb +122 -0
  92. data/app/models/easy_ml/concerns/configurable.rb +61 -0
  93. data/app/models/easy_ml/concerns/versionable.rb +19 -0
  94. data/app/models/easy_ml/dataset.rb +767 -0
  95. data/app/models/easy_ml/dataset_history.rb +56 -0
  96. data/app/models/easy_ml/datasource.rb +182 -0
  97. data/app/models/easy_ml/datasource_history.rb +24 -0
  98. data/app/models/easy_ml/datasources/base_datasource.rb +54 -0
  99. data/app/models/easy_ml/datasources/file_datasource.rb +58 -0
  100. data/app/models/easy_ml/datasources/polars_datasource.rb +89 -0
  101. data/app/models/easy_ml/datasources/s3_datasource.rb +97 -0
  102. data/app/models/easy_ml/deploy.rb +114 -0
  103. data/app/models/easy_ml/event.rb +79 -0
  104. data/app/models/easy_ml/feature.rb +437 -0
  105. data/app/models/easy_ml/feature_history.rb +38 -0
  106. data/app/models/easy_ml/model.rb +575 -41
  107. data/app/models/easy_ml/model_file.rb +133 -0
  108. data/app/models/easy_ml/model_file_history.rb +24 -0
  109. data/app/models/easy_ml/model_history.rb +51 -0
  110. data/app/models/easy_ml/models/base_model.rb +58 -0
  111. data/app/models/easy_ml/models/hyperparameters/base.rb +99 -0
  112. data/app/models/easy_ml/models/hyperparameters/xgboost/dart.rb +82 -0
  113. data/app/models/easy_ml/models/hyperparameters/xgboost/gblinear.rb +82 -0
  114. data/app/models/easy_ml/models/hyperparameters/xgboost/gbtree.rb +97 -0
  115. data/app/models/easy_ml/models/hyperparameters/xgboost.rb +71 -0
  116. data/app/models/easy_ml/models/xgboost/evals_callback.rb +138 -0
  117. data/app/models/easy_ml/models/xgboost/progress_callback.rb +39 -0
  118. data/app/models/easy_ml/models/xgboost.rb +544 -5
  119. data/app/models/easy_ml/prediction.rb +44 -0
  120. data/app/models/easy_ml/retraining_job.rb +278 -0
  121. data/app/models/easy_ml/retraining_run.rb +184 -0
  122. data/app/models/easy_ml/settings.rb +37 -0
  123. data/app/models/easy_ml/splitter.rb +90 -0
  124. data/app/models/easy_ml/splitters/base_splitter.rb +28 -0
  125. data/app/models/easy_ml/splitters/date_splitter.rb +91 -0
  126. data/app/models/easy_ml/splitters/predefined_splitter.rb +74 -0
  127. data/app/models/easy_ml/splitters/random_splitter.rb +82 -0
  128. data/app/models/easy_ml/tuner_job.rb +56 -0
  129. data/app/models/easy_ml/tuner_run.rb +31 -0
  130. data/app/models/splitter_history.rb +6 -0
  131. data/app/serializers/easy_ml/column_serializer.rb +27 -0
  132. data/app/serializers/easy_ml/dataset_serializer.rb +73 -0
  133. data/app/serializers/easy_ml/datasource_serializer.rb +64 -0
  134. data/app/serializers/easy_ml/feature_serializer.rb +27 -0
  135. data/app/serializers/easy_ml/model_serializer.rb +90 -0
  136. data/app/serializers/easy_ml/retraining_job_serializer.rb +22 -0
  137. data/app/serializers/easy_ml/retraining_run_serializer.rb +39 -0
  138. data/app/serializers/easy_ml/settings_serializer.rb +9 -0
  139. data/app/views/layouts/easy_ml/application.html.erb +15 -0
  140. data/config/initializers/resque.rb +3 -0
  141. data/config/resque-pool.yml +6 -0
  142. data/config/routes.rb +39 -0
  143. data/config/spring.rb +1 -0
  144. data/config/vite.json +15 -0
  145. data/lib/easy_ml/configuration.rb +64 -0
  146. data/lib/easy_ml/core/evaluators/base_evaluator.rb +53 -0
  147. data/lib/easy_ml/core/evaluators/classification_evaluators.rb +126 -0
  148. data/lib/easy_ml/core/evaluators/regression_evaluators.rb +66 -0
  149. data/lib/easy_ml/core/model_evaluator.rb +161 -89
  150. data/lib/easy_ml/core/tuner/adapters/base_adapter.rb +28 -18
  151. data/lib/easy_ml/core/tuner/adapters/xgboost_adapter.rb +4 -25
  152. data/lib/easy_ml/core/tuner.rb +123 -62
  153. data/lib/easy_ml/core.rb +0 -3
  154. data/lib/easy_ml/core_ext/hash.rb +24 -0
  155. data/lib/easy_ml/core_ext/pathname.rb +11 -5
  156. data/lib/easy_ml/data/date_converter.rb +90 -0
  157. data/lib/easy_ml/data/filter_extensions.rb +31 -0
  158. data/lib/easy_ml/data/polars_column.rb +126 -0
  159. data/lib/easy_ml/data/polars_reader.rb +297 -0
  160. data/lib/easy_ml/data/preprocessor.rb +280 -142
  161. data/lib/easy_ml/data/simple_imputer.rb +255 -0
  162. data/lib/easy_ml/data/splits/file_split.rb +252 -0
  163. data/lib/easy_ml/data/splits/in_memory_split.rb +54 -0
  164. data/lib/easy_ml/data/splits/split.rb +95 -0
  165. data/lib/easy_ml/data/splits.rb +9 -0
  166. data/lib/easy_ml/data/statistics_learner.rb +93 -0
  167. data/lib/easy_ml/data/synced_directory.rb +341 -0
  168. data/lib/easy_ml/data.rb +6 -2
  169. data/lib/easy_ml/engine.rb +105 -6
  170. data/lib/easy_ml/feature_store.rb +227 -0
  171. data/lib/easy_ml/features.rb +61 -0
  172. data/lib/easy_ml/initializers/inflections.rb +17 -3
  173. data/lib/easy_ml/logging.rb +2 -2
  174. data/lib/easy_ml/predict.rb +74 -0
  175. data/lib/easy_ml/railtie/generators/migration/migration_generator.rb +192 -36
  176. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_column_histories.rb.tt +9 -0
  177. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_columns.rb.tt +25 -0
  178. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_dataset_histories.rb.tt +9 -0
  179. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasets.rb.tt +31 -0
  180. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasource_histories.rb.tt +9 -0
  181. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasources.rb.tt +16 -0
  182. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_deploys.rb.tt +24 -0
  183. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_events.rb.tt +20 -0
  184. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_feature_histories.rb.tt +14 -0
  185. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_features.rb.tt +32 -0
  186. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_file_histories.rb.tt +9 -0
  187. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_files.rb.tt +17 -0
  188. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_histories.rb.tt +9 -0
  189. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_models.rb.tt +20 -9
  190. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_predictions.rb.tt +17 -0
  191. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_retraining_jobs.rb.tt +77 -0
  192. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_settings.rb.tt +9 -0
  193. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_splitter_histories.rb.tt +9 -0
  194. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_splitters.rb.tt +15 -0
  195. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_tuner_jobs.rb.tt +40 -0
  196. data/lib/easy_ml/support/est.rb +5 -1
  197. data/lib/easy_ml/support/file_rotate.rb +79 -15
  198. data/lib/easy_ml/support/file_support.rb +9 -0
  199. data/lib/easy_ml/support/local_file.rb +24 -0
  200. data/lib/easy_ml/support/lockable.rb +62 -0
  201. data/lib/easy_ml/support/synced_file.rb +103 -0
  202. data/lib/easy_ml/support/utc.rb +5 -1
  203. data/lib/easy_ml/support.rb +6 -3
  204. data/lib/easy_ml/version.rb +4 -1
  205. data/lib/easy_ml.rb +7 -2
  206. metadata +355 -72
  207. data/app/models/easy_ml/models.rb +0 -5
  208. data/lib/easy_ml/core/model.rb +0 -30
  209. data/lib/easy_ml/core/model_core.rb +0 -181
  210. data/lib/easy_ml/core/models/hyperparameters/base.rb +0 -34
  211. data/lib/easy_ml/core/models/hyperparameters/xgboost.rb +0 -19
  212. data/lib/easy_ml/core/models/xgboost.rb +0 -10
  213. data/lib/easy_ml/core/models/xgboost_core.rb +0 -220
  214. data/lib/easy_ml/core/models.rb +0 -10
  215. data/lib/easy_ml/core/uploaders/model_uploader.rb +0 -24
  216. data/lib/easy_ml/core/uploaders.rb +0 -7
  217. data/lib/easy_ml/data/dataloader.rb +0 -6
  218. data/lib/easy_ml/data/dataset/data/preprocessor/statistics.json +0 -31
  219. data/lib/easy_ml/data/dataset/data/sample_info.json +0 -1
  220. data/lib/easy_ml/data/dataset/dataset/files/sample_info.json +0 -1
  221. data/lib/easy_ml/data/dataset/splits/file_split.rb +0 -140
  222. data/lib/easy_ml/data/dataset/splits/in_memory_split.rb +0 -49
  223. data/lib/easy_ml/data/dataset/splits/split.rb +0 -98
  224. data/lib/easy_ml/data/dataset/splits.rb +0 -11
  225. data/lib/easy_ml/data/dataset/splitters/date_splitter.rb +0 -43
  226. data/lib/easy_ml/data/dataset/splitters.rb +0 -9
  227. data/lib/easy_ml/data/dataset.rb +0 -430
  228. data/lib/easy_ml/data/datasource/datasource_factory.rb +0 -60
  229. data/lib/easy_ml/data/datasource/file_datasource.rb +0 -40
  230. data/lib/easy_ml/data/datasource/merged_datasource.rb +0 -64
  231. data/lib/easy_ml/data/datasource/polars_datasource.rb +0 -41
  232. data/lib/easy_ml/data/datasource/s3_datasource.rb +0 -89
  233. data/lib/easy_ml/data/datasource.rb +0 -33
  234. data/lib/easy_ml/data/preprocessor/preprocessor.rb +0 -205
  235. data/lib/easy_ml/data/preprocessor/simple_imputer.rb +0 -402
  236. data/lib/easy_ml/deployment.rb +0 -5
  237. data/lib/easy_ml/support/synced_directory.rb +0 -134
  238. data/lib/easy_ml/transforms.rb +0 -29
  239. /data/{lib/easy_ml/core → app/models/easy_ml}/models/hyperparameters.rb +0 -0
@@ -0,0 +1,726 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { X, AlertCircle, Calendar, Settings2, Info, Layers, Repeat } from 'lucide-react';
3
+ import { SearchableSelect } from './SearchableSelect';
4
+
5
+ interface ScheduleModalProps {
6
+ isOpen: boolean;
7
+ onClose: () => void;
8
+ onSave: (data: any) => void;
9
+ initialData: {
10
+ task: string;
11
+ metrics: string[];
12
+ modelType?: string;
13
+ dataset?: {
14
+ columns: Array<{ name: string }>;
15
+ };
16
+ retraining_job?: {
17
+ frequency: string;
18
+ tuning_frequency: string;
19
+ at: {
20
+ hour: number;
21
+ day_of_week?: number;
22
+ day_of_month?: number;
23
+ }
24
+ direction: string;
25
+ batch_mode?: boolean;
26
+ batch_size?: number;
27
+ batch_overlap?: number;
28
+ batch_key?: string;
29
+ active: boolean;
30
+ metric?: string;
31
+ threshold?: number;
32
+ tuner_config?: {
33
+ n_trials: number;
34
+ objective: string;
35
+ config: Record<string, any>;
36
+ };
37
+ tuning_enabled?: boolean;
38
+ };
39
+ };
40
+ tunerJobConstants: any;
41
+ timezone: string;
42
+ retrainingJobConstants: any;
43
+ }
44
+
45
+ const METRICS = {
46
+ classification: [
47
+ { value: 'accuracy_score', label: 'Accuracy', description: 'Overall prediction accuracy' },
48
+ { value: 'precision_score', label: 'Precision', description: 'Ratio of true positives to predicted positives' },
49
+ { value: 'recall_score', label: 'Recall', description: 'Ratio of true positives to actual positives' },
50
+ { value: 'f1_score', label: 'F1 Score', description: 'Harmonic mean of precision and recall' }
51
+ ],
52
+ regression: [
53
+ { value: 'mean_absolute_error', label: 'Mean Absolute Error', description: 'Average absolute differences between predicted and actual values' },
54
+ { value: 'mean_squared_error', label: 'Mean Squared Error', description: 'Average squared differences between predicted and actual values' },
55
+ { value: 'root_mean_squared_error', label: 'Root Mean Squared Error', description: 'Square root of mean squared error' },
56
+ { value: 'r2_score', label: 'R² Score', description: 'Proportion of variance in the target that is predictable' }
57
+ ]
58
+ };
59
+
60
+ export function ScheduleModal({ isOpen, onClose, onSave, initialData, tunerJobConstants, timezone, retrainingJobConstants }: ScheduleModalProps) {
61
+ const [showBatchTrainingInfo, setShowBatchTrainingInfo] = useState(false);
62
+ const [activeBatchPopover, setActiveBatchPopover] = useState<'size' | 'overlap' | null>(null);
63
+
64
+ // Get all base parameters (those with options)
65
+ const baseParameters = Object.entries(tunerJobConstants)
66
+ .filter(([_, value]) => Array.isArray(value.options))
67
+ .reduce((acc, [key, value]) => ({
68
+ ...acc,
69
+ [key]: value.options[0].value // Default to first option if not set
70
+ }), {});
71
+
72
+ // Get default numeric parameters for the default booster
73
+ const defaultBooster = baseParameters.booster;
74
+ const defaultNumericParameters = Object.entries(tunerJobConstants.hyperparameters[defaultBooster] || {})
75
+ .filter(([_, value]) => !Array.isArray(value.options))
76
+ .reduce((acc, [key, value]) => ({
77
+ ...acc,
78
+ [key]: {
79
+ min: value.min,
80
+ max: value.max
81
+ }
82
+ }), {});
83
+
84
+ const [formData, setFormData] = useState({
85
+ retraining_job_attributes: {
86
+ id: initialData.retraining_job?.id || null,
87
+ active: initialData.retraining_job?.active ?? false,
88
+ frequency: initialData.retraining_job?.frequency || retrainingJobConstants.frequency[0].value as string,
89
+ tuning_frequency: initialData.retraining_job?.tuning_frequency || 'month',
90
+ direction: initialData.retraining_job?.direction || 'maximize',
91
+ batch_mode: initialData.retraining_job?.batch_mode || false,
92
+ batch_size: initialData.retraining_job?.batch_size || 100,
93
+ batch_overlap: initialData.retraining_job?.batch_overlap || 1,
94
+ batch_key: initialData.retraining_job?.batch_key || '',
95
+ at: {
96
+ hour: initialData.retraining_job?.at?.hour ?? 2,
97
+ day_of_week: initialData.retraining_job?.at?.day_of_week ?? 1,
98
+ day_of_month: initialData.retraining_job?.at?.day_of_month ?? 1
99
+ },
100
+ metric: initialData.retraining_job?.metric || METRICS[initialData.task === 'classification' ? 'classification' : 'regression'][0].value,
101
+ threshold: initialData.retraining_job?.threshold || (initialData.task === 'classification' ? 0.85 : 0.1),
102
+ tuner_config: initialData.retraining_job?.tuner_config ? {
103
+ n_trials: initialData.retraining_job.tuner_config.n_trials || 10,
104
+ config: {
105
+ ...baseParameters,
106
+ ...defaultNumericParameters,
107
+ ...initialData.retraining_job.tuner_config.config
108
+ }
109
+ } : undefined,
110
+ tuning_enabled: initialData.retraining_job?.tuning_enabled ?? false
111
+ }
112
+ });
113
+
114
+ useEffect(() => {
115
+ if (formData.retraining_job_attributes.tuner_config && Object.keys(formData.retraining_job_attributes.tuner_config.config).length === 0) {
116
+ setFormData(prev => ({
117
+ ...prev,
118
+ retraining_job_attributes: {
119
+ ...prev.retraining_job_attributes,
120
+ tuner_config: {
121
+ ...prev.retraining_job_attributes.tuner_config,
122
+ config: {
123
+ ...baseParameters,
124
+ ...defaultNumericParameters
125
+ }
126
+ }
127
+ }
128
+ }));
129
+ }
130
+ }, [formData.retraining_job_attributes.tuner_config]);
131
+
132
+ if (!isOpen) return null;
133
+
134
+ const handleBaseParameterChange = (parameter: string, value: string) => {
135
+ setFormData(prev => ({
136
+ ...prev,
137
+ retraining_job_attributes: {
138
+ ...prev.retraining_job_attributes,
139
+ tuner_config: {
140
+ ...prev.retraining_job_attributes.tuner_config,
141
+ config: {
142
+ ...prev.retraining_job_attributes.tuner_config.config,
143
+ [parameter]: value
144
+ }
145
+ }
146
+ }
147
+ }));
148
+ };
149
+
150
+ const renderHyperparameterControls = () => {
151
+ const baseParameters = Object.entries(tunerJobConstants).filter(
152
+ ([key, value]) => Array.isArray(value.options)
153
+ );
154
+
155
+ // Include all base parameters, not just those in config
156
+ const selectedBaseParams = baseParameters.map(([key]) => key);
157
+
158
+ return (
159
+ <div className="space-y-4">
160
+ {baseParameters.map(([key, value]) => (
161
+ <div key={key}>
162
+ <label className="block text-sm font-medium text-gray-700">
163
+ {value.label}
164
+ </label>
165
+ <SearchableSelect
166
+ options={value.options.map((option: any) => ({
167
+ value: option.value,
168
+ label: option.label,
169
+ description: option.description
170
+ }))}
171
+ value={formData.retraining_job_attributes.tuner_config?.config[key] || value.options[0].value}
172
+ onChange={(val) => handleBaseParameterChange(key, val as string)}
173
+ />
174
+ </div>
175
+ ))}
176
+
177
+ {selectedBaseParams.map(param => {
178
+ const subParams = Object.entries(tunerJobConstants).filter(
179
+ ([key, value]) => value.depends_on === param
180
+ );
181
+
182
+ const selectedValue = formData.retraining_job_attributes.tuner_config?.config[param] ||
183
+ tunerJobConstants[param].options[0].value; // Use default if not in config
184
+
185
+ return (
186
+ <div key={param} className="space-y-4">
187
+ <h4 className="text-sm font-medium text-gray-900">Parameter Ranges</h4>
188
+ <div className="space-y-4 max-h-[400px] overflow-y-auto pr-2">
189
+ {subParams.map(([subKey, subValue]: any) => {
190
+ const relevantParams = subValue[selectedValue];
191
+ if (!relevantParams) return null;
192
+
193
+ return Object.entries(relevantParams).map(([paramKey, paramValue]: any) => {
194
+ if (paramValue.min !== undefined && paramValue.max !== undefined) {
195
+ return (
196
+ <div key={paramKey} className="bg-gray-50 p-4 rounded-lg">
197
+ <div className="flex items-center justify-between mb-2">
198
+ <label className="text-sm font-medium text-gray-900">
199
+ {paramValue.label}
200
+ </label>
201
+ <span className="text-xs text-gray-500">{paramValue.description}</span>
202
+ </div>
203
+ <div className="grid grid-cols-2 gap-4">
204
+ <div>
205
+ <label className="block text-xs text-gray-500 mb-1">
206
+ Minimum
207
+ </label>
208
+ <input
209
+ type="number"
210
+ min={paramValue.min}
211
+ max={paramValue.max}
212
+ step={paramValue.step}
213
+ value={formData.retraining_job_attributes.tuner_config?.config[paramKey]?.min ?? paramValue.min}
214
+ onChange={(e) => handleParameterChange(
215
+ paramKey,
216
+ 'min',
217
+ parseFloat(e.target.value)
218
+ )}
219
+ className="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
220
+ />
221
+ </div>
222
+ <div>
223
+ <label className="block text-xs text-gray-500 mb-1">
224
+ Maximum
225
+ </label>
226
+ <input
227
+ type="number"
228
+ min={paramValue.min}
229
+ max={paramValue.max}
230
+ step={paramValue.step}
231
+ value={formData.retraining_job_attributes.tuner_config?.config[paramKey]?.max ?? paramValue.max}
232
+ onChange={(e) => handleParameterChange(
233
+ paramKey,
234
+ 'max',
235
+ parseFloat(e.target.value)
236
+ )}
237
+ className="block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
238
+ />
239
+ </div>
240
+ </div>
241
+ </div>
242
+ );
243
+ }
244
+ return null;
245
+ });
246
+ })}
247
+ </div>
248
+ </div>
249
+ );
250
+ })}
251
+ </div>
252
+ );
253
+ };
254
+
255
+ const handleParameterChange = (parameter: string, type: 'min' | 'max', value: number) => {
256
+ setFormData(prev => ({
257
+ ...prev,
258
+ retraining_job_attributes: {
259
+ ...prev.retraining_job_attributes,
260
+ tuner_config: {
261
+ ...prev.retraining_job_attributes.tuner_config,
262
+ config: {
263
+ ...prev.retraining_job_attributes.tuner_config.config,
264
+ [parameter]: {
265
+ ...prev.retraining_job_attributes.tuner_config.config[parameter],
266
+ [type]: value
267
+ }
268
+ }
269
+ }
270
+ }
271
+ }));
272
+ };
273
+
274
+ const handleTrainingScheduleChange = (field: string, value: string | number) => {
275
+ setFormData(prev => {
276
+ if (field === 'hour' || field === 'day_of_week' || field === 'day_of_month') {
277
+ return {
278
+ ...prev,
279
+ retraining_job_attributes: {
280
+ ...prev.retraining_job_attributes,
281
+ at: {
282
+ ...prev.retraining_job_attributes.at,
283
+ [field]: value
284
+ }
285
+ }
286
+ };
287
+ }
288
+ return {
289
+ ...prev,
290
+ retraining_job_attributes: {
291
+ ...prev.retraining_job_attributes,
292
+ [field]: value
293
+ }
294
+ };
295
+ });
296
+ };
297
+
298
+ const handleEvaluatorChange = (field: string, value: string | number) => {
299
+ setFormData(prev => ({
300
+ ...prev,
301
+ retraining_job_attributes: {
302
+ ...prev.retraining_job_attributes,
303
+ [field]: value
304
+ }
305
+ }));
306
+ };
307
+
308
+ const handleSave = () => {
309
+ const { retraining_job_attributes } = formData;
310
+ const at: any = { hour: retraining_job_attributes.at.hour };
311
+
312
+ // Only include relevant date attributes based on frequency
313
+ switch (retraining_job_attributes.frequency) {
314
+ case 'day':
315
+ // For daily frequency, only include hour
316
+ break;
317
+ case 'week':
318
+ // For weekly frequency, include hour and day_of_week
319
+ at.day_of_week = retraining_job_attributes.at.day_of_week;
320
+ break;
321
+ case 'month':
322
+ // For monthly frequency, include hour and day_of_month
323
+ at.day_of_month = retraining_job_attributes.at.day_of_month;
324
+ break;
325
+ }
326
+
327
+ const updatedData = {
328
+ retraining_job_attributes: {
329
+ ...retraining_job_attributes,
330
+ at
331
+ }
332
+ };
333
+
334
+ onSave(updatedData);
335
+ onClose();
336
+ };
337
+
338
+ return (
339
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
340
+ <div className="bg-white rounded-lg w-full max-w-6xl max-h-[90vh] overflow-hidden">
341
+ <div className="flex justify-between items-center p-4 border-b">
342
+ <h2 className="text-lg font-semibold">Training Configuration</h2>
343
+ <button
344
+ onClick={onClose}
345
+ className="text-gray-500 hover:text-gray-700"
346
+ >
347
+ <X className="w-5 h-5" />
348
+ </button>
349
+ </div>
350
+
351
+ <div className="p-6 grid grid-cols-2 gap-8 max-h-[calc(90vh-8rem)] overflow-y-auto">
352
+ {/* Left Column */}
353
+ <div className="space-y-8">
354
+ {/* Training Schedule */}
355
+ <div className="space-y-4">
356
+ <div className="flex items-center justify-between">
357
+ <div className="flex items-center gap-2">
358
+ <Calendar className="w-5 h-5 text-blue-600" />
359
+ <h3 className="text-lg font-medium text-gray-900">Training Schedule</h3>
360
+ </div>
361
+ <div className="flex items-center">
362
+ <input
363
+ type="checkbox"
364
+ id="scheduleEnabled"
365
+ checked={formData.retraining_job_attributes.active}
366
+ onChange={(e) => setFormData(prev => ({
367
+ ...prev,
368
+ retraining_job_attributes: {
369
+ ...prev.retraining_job_attributes,
370
+ active: e.target.checked
371
+ }
372
+ }))}
373
+ className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
374
+ />
375
+ <label htmlFor="scheduleEnabled" className="ml-2 text-sm text-gray-700">
376
+ Enable scheduled training
377
+ </label>
378
+ </div>
379
+ </div>
380
+
381
+ {!formData.retraining_job_attributes.active && (
382
+ <div className="bg-gray-50 rounded-lg p-4">
383
+ <div className="flex items-start gap-2">
384
+ <AlertCircle className="w-5 h-5 text-gray-400 mt-0.5" />
385
+ <div>
386
+ <h4 className="text-sm font-medium text-gray-900">Manual Training Mode</h4>
387
+ <p className="mt-1 text-sm text-gray-500">
388
+ The model will only be trained when you manually trigger training. You can do this from the model details page at any time.
389
+ </p>
390
+ </div>
391
+ </div>
392
+ </div>
393
+ )}
394
+
395
+ {formData.retraining_job_attributes.active && (
396
+ <>
397
+ <div className="space-y-6">
398
+ <div className="grid grid-cols-2 gap-4">
399
+ <div>
400
+ <label className="block text-sm font-medium text-gray-700">
401
+ Frequency
402
+ </label>
403
+ <SearchableSelect
404
+ options={retrainingJobConstants.frequency.map((freq: { value: string; label: string; description: string }) => ({
405
+ value: freq.value,
406
+ label: freq.label,
407
+ description: freq.description
408
+ }))}
409
+ value={formData.retraining_job_attributes.frequency}
410
+ onChange={(value) => handleTrainingScheduleChange('frequency', value)}
411
+ />
412
+ </div>
413
+
414
+ {formData.retraining_job_attributes.frequency === 'week' && (
415
+ <div>
416
+ <label className="block text-sm font-medium text-gray-700">
417
+ Day of Week
418
+ </label>
419
+ <SearchableSelect
420
+ options={[
421
+ { value: 0, label: 'Sunday' },
422
+ { value: 1, label: 'Monday' },
423
+ { value: 2, label: 'Tuesday' },
424
+ { value: 3, label: 'Wednesday' },
425
+ { value: 4, label: 'Thursday' },
426
+ { value: 5, label: 'Friday' },
427
+ { value: 6, label: 'Saturday' }
428
+ ]}
429
+ value={formData.retraining_job_attributes.at.day_of_week}
430
+ onChange={(value) => handleTrainingScheduleChange('day_of_week', value)}
431
+ />
432
+ </div>
433
+ )}
434
+
435
+ {formData.retraining_job_attributes.frequency === 'month' && (
436
+ <div>
437
+ <label className="block text-sm font-medium text-gray-700">
438
+ Day of Month
439
+ </label>
440
+ <SearchableSelect
441
+ options={Array.from({ length: 31 }, (_, i) => ({
442
+ value: i + 1,
443
+ label: `Day ${i + 1}`
444
+ }))}
445
+ value={formData.retraining_job_attributes.at.day_of_month}
446
+ onChange={(value) => handleTrainingScheduleChange('day_of_month', value)}
447
+ />
448
+ </div>
449
+ )}
450
+
451
+ <div>
452
+ <label className="block text-sm font-medium text-gray-700">
453
+ Hour ({timezone})
454
+ </label>
455
+ <SearchableSelect
456
+ options={Array.from({ length: 24 }, (_, i) => ({
457
+ value: i,
458
+ label: `${i}:00`
459
+ }))}
460
+ value={formData.retraining_job_attributes.at.hour}
461
+ onChange={(value) => handleTrainingScheduleChange('hour', value)}
462
+ />
463
+ </div>
464
+ </div>
465
+ </div>
466
+ </>
467
+ )}
468
+
469
+ {/* Batch Training Options */}
470
+ <div className="space-y-4 pt-4 border-t">
471
+ <div className="flex items-center justify-between">
472
+ <div className="flex items-center gap-2">
473
+ <label htmlFor="batchTrainingEnabled" className="text-sm font-medium text-gray-700 flex items-center gap-2">
474
+ Enable Batch Training
475
+ <button
476
+ type="button"
477
+ onClick={() => setShowBatchTrainingInfo(!showBatchTrainingInfo)}
478
+ className="text-gray-400 hover:text-gray-600"
479
+ >
480
+ <Info className="w-4 h-4" />
481
+ </button>
482
+ </label>
483
+ </div>
484
+ <input
485
+ type="checkbox"
486
+ id="batchMode"
487
+ checked={formData.retraining_job_attributes.batch_mode}
488
+ onChange={(e) => setFormData({ ...formData, retraining_job_attributes: { ...formData.retraining_job_attributes, batch_mode: e.target.checked } })}
489
+ className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
490
+ />
491
+ </div>
492
+
493
+ {showBatchTrainingInfo && (
494
+ <div className="bg-blue-50 rounded-lg p-4 text-sm text-blue-700">
495
+ <ul className="space-y-2">
496
+ <li>• When disabled, the model will train on the entire dataset in a single pass.</li>
497
+ <li>• When enabled, the model will learn from small batches of data iteratively, improving training speed</li>
498
+ </ul>
499
+ </div>
500
+ )}
501
+
502
+ {formData.retraining_job_attributes.batch_mode && (
503
+ <div className="mt-4">
504
+ <div className="flex items-center gap-2">
505
+ <div className="flex-1">
506
+ <label className="block text-sm font-medium text-gray-700">
507
+ Batch Size
508
+ </label>
509
+ <input
510
+ type="number"
511
+ value={formData.retraining_job_attributes.batch_size}
512
+ onChange={(e) => setFormData({
513
+ ...formData,
514
+ retraining_job_attributes: {
515
+ ...formData.retraining_job_attributes,
516
+ batch_size: parseInt(e.target.value)
517
+ }
518
+ })}
519
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
520
+ />
521
+ </div>
522
+ <div className="flex-1">
523
+ <label className="block text-sm font-medium text-gray-700">
524
+ Batch Overlap
525
+ </label>
526
+ <input
527
+ type="number"
528
+ value={formData.retraining_job_attributes.batch_overlap}
529
+ onChange={(e) => setFormData({
530
+ ...formData,
531
+ retraining_job_attributes: {
532
+ ...formData.retraining_job_attributes,
533
+ batch_overlap: parseInt(e.target.value)
534
+ }
535
+ })}
536
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm"
537
+ />
538
+ </div>
539
+ </div>
540
+ <div className="mt-4">
541
+ <label className="block text-sm font-medium text-gray-700">
542
+ Batch Key
543
+ </label>
544
+ <SearchableSelect
545
+ value={formData.retraining_job_attributes.batch_key}
546
+ onChange={(value) => setFormData({
547
+ ...formData,
548
+ retraining_job_attributes: {
549
+ ...formData.retraining_job_attributes,
550
+ batch_key: value
551
+ }
552
+ })}
553
+ options={initialData.dataset?.columns?.map(column => ({
554
+ value: column.name,
555
+ label: column.name
556
+ })) || []}
557
+ placeholder="Select a column for batch key"
558
+ />
559
+ </div>
560
+ </div>
561
+ )}
562
+ </div>
563
+
564
+ {/* Evaluator Configuration */}
565
+ <div className="border-t border-gray-200 pt-6">
566
+ <div className="flex items-center gap-2 mb-4">
567
+ <AlertCircle className="w-5 h-5 text-blue-600" />
568
+ <h3 className="text-lg font-medium text-gray-900">Evaluator Configuration</h3>
569
+ </div>
570
+
571
+ <div className="space-y-6">
572
+ <div className="grid grid-cols-2 gap-4">
573
+ <div>
574
+ <label className="block text-sm font-medium text-gray-700">
575
+ Metric
576
+ </label>
577
+ <SearchableSelect
578
+ options={METRICS[initialData.task === 'classification' ? 'classification' : 'regression'].map((metric) => ({
579
+ value: metric.value,
580
+ label: metric.label,
581
+ description: metric.description
582
+ }))}
583
+ value={formData.retraining_job_attributes.metric}
584
+ onChange={(value) => handleEvaluatorChange('metric', value)}
585
+ />
586
+ </div>
587
+
588
+ <div>
589
+ <label className="block text-sm font-medium text-gray-700">
590
+ Threshold
591
+ </label>
592
+ <input
593
+ type="number"
594
+ value={formData.retraining_job_attributes.threshold}
595
+ onChange={(e) => handleEvaluatorChange('threshold', parseFloat(e.target.value))}
596
+ step={0.01}
597
+ min={0}
598
+ max={1}
599
+ 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"
600
+ />
601
+ </div>
602
+
603
+ </div>
604
+
605
+ {/* Deployment Criteria */}
606
+ <div className="bg-blue-50 rounded-md p-4">
607
+ <div className="flex items-start">
608
+ <AlertCircle className="w-5 h-5 text-blue-400 mt-0.5" />
609
+ <div className="ml-3">
610
+ <h3 className="text-sm font-medium text-blue-800">Deployment Criteria</h3>
611
+ <p className="mt-2 text-sm text-blue-700">
612
+ The model will be automatically deployed when the {formData.retraining_job_attributes.metric} is{' '}
613
+ {formData.retraining_job_attributes.direction === 'minimize' ? 'below' : 'above'} {formData.retraining_job_attributes.threshold}.
614
+ </p>
615
+ </div>
616
+ </div>
617
+ </div>
618
+ </div>
619
+ </div>
620
+ </div>
621
+ </div>
622
+
623
+ {/* Right Column */}
624
+ <div className="space-y-8">
625
+ <div className="space-y-4">
626
+ <div className="flex items-center justify-between">
627
+ <div className="flex items-center gap-2">
628
+ <Settings2 className="w-5 h-5 text-blue-600" />
629
+ <h3 className="text-lg font-medium text-gray-900">Hyperparameter Tuning</h3>
630
+ </div>
631
+ <div className="flex items-center">
632
+ <input
633
+ type="checkbox"
634
+ id="tuningEnabled"
635
+ checked={formData.retraining_job_attributes.tuning_enabled || false}
636
+ onChange={(e) => setFormData(prev => ({
637
+ ...prev,
638
+ retraining_job_attributes: {
639
+ ...prev.retraining_job_attributes,
640
+ tuning_enabled: e.target.checked,
641
+ tuner_config: e.target.checked ? {
642
+ n_trials: 10,
643
+ config: defaultNumericParameters
644
+ } : undefined
645
+ }
646
+ }))}
647
+ className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
648
+ />
649
+ <label htmlFor="tuningEnabled" className="ml-2 text-sm text-gray-700">
650
+ Enable tuning
651
+ </label>
652
+ </div>
653
+ </div>
654
+
655
+ {formData.retraining_job_attributes.tuning_enabled && (
656
+ <div className="space-y-6">
657
+ <div className="grid grid-cols-2 gap-4">
658
+ <div>
659
+ <label className="block text-sm font-medium text-gray-700">
660
+ Frequency
661
+ </label>
662
+ <SearchableSelect
663
+ options={[
664
+ { value: 'always', label: 'Always', description: 'Tune hyperparameters every time' },
665
+ { value: 'week', label: 'Weekly', description: 'Tune hyperparameters once every week' },
666
+ { value: 'month', label: 'Monthly', description: 'Tune hyperparameters once every month' }
667
+ ]}
668
+ value={formData.retraining_job_attributes.tuning_frequency || 'week'}
669
+ onChange={(value) => setFormData(prev => ({
670
+ ...prev,
671
+ retraining_job_attributes: {
672
+ ...prev.retraining_job_attributes,
673
+ tuning_frequency: value as 'week' | 'month' | 'always'
674
+ }
675
+ }))}
676
+ />
677
+ </div>
678
+
679
+ <div>
680
+ <label className="block text-sm font-medium text-gray-700">
681
+ Number of Trials
682
+ </label>
683
+ <input
684
+ type="number"
685
+ min="1"
686
+ max="1000"
687
+ value={formData.retraining_job_attributes.tuner_config?.n_trials || 10}
688
+ onChange={(e) => setFormData(prev => ({
689
+ ...prev,
690
+ retraining_job_attributes: {
691
+ ...prev.retraining_job_attributes,
692
+ tuner_config: {
693
+ ...prev.retraining_job_attributes.tuner_config,
694
+ n_trials: parseInt(e.target.value)
695
+ }
696
+ }
697
+ }))}
698
+ 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"
699
+ />
700
+ </div>
701
+ </div>
702
+ {renderHyperparameterControls()}
703
+ </div>
704
+ )}
705
+ </div>
706
+ </div>
707
+ </div>
708
+
709
+ <div className="flex justify-end gap-4 p-4 border-t">
710
+ <button
711
+ onClick={onClose}
712
+ className="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-500"
713
+ >
714
+ Cancel
715
+ </button>
716
+ <button
717
+ onClick={handleSave}
718
+ className="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md"
719
+ >
720
+ Save Changes
721
+ </button>
722
+ </div>
723
+ </div>
724
+ </div>
725
+ );
726
+ }