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