easy_ml 0.1.3 → 0.2.0.pre.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (239) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -26
  3. data/Rakefile +45 -0
  4. data/app/controllers/easy_ml/application_controller.rb +67 -0
  5. data/app/controllers/easy_ml/columns_controller.rb +38 -0
  6. data/app/controllers/easy_ml/datasets_controller.rb +156 -0
  7. data/app/controllers/easy_ml/datasources_controller.rb +88 -0
  8. data/app/controllers/easy_ml/deploys_controller.rb +20 -0
  9. data/app/controllers/easy_ml/models_controller.rb +151 -0
  10. data/app/controllers/easy_ml/retraining_runs_controller.rb +19 -0
  11. data/app/controllers/easy_ml/settings_controller.rb +59 -0
  12. data/app/frontend/components/AlertProvider.tsx +108 -0
  13. data/app/frontend/components/DatasetPreview.tsx +161 -0
  14. data/app/frontend/components/EmptyState.tsx +28 -0
  15. data/app/frontend/components/ModelCard.tsx +255 -0
  16. data/app/frontend/components/ModelDetails.tsx +334 -0
  17. data/app/frontend/components/ModelForm.tsx +384 -0
  18. data/app/frontend/components/Navigation.tsx +300 -0
  19. data/app/frontend/components/Pagination.tsx +72 -0
  20. data/app/frontend/components/Popover.tsx +55 -0
  21. data/app/frontend/components/PredictionStream.tsx +105 -0
  22. data/app/frontend/components/ScheduleModal.tsx +726 -0
  23. data/app/frontend/components/SearchInput.tsx +23 -0
  24. data/app/frontend/components/SearchableSelect.tsx +132 -0
  25. data/app/frontend/components/dataset/AutosaveIndicator.tsx +39 -0
  26. data/app/frontend/components/dataset/ColumnConfigModal.tsx +431 -0
  27. data/app/frontend/components/dataset/ColumnFilters.tsx +256 -0
  28. data/app/frontend/components/dataset/ColumnList.tsx +101 -0
  29. data/app/frontend/components/dataset/FeatureConfigPopover.tsx +57 -0
  30. data/app/frontend/components/dataset/FeaturePicker.tsx +205 -0
  31. data/app/frontend/components/dataset/PreprocessingConfig.tsx +704 -0
  32. data/app/frontend/components/dataset/SplitConfigurator.tsx +120 -0
  33. data/app/frontend/components/dataset/splitters/DateSplitter.tsx +58 -0
  34. data/app/frontend/components/dataset/splitters/KFoldSplitter.tsx +68 -0
  35. data/app/frontend/components/dataset/splitters/LeavePOutSplitter.tsx +29 -0
  36. data/app/frontend/components/dataset/splitters/PredefinedSplitter.tsx +146 -0
  37. data/app/frontend/components/dataset/splitters/RandomSplitter.tsx +85 -0
  38. data/app/frontend/components/dataset/splitters/StratifiedSplitter.tsx +79 -0
  39. data/app/frontend/components/dataset/splitters/constants.ts +77 -0
  40. data/app/frontend/components/dataset/splitters/types.ts +168 -0
  41. data/app/frontend/components/dataset/splitters/utils.ts +53 -0
  42. data/app/frontend/components/features/CodeEditor.tsx +46 -0
  43. data/app/frontend/components/features/DataPreview.tsx +150 -0
  44. data/app/frontend/components/features/FeatureCard.tsx +88 -0
  45. data/app/frontend/components/features/FeatureForm.tsx +235 -0
  46. data/app/frontend/components/features/FeatureGroupCard.tsx +54 -0
  47. data/app/frontend/components/settings/PluginSettings.tsx +81 -0
  48. data/app/frontend/components/ui/badge.tsx +44 -0
  49. data/app/frontend/components/ui/collapsible.tsx +9 -0
  50. data/app/frontend/components/ui/scroll-area.tsx +46 -0
  51. data/app/frontend/components/ui/separator.tsx +29 -0
  52. data/app/frontend/entrypoints/App.tsx +40 -0
  53. data/app/frontend/entrypoints/Application.tsx +24 -0
  54. data/app/frontend/hooks/useAutosave.ts +61 -0
  55. data/app/frontend/layouts/Layout.tsx +38 -0
  56. data/app/frontend/lib/utils.ts +6 -0
  57. data/app/frontend/mockData.ts +272 -0
  58. data/app/frontend/pages/DatasetDetailsPage.tsx +103 -0
  59. data/app/frontend/pages/DatasetsPage.tsx +261 -0
  60. data/app/frontend/pages/DatasourceFormPage.tsx +147 -0
  61. data/app/frontend/pages/DatasourcesPage.tsx +261 -0
  62. data/app/frontend/pages/EditModelPage.tsx +45 -0
  63. data/app/frontend/pages/EditTransformationPage.tsx +56 -0
  64. data/app/frontend/pages/ModelsPage.tsx +115 -0
  65. data/app/frontend/pages/NewDatasetPage.tsx +366 -0
  66. data/app/frontend/pages/NewModelPage.tsx +45 -0
  67. data/app/frontend/pages/NewTransformationPage.tsx +43 -0
  68. data/app/frontend/pages/SettingsPage.tsx +272 -0
  69. data/app/frontend/pages/ShowModelPage.tsx +30 -0
  70. data/app/frontend/pages/TransformationsPage.tsx +95 -0
  71. data/app/frontend/styles/application.css +100 -0
  72. data/app/frontend/types/dataset.ts +146 -0
  73. data/app/frontend/types/datasource.ts +33 -0
  74. data/app/frontend/types/preprocessing.ts +1 -0
  75. data/app/frontend/types.ts +113 -0
  76. data/app/helpers/easy_ml/application_helper.rb +10 -0
  77. data/app/jobs/easy_ml/application_job.rb +21 -0
  78. data/app/jobs/easy_ml/batch_job.rb +46 -0
  79. data/app/jobs/easy_ml/compute_feature_job.rb +19 -0
  80. data/app/jobs/easy_ml/deploy_job.rb +13 -0
  81. data/app/jobs/easy_ml/finalize_feature_job.rb +15 -0
  82. data/app/jobs/easy_ml/refresh_dataset_job.rb +32 -0
  83. data/app/jobs/easy_ml/schedule_retraining_job.rb +11 -0
  84. data/app/jobs/easy_ml/sync_datasource_job.rb +17 -0
  85. data/app/jobs/easy_ml/training_job.rb +62 -0
  86. data/app/models/easy_ml/adapters/base_adapter.rb +45 -0
  87. data/app/models/easy_ml/adapters/polars_adapter.rb +77 -0
  88. data/app/models/easy_ml/cleaner.rb +82 -0
  89. data/app/models/easy_ml/column.rb +124 -0
  90. data/app/models/easy_ml/column_history.rb +30 -0
  91. data/app/models/easy_ml/column_list.rb +122 -0
  92. data/app/models/easy_ml/concerns/configurable.rb +61 -0
  93. data/app/models/easy_ml/concerns/versionable.rb +19 -0
  94. data/app/models/easy_ml/dataset.rb +767 -0
  95. data/app/models/easy_ml/dataset_history.rb +56 -0
  96. data/app/models/easy_ml/datasource.rb +182 -0
  97. data/app/models/easy_ml/datasource_history.rb +24 -0
  98. data/app/models/easy_ml/datasources/base_datasource.rb +54 -0
  99. data/app/models/easy_ml/datasources/file_datasource.rb +58 -0
  100. data/app/models/easy_ml/datasources/polars_datasource.rb +89 -0
  101. data/app/models/easy_ml/datasources/s3_datasource.rb +97 -0
  102. data/app/models/easy_ml/deploy.rb +114 -0
  103. data/app/models/easy_ml/event.rb +79 -0
  104. data/app/models/easy_ml/feature.rb +437 -0
  105. data/app/models/easy_ml/feature_history.rb +38 -0
  106. data/app/models/easy_ml/model.rb +575 -41
  107. data/app/models/easy_ml/model_file.rb +133 -0
  108. data/app/models/easy_ml/model_file_history.rb +24 -0
  109. data/app/models/easy_ml/model_history.rb +51 -0
  110. data/app/models/easy_ml/models/base_model.rb +58 -0
  111. data/app/models/easy_ml/models/hyperparameters/base.rb +99 -0
  112. data/app/models/easy_ml/models/hyperparameters/xgboost/dart.rb +82 -0
  113. data/app/models/easy_ml/models/hyperparameters/xgboost/gblinear.rb +82 -0
  114. data/app/models/easy_ml/models/hyperparameters/xgboost/gbtree.rb +97 -0
  115. data/app/models/easy_ml/models/hyperparameters/xgboost.rb +71 -0
  116. data/app/models/easy_ml/models/xgboost/evals_callback.rb +138 -0
  117. data/app/models/easy_ml/models/xgboost/progress_callback.rb +39 -0
  118. data/app/models/easy_ml/models/xgboost.rb +544 -4
  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
+ }