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,235 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { AlertCircle } from 'lucide-react';
3
+ import type { Dataset, FeatureGroup } from '../../types';
4
+ import { CodeEditor } from './CodeEditor';
5
+ import { DataPreview } from './DataPreview';
6
+
7
+ interface FeatureFormProps {
8
+ datasets: Dataset[];
9
+ groups: FeatureGroup[];
10
+ initialData?: {
11
+ name: string;
12
+ description: string;
13
+ groupId: number;
14
+ testDatasetId: number;
15
+ inputColumns: string[];
16
+ outputColumns: string[];
17
+ code: string;
18
+ };
19
+ onSubmit: (data: any) => void;
20
+ onCancel: () => void;
21
+ }
22
+
23
+ export function FeatureForm({
24
+ datasets,
25
+ groups,
26
+ initialData,
27
+ onSubmit,
28
+ onCancel
29
+ }: FeatureFormProps) {
30
+ const [formData, setFormData] = useState({
31
+ name: initialData?.name || '',
32
+ description: initialData?.description || '',
33
+ groupId: initialData?.groupId || '',
34
+ testDatasetId: initialData?.testDatasetId || '',
35
+ inputColumns: initialData?.inputColumns || [],
36
+ outputColumns: initialData?.outputColumns || [],
37
+ code: initialData?.code || ''
38
+ });
39
+
40
+ const [selectedDataset, setSelectedDataset] = useState<Dataset | null>(
41
+ initialData?.testDatasetId
42
+ ? datasets.find(d => d.id === initialData.testDatasetId) || null
43
+ : null
44
+ );
45
+
46
+ const handleSubmit = (e: React.FormEvent) => {
47
+ e.preventDefault();
48
+ onSubmit(formData);
49
+ };
50
+
51
+ const handleDatasetChange = (datasetId: string) => {
52
+ const dataset = datasets.find(d => d.id === Number(datasetId)) || null;
53
+ setSelectedDataset(dataset);
54
+ setFormData(prev => ({
55
+ ...prev,
56
+ testDatasetId: datasetId,
57
+ inputColumns: [],
58
+ outputColumns: []
59
+ }));
60
+ };
61
+
62
+ const toggleColumn = (columnName: string, type: 'input' | 'output') => {
63
+ setFormData(prev => ({
64
+ ...prev,
65
+ [type === 'input' ? 'inputColumns' : 'outputColumns']:
66
+ prev[type === 'input' ? 'inputColumns' : 'outputColumns'].includes(columnName)
67
+ ? prev[type === 'input' ? 'inputColumns' : 'outputColumns'].filter(c => c !== columnName)
68
+ : [...prev[type === 'input' ? 'inputColumns' : 'outputColumns'], columnName]
69
+ }));
70
+ };
71
+
72
+ return (
73
+ <form onSubmit={handleSubmit} className="p-6 space-y-8">
74
+ <div className="grid grid-cols-2 gap-6">
75
+ <div>
76
+ <label htmlFor="name" className="block text-sm font-medium text-gray-700">
77
+ Name
78
+ </label>
79
+ <input
80
+ type="text"
81
+ id="name"
82
+ value={formData.name}
83
+ onChange={(e) => setFormData({ ...formData, name: e.target.value })}
84
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
85
+ required
86
+ />
87
+ </div>
88
+
89
+ <div>
90
+ <label htmlFor="group" className="block text-sm font-medium text-gray-700">
91
+ Group
92
+ </label>
93
+ <select
94
+ id="group"
95
+ value={formData.groupId}
96
+ onChange={(e) => setFormData({ ...formData, groupId: e.target.value })}
97
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
98
+ required
99
+ >
100
+ <option value="">Select a group...</option>
101
+ {groups.map((group) => (
102
+ <option key={group.id} value={group.id}>
103
+ {group.name}
104
+ </option>
105
+ ))}
106
+ </select>
107
+ </div>
108
+ </div>
109
+
110
+ <div>
111
+ <label htmlFor="description" className="block text-sm font-medium text-gray-700">
112
+ Description
113
+ </label>
114
+ <textarea
115
+ id="description"
116
+ value={formData.description}
117
+ onChange={(e) => setFormData({ ...formData, description: e.target.value })}
118
+ rows={3}
119
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
120
+ />
121
+ </div>
122
+
123
+ <div>
124
+ <label htmlFor="dataset" className="block text-sm font-medium text-gray-700">
125
+ Test Dataset
126
+ </label>
127
+ <select
128
+ id="dataset"
129
+ value={formData.testDatasetId}
130
+ onChange={(e) => handleDatasetChange(e.target.value)}
131
+ className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500"
132
+ required
133
+ >
134
+ <option value="">Select a dataset...</option>
135
+ {datasets.map((dataset) => (
136
+ <option key={dataset.id} value={dataset.id}>
137
+ {dataset.name}
138
+ </option>
139
+ ))}
140
+ </select>
141
+ </div>
142
+
143
+ {selectedDataset && (
144
+ <div className="grid grid-cols-2 gap-6">
145
+ <div>
146
+ <label className="block text-sm font-medium text-gray-700 mb-2">
147
+ Input Columns
148
+ </label>
149
+ <div className="space-y-2">
150
+ {selectedDataset.columns.map((column) => (
151
+ <label
152
+ key={column.name}
153
+ className="flex items-center gap-2 p-2 rounded-md hover:bg-gray-50"
154
+ >
155
+ <input
156
+ type="checkbox"
157
+ checked={formData.inputColumns.includes(column.name)}
158
+ onChange={() => toggleColumn(column.name, 'input')}
159
+ className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
160
+ />
161
+ <span className="text-sm text-gray-900">{column.name}</span>
162
+ <span className="text-xs text-gray-500">({column.type})</span>
163
+ </label>
164
+ ))}
165
+ </div>
166
+ </div>
167
+
168
+ <div>
169
+ <label className="block text-sm font-medium text-gray-700 mb-2">
170
+ Output Columns
171
+ </label>
172
+ <div className="space-y-2">
173
+ {selectedDataset.columns.map((column) => (
174
+ <label
175
+ key={column.name}
176
+ className="flex items-center gap-2 p-2 rounded-md hover:bg-gray-50"
177
+ >
178
+ <input
179
+ type="checkbox"
180
+ checked={formData.outputColumns.includes(column.name)}
181
+ onChange={() => toggleColumn(column.name, 'output')}
182
+ className="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
183
+ />
184
+ <span className="text-sm text-gray-900">{column.name}</span>
185
+ <span className="text-xs text-gray-500">({column.type})</span>
186
+ </label>
187
+ ))}
188
+ </div>
189
+ </div>
190
+ </div>
191
+ )}
192
+
193
+ <div>
194
+ <label className="block text-sm font-medium text-gray-700 mb-2">
195
+ Feature Code
196
+ </label>
197
+ <div className="bg-gray-50 rounded-lg p-4">
198
+ <CodeEditor
199
+ value={formData.code}
200
+ onChange={(code) => setFormData({ ...formData, code })}
201
+ language="ruby"
202
+ />
203
+ </div>
204
+ </div>
205
+
206
+ {selectedDataset && formData.code && (
207
+ <div>
208
+ <h3 className="text-sm font-medium text-gray-900 mb-2">Preview</h3>
209
+ <DataPreview
210
+ dataset={selectedDataset}
211
+ code={formData.code}
212
+ inputColumns={formData.inputColumns}
213
+ outputColumns={formData.outputColumns}
214
+ />
215
+ </div>
216
+ )}
217
+
218
+ <div className="flex justify-end gap-3 pt-6 border-t">
219
+ <button
220
+ type="button"
221
+ onClick={onCancel}
222
+ className="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900"
223
+ >
224
+ Cancel
225
+ </button>
226
+ <button
227
+ type="submit"
228
+ 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"
229
+ >
230
+ {initialData ? 'Save Changes' : 'Create Feature'}
231
+ </button>
232
+ </div>
233
+ </form>
234
+ );
235
+ }
@@ -0,0 +1,54 @@
1
+ import React from 'react';
2
+ // import { Link } from 'react-router-dom';
3
+ import { FolderOpen, Settings, Trash2 } from 'lucide-react';
4
+ import type { FeatureGroup } from '../../types';
5
+
6
+ interface FeatureGroupCardProps {
7
+ group: FeatureGroup;
8
+ }
9
+
10
+ export function FeatureGroupCard({ group }: FeatureGroupCardProps) {
11
+ return (
12
+ <div className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
13
+ <div className="flex justify-between items-start mb-4">
14
+ <div className="flex items-start gap-3">
15
+ <FolderOpen className="w-5 h-5 text-blue-600 mt-1" />
16
+ <div>
17
+ <h3 className="text-lg font-semibold text-gray-900">
18
+ {group.name}
19
+ </h3>
20
+ <p className="text-sm text-gray-500 mt-1">
21
+ {group.description}
22
+ </p>
23
+ </div>
24
+ </div>
25
+ <div className="flex gap-2">
26
+ <Link
27
+ to={`/features/groups/${group.id}/edit`}
28
+ className="text-gray-400 hover:text-blue-600 transition-colors"
29
+ title="Edit group"
30
+ >
31
+ <Settings className="w-5 h-5" />
32
+ </Link>
33
+ <button
34
+ className="text-gray-400 hover:text-red-600 transition-colors"
35
+ title="Delete group"
36
+ >
37
+ <Trash2 className="w-5 h-5" />
38
+ </button>
39
+ </div>
40
+ </div>
41
+
42
+ <div className="mt-4 pt-4 border-t border-gray-100">
43
+ <div className="flex items-center justify-between text-sm">
44
+ <span className="text-gray-500">
45
+ {group.features.length} features
46
+ </span>
47
+ <span className="text-gray-500">
48
+ Last updated {new Date(group.updatedAt).toLocaleDateString()}
49
+ </span>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ );
54
+ }
@@ -0,0 +1,81 @@
1
+ import React, { useState } from 'react';
2
+ import { Puzzle, Key, ExternalLink } from 'lucide-react';
3
+
4
+ interface PluginSettingsProps {
5
+ settings: {
6
+ wandb_api_key: string;
7
+ };
8
+ setData: (data: any) => void;
9
+ }
10
+
11
+ export function PluginSettings({ settings, setData }: PluginSettingsProps) {
12
+ const [showApiKey, setShowApiKey] = useState(false);
13
+
14
+ return (
15
+ <div className="space-y-4">
16
+ <div className="flex items-center gap-2 mb-4">
17
+ <Puzzle className="w-5 h-5 text-gray-500" />
18
+ <h3 className="text-lg font-medium text-gray-900">Plugins</h3>
19
+ </div>
20
+
21
+ <div className="space-y-6">
22
+ <div className="border border-gray-200 rounded-lg p-4">
23
+ <div className="flex items-start justify-between">
24
+ <div className="flex items-start gap-3">
25
+ <img
26
+ src="https://raw.githubusercontent.com/wandb/assets/main/wandb-dots-logo.svg"
27
+ alt="Weights & Biases"
28
+ className="w-8 h-8"
29
+ />
30
+ <div>
31
+ <h4 className="text-base font-medium text-gray-900">Weights & Biases</h4>
32
+ <p className="text-sm text-gray-500 mt-1">
33
+ Track and visualize machine learning experiments
34
+ </p>
35
+ </div>
36
+ </div>
37
+ <a
38
+ href="https://wandb.ai/settings"
39
+ target="_blank"
40
+ rel="noopener noreferrer"
41
+ className="text-blue-600 hover:text-blue-700 inline-flex items-center gap-1 text-sm"
42
+ >
43
+ Get API Key
44
+ <ExternalLink className="w-4 h-4" />
45
+ </a>
46
+ </div>
47
+
48
+ <div className="mt-4">
49
+ <label htmlFor="wandb_api_key" className="block text-sm font-medium text-gray-700">
50
+ API Key
51
+ </label>
52
+ <div className="mt-1 relative rounded-md shadow-sm">
53
+ <div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
54
+ <Key className="h-5 w-5 text-gray-400" />
55
+ </div>
56
+ <input
57
+ type={showApiKey ? "text" : "password"}
58
+ name="wandb_api_key"
59
+ id="wandb_api_key"
60
+ value={settings.wandb_api_key}
61
+ onChange={(e) => setData({ settings: { ...settings, wandb_api_key: e.target.value } })}
62
+ className="focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md"
63
+ placeholder="Enter your Weights & Biases API key"
64
+ />
65
+ <button
66
+ type="button"
67
+ onClick={() => setShowApiKey(!showApiKey)}
68
+ className="absolute inset-y-0 right-0 pr-3 flex items-center"
69
+ >
70
+ <Key className={`h-5 w-5 ${showApiKey ? 'text-gray-400' : 'text-gray-600'}`} />
71
+ </button>
72
+ </div>
73
+ <p className="mt-1 text-xs text-gray-500">
74
+ Your API key will be used to log metrics, artifacts, and experiment results
75
+ </p>
76
+ </div>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ );
81
+ }
@@ -0,0 +1,44 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const badgeVariants = cva(
7
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default:
12
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13
+ secondary:
14
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15
+ destructive:
16
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17
+ outline: "text-foreground",
18
+ information:
19
+ "border-transparent bg-blue-100 text-blue-800 hover:bg-blue-600",
20
+ important:
21
+ "border-transparent bg-red-100 text-red-800 hover:bg-red-600",
22
+ warning:
23
+ "border-transparent bg-yellow-100 text-yellow-800 hover:bg-yellow-600",
24
+ success:
25
+ "border-transparent bg-green-100 text-green-800 hover:bg-green-600",
26
+ },
27
+ },
28
+ defaultVariants: {
29
+ variant: "default",
30
+ },
31
+ }
32
+ )
33
+
34
+ export interface BadgeProps
35
+ extends React.HTMLAttributes<HTMLDivElement>,
36
+ VariantProps<typeof badgeVariants> {}
37
+
38
+ function Badge({ className, variant, ...props }: BadgeProps) {
39
+ return (
40
+ <div className={cn(badgeVariants({ variant }), className)} {...props} />
41
+ )
42
+ }
43
+
44
+ export { Badge, badgeVariants }
@@ -0,0 +1,9 @@
1
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
2
+
3
+ const Collapsible = CollapsiblePrimitive.Root
4
+
5
+ const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
6
+
7
+ const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
8
+
9
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent }
@@ -0,0 +1,46 @@
1
+ import * as React from "react"
2
+ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const ScrollArea = React.forwardRef<
7
+ React.ElementRef<typeof ScrollAreaPrimitive.Root>,
8
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
9
+ >(({ className, children, ...props }, ref) => (
10
+ <ScrollAreaPrimitive.Root
11
+ ref={ref}
12
+ className={cn("relative overflow-hidden", className)}
13
+ {...props}
14
+ >
15
+ <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
16
+ {children}
17
+ </ScrollAreaPrimitive.Viewport>
18
+ <ScrollBar />
19
+ <ScrollAreaPrimitive.Corner />
20
+ </ScrollAreaPrimitive.Root>
21
+ ))
22
+ ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
23
+
24
+ const ScrollBar = React.forwardRef<
25
+ React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
26
+ React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
27
+ >(({ className, orientation = "vertical", ...props }, ref) => (
28
+ <ScrollAreaPrimitive.ScrollAreaScrollbar
29
+ ref={ref}
30
+ orientation={orientation}
31
+ className={cn(
32
+ "flex touch-none select-none transition-colors",
33
+ orientation === "vertical" &&
34
+ "h-full w-2.5 border-l border-l-transparent p-[1px]",
35
+ orientation === "horizontal" &&
36
+ "h-2.5 flex-col border-t border-t-transparent p-[1px]",
37
+ className
38
+ )}
39
+ {...props}
40
+ >
41
+ <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
42
+ </ScrollAreaPrimitive.ScrollAreaScrollbar>
43
+ ))
44
+ ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
45
+
46
+ export { ScrollArea, ScrollBar }
@@ -0,0 +1,29 @@
1
+ import * as React from "react"
2
+ import * as SeparatorPrimitive from "@radix-ui/react-separator"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const Separator = React.forwardRef<
7
+ React.ElementRef<typeof SeparatorPrimitive.Root>,
8
+ React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
9
+ >(
10
+ (
11
+ { className, orientation = "horizontal", decorative = true, ...props },
12
+ ref
13
+ ) => (
14
+ <SeparatorPrimitive.Root
15
+ ref={ref}
16
+ decorative={decorative}
17
+ orientation={orientation}
18
+ className={cn(
19
+ "shrink-0 bg-border",
20
+ orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
21
+ className
22
+ )}
23
+ {...props}
24
+ />
25
+ )
26
+ )
27
+ Separator.displayName = SeparatorPrimitive.Root.displayName
28
+
29
+ export { Separator }
@@ -0,0 +1,40 @@
1
+ import React from 'react';
2
+ import { BrowserRouter, Routes, Route } from 'react-router-dom';
3
+ import { Navigation } from './components/Navigation';
4
+ import { ModelsPage } from './pages/ModelsPage';
5
+ import { NewModelPage } from './pages/NewModelPage';
6
+ import { EditModelPage } from './pages/EditModelPage';
7
+ import { DatasourcesPage } from './pages/DatasourcesPage';
8
+ import { NewDatasourcePage } from './pages/NewDatasourcePage';
9
+ import { EditDatasourcePage } from './pages/EditDatasourcePage';
10
+ import { DatasetsPage } from './pages/DatasetsPage';
11
+ import { NewDatasetPage } from './pages/NewDatasetPage';
12
+ import { DatasetDetailsPage } from './pages/DatasetDetailsPage';
13
+ import { SettingsPage } from './pages/SettingsPage';
14
+ import { FeaturesPage } from './pages/FeaturesPage';
15
+ import { NewFeaturePage } from './pages/NewFeaturePage';
16
+ import { EditFeaturePage } from './pages/EditFeaturePage';
17
+
18
+ export default function App() {
19
+ return (
20
+ <BrowserRouter>
21
+ <Navigation>
22
+ <Routes>
23
+ <Route path="/" element={<ModelsPage />} />
24
+ <Route path="/models/new" element={<NewModelPage />} />
25
+ <Route path="/models/:id/edit" element={<EditModelPage />} />
26
+ <Route path="/datasources" element={<DatasourcesPage />} />
27
+ <Route path="/datasources/new" element={<NewDatasourcePage />} />
28
+ <Route path="/datasources/:id/edit" element={<EditDatasourcePage />} />
29
+ <Route path="/datasets" element={<DatasetsPage />} />
30
+ <Route path="/datasets/new" element={<NewDatasetPage />} />
31
+ <Route path="/datasets/:id" element={<DatasetDetailsPage />} />
32
+ <Route path="/features" element={<FeaturesPage />} />
33
+ <Route path="/features/new" element={<NewFeaturePage />} />
34
+ <Route path="/features/:id/edit" element={<EditFeaturePage />} />
35
+ <Route path="/settings" element={<SettingsPage />} />
36
+ </Routes>
37
+ </Navigation>
38
+ </BrowserRouter>
39
+ );
40
+ }
@@ -0,0 +1,24 @@
1
+ import "../styles/application.css"
2
+ import { createInertiaApp } from '@inertiajs/react'
3
+ import { createRoot } from 'react-dom/client'
4
+ import Layout from '../layouts/Layout';
5
+
6
+ document.addEventListener('DOMContentLoaded', () => {
7
+ createInertiaApp({
8
+ resolve: name => {
9
+ const pages = import.meta.glob('../pages/**/*.tsx', { eager: true })
10
+ let page = pages[`../${name}.tsx`];
11
+ if (!page.default) {
12
+ alert(`The page ${name} could not be found, you probably forgot to export default.`);
13
+ return;
14
+ }
15
+ page.default.layout = page.default.layout || (page => <Layout children={page} />)
16
+ return page;
17
+ },
18
+ setup({ el, App, props }) {
19
+ createRoot(el).render(
20
+ <App {...props} />
21
+ )
22
+ },
23
+ })
24
+ })
@@ -0,0 +1,61 @@
1
+ import { useState, useEffect, useCallback, useRef } from 'react';
2
+ import debounce from 'lodash/debounce';
3
+ import isEqual from 'lodash/isEqual';
4
+
5
+ interface AutosaveStatus {
6
+ saving: boolean;
7
+ saved: boolean;
8
+ error: string | null;
9
+ }
10
+
11
+ export function useAutosave<T>(
12
+ data: T,
13
+ onSave: (data: T) => Promise<void>,
14
+ debounceMs: number = 1000
15
+ ) {
16
+ const [status, setStatus] = useState<AutosaveStatus>({
17
+ saving: false,
18
+ saved: false,
19
+ error: null
20
+ });
21
+
22
+ const previousSerializedData = useRef(JSON.stringify(data));
23
+
24
+ const debouncedSave = useCallback(
25
+ debounce(async (newData: T) => {
26
+ setStatus(prev => ({ ...prev, saving: true, error: null }));
27
+ try {
28
+ await onSave(newData);
29
+ previousSerializedData.current = JSON.stringify(newData); // Update reference after saving
30
+ setStatus({ saving: false, saved: true, error: null });
31
+
32
+ // Reset "saved" status after 3 seconds
33
+ setTimeout(() => {
34
+ setStatus(prev => ({ ...prev, saved: false }));
35
+ }, 4000);
36
+ } catch (err) {
37
+ setStatus({
38
+ saving: false,
39
+ saved: false,
40
+ error: err instanceof Error ? err.message : 'Failed to save changes'
41
+ });
42
+ }
43
+ }, debounceMs),
44
+ [onSave, debounceMs]
45
+ );
46
+
47
+ useEffect(() => {
48
+ // Serialize current data for deep comparison
49
+ const serializedData = JSON.stringify(data);
50
+
51
+ if (serializedData !== previousSerializedData.current) {
52
+ debouncedSave(data); // Trigger save if there's a difference
53
+ }
54
+
55
+ return () => {
56
+ debouncedSave.cancel();
57
+ };
58
+ }, [data, debouncedSave]);
59
+
60
+ return status;
61
+ }
@@ -0,0 +1,38 @@
1
+ import React, { useEffect } from "react";
2
+ import { Navigation } from "../components/Navigation";
3
+ import { AlertProvider, useAlerts } from '../components/AlertProvider';
4
+ import { usePage } from '@inertiajs/react';
5
+
6
+ interface PageProps {
7
+ flash: Array<{
8
+ type: 'success' | 'error' | 'info';
9
+ message: string;
10
+ }>;
11
+ }
12
+
13
+ function FlashMessageHandler({ children }: { children: React.ReactNode }) {
14
+ const { showAlert } = useAlerts();
15
+ const { flash } = usePage<PageProps>().props;
16
+
17
+ useEffect(() => {
18
+ if (flash) {
19
+ flash.forEach(({ type, message }) => {
20
+ showAlert(type, message);
21
+ });
22
+ }
23
+ }, [flash, showAlert]);
24
+
25
+ return <>{children}</>;
26
+ }
27
+
28
+ export default function Layout({ children }: { children: React.ReactNode }) {
29
+ return (
30
+ <AlertProvider>
31
+ <FlashMessageHandler>
32
+ <Navigation>
33
+ {children}
34
+ </Navigation>
35
+ </FlashMessageHandler>
36
+ </AlertProvider>
37
+ );
38
+ }
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs))
6
+ }