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.
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
+ }