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