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,23 @@
1
+ import React from 'react';
2
+ import { Search } from 'lucide-react';
3
+
4
+ interface SearchInputProps {
5
+ value: string;
6
+ onChange: (value: string) => void;
7
+ placeholder?: string;
8
+ }
9
+
10
+ export function SearchInput({ value, onChange, placeholder = 'Search...' }: SearchInputProps) {
11
+ return (
12
+ <div className="relative">
13
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
14
+ <input
15
+ type="text"
16
+ value={value}
17
+ onChange={(e) => onChange(e.target.value)}
18
+ placeholder={placeholder}
19
+ className="pl-9 pr-4 py-2 w-64 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
20
+ />
21
+ </div>
22
+ );
23
+ }
@@ -0,0 +1,132 @@
1
+ import React, { useState, useRef, useEffect, forwardRef } from 'react';
2
+ import { Search, Check } from 'lucide-react';
3
+
4
+ interface Option {
5
+ value: string | number;
6
+ label: string;
7
+ description?: string;
8
+ metadata?: Record<string, any>;
9
+ }
10
+
11
+ interface SearchableSelectProps {
12
+ options: Option[];
13
+ value: Option['value'] | null;
14
+ onChange: (value: Option['value']) => void;
15
+ placeholder?: string;
16
+ renderOption?: (option: Option) => React.ReactNode;
17
+ }
18
+
19
+ export const SearchableSelect = forwardRef<HTMLButtonElement, SearchableSelectProps>(
20
+ ({ options, value, onChange, placeholder = 'Search...', renderOption }, ref) => {
21
+ const [isOpen, setIsOpen] = useState(false);
22
+ const [searchQuery, setSearchQuery] = useState('');
23
+ const containerRef = useRef<HTMLDivElement>(null);
24
+ const inputRef = useRef<HTMLInputElement>(null);
25
+
26
+ const selectedOption = options.find(opt => opt.value === value);
27
+
28
+ const filteredOptions = options.filter(option =>
29
+ option.label.toLowerCase().includes(searchQuery.toLowerCase()) ||
30
+ option.description?.toLowerCase().includes(searchQuery.toLowerCase())
31
+ );
32
+
33
+ useEffect(() => {
34
+ function handleClickOutside(event: MouseEvent) {
35
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
36
+ setIsOpen(false);
37
+ }
38
+ }
39
+
40
+ document.addEventListener('mousedown', handleClickOutside);
41
+ return () => document.removeEventListener('mousedown', handleClickOutside);
42
+ }, []);
43
+
44
+ useEffect(() => {
45
+ if (isOpen && inputRef.current) {
46
+ inputRef.current.focus();
47
+ }
48
+ }, [isOpen]);
49
+
50
+ return (
51
+ <div className="relative" ref={containerRef}>
52
+ <button
53
+ type="button"
54
+ onClick={() => setIsOpen(!isOpen)}
55
+ className="w-full bg-white relative border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-pointer focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
56
+ ref={ref}
57
+ >
58
+ {selectedOption ? (
59
+ <span className="block truncate">{selectedOption.label}</span>
60
+ ) : (
61
+ <span className="block truncate text-gray-500">{placeholder}</span>
62
+ )}
63
+ </button>
64
+
65
+ {isOpen && (
66
+ <div className="absolute z-10 mt-1 w-full bg-white shadow-lg max-h-96 rounded-md overflow-hidden">
67
+ <div className="p-2 border-b">
68
+ <div className="relative">
69
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
70
+ <input
71
+ ref={inputRef}
72
+ type="text"
73
+ className="w-full pl-9 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
74
+ placeholder="Search..."
75
+ value={searchQuery}
76
+ onChange={(e) => setSearchQuery(e.target.value)}
77
+ onClick={(e) => e.stopPropagation()}
78
+ />
79
+ </div>
80
+ </div>
81
+
82
+ <div className="max-h-60 overflow-y-auto">
83
+ {filteredOptions.length === 0 ? (
84
+ <div className="text-center py-4 text-sm text-gray-500">
85
+ No results found
86
+ </div>
87
+ ) : (
88
+ <ul className="py-1">
89
+ {filteredOptions.map((option) => (
90
+ <li key={option.value}>
91
+ <button
92
+ type="button"
93
+ className={`w-full text-left px-4 py-2 hover:bg-gray-100 ${
94
+ option.value === value ? 'bg-blue-50' : ''
95
+ }`}
96
+ onClick={() => {
97
+ onChange(option.value);
98
+ setIsOpen(false);
99
+ setSearchQuery('');
100
+ }}
101
+ >
102
+ {renderOption ? (
103
+ renderOption(option)
104
+ ) : (
105
+ <div className="flex items-center justify-between">
106
+ <div>
107
+ <div className="font-medium">{option.label}</div>
108
+ {option.description && (
109
+ <div className="text-sm text-gray-500">
110
+ {option.description}
111
+ </div>
112
+ )}
113
+ </div>
114
+ {option.value === value && (
115
+ <Check className="w-4 h-4 text-blue-600" />
116
+ )}
117
+ </div>
118
+ )}
119
+ </button>
120
+ </li>
121
+ ))}
122
+ </ul>
123
+ )}
124
+ </div>
125
+ </div>
126
+ )}
127
+ </div>
128
+ );
129
+ }
130
+ );
131
+
132
+ SearchableSelect.displayName = 'SearchableSelect';
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ import { Save, AlertCircle, Loader2 } from 'lucide-react';
3
+
4
+ interface AutosaveIndicatorProps {
5
+ saving: boolean;
6
+ saved: boolean;
7
+ error: string | null;
8
+ }
9
+
10
+ export function AutosaveIndicator({ saving, saved, error }: AutosaveIndicatorProps) {
11
+ if (error) {
12
+ return (
13
+ <div className="flex items-center gap-2 text-red-600">
14
+ <AlertCircle className="w-4 h-4" />
15
+ <span className="text-sm font-medium">{error}</span>
16
+ </div>
17
+ );
18
+ }
19
+
20
+ if (saving) {
21
+ return (
22
+ <div className="flex items-center gap-2 text-blue-600">
23
+ <Loader2 className="w-4 h-4 animate-spin" />
24
+ <span className="text-sm font-medium">Saving changes...</span>
25
+ </div>
26
+ );
27
+ }
28
+
29
+ if (saved) {
30
+ return (
31
+ <div className="flex items-center gap-2 text-green-600">
32
+ <Save className="w-4 h-4" />
33
+ <span className="text-sm font-medium">Changes saved</span>
34
+ </div>
35
+ );
36
+ }
37
+
38
+ return null;
39
+ }
@@ -0,0 +1,431 @@
1
+ import React, { useState, useMemo, useCallback, useEffect } from "react";
2
+ import {
3
+ X,
4
+ Settings2,
5
+ AlertCircle,
6
+ Target,
7
+ EyeOff,
8
+ Search,
9
+ Wand2,
10
+ Play,
11
+ Loader2,
12
+ Sparkles,
13
+ } from "lucide-react";
14
+ import { PreprocessingConfig } from "./PreprocessingConfig";
15
+ import { ColumnList } from "./ColumnList";
16
+ import { ColumnFilters } from "./ColumnFilters";
17
+ import { AutosaveIndicator } from "./AutosaveIndicator";
18
+ import { SearchableSelect } from "../SearchableSelect";
19
+ import { useAutosave } from "../../hooks/useAutosave";
20
+ import { Dataset, Column, Feature } from "../../types/dataset";
21
+ import type { PreprocessingStep } from "../../types/dataset";
22
+ import { FeaturePicker } from "./FeaturePicker";
23
+ import { router } from "@inertiajs/react";
24
+
25
+ interface ColumnConfig {
26
+ targetColumn?: string;
27
+ }
28
+
29
+ interface ColumnConfigModalProps {
30
+ isOpen: boolean;
31
+ onClose: () => void;
32
+ initialDataset: Dataset;
33
+ onSave: (dataset: Dataset) => Promise<void>;
34
+ constants: any;
35
+ }
36
+
37
+ export function ColumnConfigModal({
38
+ isOpen,
39
+ onClose,
40
+ initialDataset,
41
+ onSave,
42
+ constants,
43
+ }: ColumnConfigModalProps) {
44
+ const [dataset, setDataset] = useState<Dataset>(initialDataset);
45
+ const [activeTab, setActiveTab] = useState<"columns" | "features">(
46
+ "columns"
47
+ );
48
+ const [isApplying, setIsApplying] = useState(false);
49
+ const [config, setConfig] = useState<ColumnConfig>({
50
+ targetColumn: dataset.target,
51
+ });
52
+ const [selectedColumn, setSelectedColumn] = useState<string | null>(null);
53
+ const [searchQuery, setSearchQuery] = useState("");
54
+ const [activeFilters, setActiveFilters] = useState<{
55
+ view: "all" | "training" | "hidden" | "preprocessed" | "nulls";
56
+ types: string[];
57
+ }>({
58
+ view: "all",
59
+ types: [],
60
+ });
61
+ const [needsRefresh, setNeedsRefresh] = useState(
62
+ initialDataset.needs_refresh || false
63
+ );
64
+
65
+ const handleSave = useCallback(
66
+ async (data: Dataset) => {
67
+ await onSave(data);
68
+ },
69
+ [onSave]
70
+ );
71
+
72
+ const { saving, saved, error } = useAutosave(dataset, handleSave, 2000);
73
+
74
+ const colHasPreprocessingSteps = (col: Column) => {
75
+ return (
76
+ col.preprocessing_steps?.training != null &&
77
+ col.preprocessing_steps?.training?.method !== "none"
78
+ );
79
+ };
80
+
81
+ const filteredColumns = useMemo(() => {
82
+ return dataset.columns.filter((column) => {
83
+ const matchesSearch = column.name
84
+ .toLowerCase()
85
+ .includes(searchQuery.toLowerCase());
86
+ const matchesType =
87
+ activeFilters.types.length === 0 ||
88
+ activeFilters.types.includes(column.datatype);
89
+
90
+ const matchesView = (() => {
91
+ switch (activeFilters.view) {
92
+ case "training":
93
+ return !column.hidden && !column.drop_if_null;
94
+ case "hidden":
95
+ return column.hidden;
96
+ case "preprocessed":
97
+ return colHasPreprocessingSteps(column);
98
+ case "nulls":
99
+ return (column.statistics?.processed?.null_count || 0) > 0;
100
+ default:
101
+ return true;
102
+ }
103
+ })();
104
+
105
+ return matchesSearch && matchesType && matchesView;
106
+ });
107
+ }, [dataset.columns, searchQuery, activeFilters]);
108
+
109
+ const columnStats = useMemo(
110
+ () => ({
111
+ total: dataset.columns.length,
112
+ filtered: filteredColumns.length,
113
+ training: dataset.columns.filter((c) => !c.hidden && !c.drop_if_null)
114
+ .length,
115
+ hidden: dataset.columns.filter((c) => c.hidden).length,
116
+ withPreprocessing: dataset.columns.filter(colHasPreprocessingSteps)
117
+ .length,
118
+ withNulls: dataset.columns.filter(
119
+ (c) => (c.statistics?.processed?.null_count || 0) > 0
120
+ ).length,
121
+ }),
122
+ [dataset.columns, filteredColumns]
123
+ );
124
+
125
+ const columnTypes = useMemo(
126
+ () => Array.from(new Set(dataset.columns.map((c) => c.datatype))),
127
+ [dataset.columns]
128
+ );
129
+
130
+ const handleColumnSelect = (columnName: string) => {
131
+ setSelectedColumn(columnName);
132
+ };
133
+
134
+ const toggleHiddenColumn = (columnName: string) => {
135
+ const updatedColumns = dataset.columns.map((c) => ({
136
+ ...c,
137
+ hidden: c.name === columnName ? !c.hidden : c.hidden,
138
+ }));
139
+
140
+ setDataset({
141
+ ...dataset,
142
+ columns: updatedColumns,
143
+ });
144
+ setNeedsRefresh(true);
145
+ };
146
+
147
+ const setTargetColumn = (columnName: string) => {
148
+ const name = String(columnName);
149
+ setConfig({ targetColumn: columnName });
150
+ const updatedColumns = dataset.columns.map((c) => ({
151
+ ...c,
152
+ is_target: c.name === name,
153
+ }));
154
+
155
+ setDataset({
156
+ ...dataset,
157
+ columns: updatedColumns,
158
+ });
159
+ setNeedsRefresh(true);
160
+ };
161
+
162
+ const setColumnType = (columnName: string, datatype: string) => {
163
+ const updatedColumns = dataset.columns.map((c) => ({
164
+ ...c,
165
+ datatype: c.name === columnName ? datatype : c.datatype,
166
+ }));
167
+
168
+ setDataset({
169
+ ...dataset,
170
+ columns: updatedColumns,
171
+ });
172
+ setNeedsRefresh(true);
173
+ };
174
+
175
+ const handlePreprocessingUpdate = (
176
+ columnName: string,
177
+ training: PreprocessingStep,
178
+ inference: PreprocessingStep | undefined,
179
+ useDistinctInference: boolean
180
+ ) => {
181
+ const column = dataset.columns.find((c) => c.name === columnName);
182
+ if (!column) return;
183
+
184
+ const updatedColumns = dataset.columns.map((c) => {
185
+ if (c.name !== columnName) return c;
186
+
187
+ return {
188
+ ...c,
189
+ preprocessing_steps: {
190
+ training,
191
+ ...(useDistinctInference && inference ? { inference } : {}),
192
+ },
193
+ };
194
+ });
195
+
196
+ setDataset({
197
+ ...dataset,
198
+ columns: updatedColumns,
199
+ });
200
+ setNeedsRefresh(true);
201
+ };
202
+
203
+ const handleFeaturesChange = (newFeatures: Feature[]) => {
204
+ const existingFeatures = dataset.features || [];
205
+
206
+ const removedFeatures = existingFeatures
207
+ .filter(
208
+ (existing) => !newFeatures.find((t) => t.name === existing.name)
209
+ )
210
+ .map((feature) => ({ ...feature, _destroy: true }));
211
+
212
+ const featuresWithDatasetId = [
213
+ ...newFeatures,
214
+ ...removedFeatures,
215
+ ].map((feature, index) => ({
216
+ ...feature,
217
+ dataset_id: dataset.id,
218
+ feature_position: index,
219
+ }));
220
+
221
+ setDataset((prevDataset) => ({
222
+ ...prevDataset,
223
+ features: featuresWithDatasetId,
224
+ }));
225
+ setNeedsRefresh(true);
226
+ };
227
+
228
+ const handleApplyChanges = async () => {
229
+ setIsApplying(true);
230
+ try {
231
+ await onSave(dataset);
232
+ router.post(
233
+ `/easy_ml/datasets/${dataset.id}/refresh`,
234
+ {},
235
+ {
236
+ onSuccess: () => {
237
+ setIsApplying(false);
238
+ },
239
+ onError: () => {
240
+ console.error("Error refreshing dataset");
241
+ setIsApplying(false);
242
+ },
243
+ }
244
+ );
245
+ } catch (error) {
246
+ console.error("Error refreshing dataset:", error);
247
+ setIsApplying(false);
248
+ }
249
+ };
250
+
251
+ if (!isOpen) return null;
252
+
253
+ const selectedColumnData = selectedColumn
254
+ ? dataset.columns.find((c) => c.name === selectedColumn)
255
+ : null;
256
+
257
+ return (
258
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
259
+ <div className="bg-white rounded-lg w-full max-w-6xl max-h-[90vh] overflow-hidden flex flex-col">
260
+ <div className="flex justify-between items-center p-4 border-b shrink-0">
261
+ <h2 className="text-lg font-semibold">Column Configuration</h2>
262
+ <div className="flex items-center gap-4">
263
+ <div className="min-w-[0px]">
264
+ <AutosaveIndicator saving={saving} saved={saved} error={error} />
265
+ </div>
266
+ <div className="relative">
267
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
268
+ <input
269
+ type="text"
270
+ placeholder="Search columns..."
271
+ value={searchQuery}
272
+ onChange={(e) => setSearchQuery(e.target.value)}
273
+ className="pl-9 pr-4 py-2 w-64 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
274
+ />
275
+ </div>
276
+ <button
277
+ onClick={onClose}
278
+ className="text-gray-500 hover:text-gray-700"
279
+ >
280
+ <X className="w-5 h-5" />
281
+ </button>
282
+ </div>
283
+ </div>
284
+
285
+ <div className="flex border-b shrink-0">
286
+ <button
287
+ onClick={() => setActiveTab("columns")}
288
+ className={`px-4 py-2 text-sm font-medium border-b-2 ${
289
+ activeTab === "columns"
290
+ ? "border-blue-500 text-blue-600"
291
+ : "border-transparent text-gray-500 hover:text-gray-700"
292
+ }`}
293
+ >
294
+ <div className="flex items-center gap-2">
295
+ <Settings2 className="w-4 h-4" />
296
+ Column Configuration
297
+ </div>
298
+ </button>
299
+ <button
300
+ onClick={() => setActiveTab("features")}
301
+ className={`px-4 py-2 text-sm font-medium border-b-2 ${
302
+ activeTab === "features"
303
+ ? "border-blue-500 text-blue-600"
304
+ : "border-transparent text-gray-500 hover:text-gray-700"
305
+ }`}
306
+ >
307
+ <div className="flex items-center gap-2">
308
+ <Wand2 className="w-4 h-4" />
309
+ Features
310
+ <span className="px-1.5 py-0.5 text-xs font-medium bg-blue-100 text-blue-600 rounded-full">
311
+ {constants.feature_options.length}
312
+ </span>
313
+ </div>
314
+ </button>
315
+
316
+ {needsRefresh && (
317
+ <div className="ml-auto px-4 flex items-center">
318
+ <button
319
+ onClick={handleApplyChanges}
320
+ disabled={isApplying}
321
+ className="group relative inline-flex items-center gap-2 px-6 py-2.5 bg-gradient-to-r from-blue-600 to-indigo-600 text-white text-sm font-medium rounded-md hover:from-blue-700 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 transition-all duration-200 shadow-md hover:shadow-lg"
322
+ >
323
+ <div className="absolute inset-0 bg-white/10 rounded-md opacity-0 group-hover:opacity-100 transition-opacity duration-200" />
324
+ {isApplying ? (
325
+ <>
326
+ <Loader2 className="w-4 h-4 animate-spin" />
327
+ Applying Preprocessing...
328
+ </>
329
+ ) : (
330
+ <>
331
+ <Sparkles className="w-4 h-4" />
332
+ Apply Preprocessing
333
+ </>
334
+ )}
335
+ </button>
336
+ </div>
337
+ )}
338
+ </div>
339
+
340
+ {activeTab === "columns" ? (
341
+ <React.Fragment>
342
+ <div className="grid grid-cols-7 flex-1 min-h-0">
343
+ <div className="col-span-3 border-r overflow-hidden flex flex-col">
344
+ <div className="p-4 border-b shrink-0">
345
+ <label className="block text-sm font-medium text-gray-700">
346
+ Target Column
347
+ </label>
348
+ <SearchableSelect
349
+ options={dataset.columns.map((column) => ({
350
+ value: column.name,
351
+ label: column.name,
352
+ }))}
353
+ value={config.targetColumn || ""}
354
+ onChange={(value) =>
355
+ value && setTargetColumn(String(value))
356
+ }
357
+ />
358
+ </div>
359
+ <div className="shrink-0">
360
+ <ColumnFilters
361
+ types={columnTypes}
362
+ activeFilters={activeFilters}
363
+ onFilterChange={setActiveFilters}
364
+ columnStats={columnStats}
365
+ columns={dataset.columns}
366
+ colHasPreprocessingSteps={colHasPreprocessingSteps}
367
+ />
368
+ </div>
369
+
370
+ <div className="flex-1 overflow-y-auto p-4 min-h-0">
371
+ <ColumnList
372
+ columns={filteredColumns}
373
+ selectedColumn={selectedColumn}
374
+ onColumnSelect={handleColumnSelect}
375
+ onToggleHidden={toggleHiddenColumn}
376
+ />
377
+ </div>
378
+ </div>
379
+
380
+ <div className="col-span-4 overflow-y-auto p-4">
381
+ {selectedColumnData ? (
382
+ <PreprocessingConfig
383
+ column={selectedColumnData}
384
+ dataset={dataset}
385
+ setColumnType={setColumnType}
386
+ setDataset={setDataset}
387
+ constants={constants}
388
+ onUpdate={(training, inference, useDistinctInference) =>
389
+ handlePreprocessingUpdate(
390
+ selectedColumnData.name,
391
+ training,
392
+ inference,
393
+ useDistinctInference
394
+ )
395
+ }
396
+ />
397
+ ) : (
398
+ <div className="h-full flex items-center justify-center text-gray-500">
399
+ Select a column to configure preprocessing
400
+ </div>
401
+ )}
402
+ </div>
403
+ </div>
404
+ <div className="border-t p-4 flex justify-between items-center shrink-0">
405
+ <div className="text-sm text-gray-600">
406
+ {dataset.columns.filter((c) => !c.hidden).length} columns
407
+ selected for training
408
+ </div>
409
+ <div className="flex gap-3">
410
+ <button
411
+ onClick={onClose}
412
+ className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
413
+ >
414
+ Close
415
+ </button>
416
+ </div>
417
+ </div>
418
+ </React.Fragment>
419
+ ) : (
420
+ <div className="p-6 h-[calc(90vh-8rem)] overflow-y-auto">
421
+ <FeaturePicker
422
+ options={constants.feature_options}
423
+ initialFeatures={dataset.features}
424
+ onFeaturesChange={handleFeaturesChange}
425
+ />
426
+ </div>
427
+ )}
428
+ </div>
429
+ </div>
430
+ );
431
+ }