easy_ml 0.1.4 → 0.2.0.pre.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (239) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -26
  3. data/Rakefile +45 -0
  4. data/app/controllers/easy_ml/application_controller.rb +67 -0
  5. data/app/controllers/easy_ml/columns_controller.rb +38 -0
  6. data/app/controllers/easy_ml/datasets_controller.rb +156 -0
  7. data/app/controllers/easy_ml/datasources_controller.rb +88 -0
  8. data/app/controllers/easy_ml/deploys_controller.rb +20 -0
  9. data/app/controllers/easy_ml/models_controller.rb +151 -0
  10. data/app/controllers/easy_ml/retraining_runs_controller.rb +19 -0
  11. data/app/controllers/easy_ml/settings_controller.rb +59 -0
  12. data/app/frontend/components/AlertProvider.tsx +108 -0
  13. data/app/frontend/components/DatasetPreview.tsx +161 -0
  14. data/app/frontend/components/EmptyState.tsx +28 -0
  15. data/app/frontend/components/ModelCard.tsx +255 -0
  16. data/app/frontend/components/ModelDetails.tsx +334 -0
  17. data/app/frontend/components/ModelForm.tsx +384 -0
  18. data/app/frontend/components/Navigation.tsx +300 -0
  19. data/app/frontend/components/Pagination.tsx +72 -0
  20. data/app/frontend/components/Popover.tsx +55 -0
  21. data/app/frontend/components/PredictionStream.tsx +105 -0
  22. data/app/frontend/components/ScheduleModal.tsx +726 -0
  23. data/app/frontend/components/SearchInput.tsx +23 -0
  24. data/app/frontend/components/SearchableSelect.tsx +132 -0
  25. data/app/frontend/components/dataset/AutosaveIndicator.tsx +39 -0
  26. data/app/frontend/components/dataset/ColumnConfigModal.tsx +431 -0
  27. data/app/frontend/components/dataset/ColumnFilters.tsx +256 -0
  28. data/app/frontend/components/dataset/ColumnList.tsx +101 -0
  29. data/app/frontend/components/dataset/FeatureConfigPopover.tsx +57 -0
  30. data/app/frontend/components/dataset/FeaturePicker.tsx +205 -0
  31. data/app/frontend/components/dataset/PreprocessingConfig.tsx +704 -0
  32. data/app/frontend/components/dataset/SplitConfigurator.tsx +120 -0
  33. data/app/frontend/components/dataset/splitters/DateSplitter.tsx +58 -0
  34. data/app/frontend/components/dataset/splitters/KFoldSplitter.tsx +68 -0
  35. data/app/frontend/components/dataset/splitters/LeavePOutSplitter.tsx +29 -0
  36. data/app/frontend/components/dataset/splitters/PredefinedSplitter.tsx +146 -0
  37. data/app/frontend/components/dataset/splitters/RandomSplitter.tsx +85 -0
  38. data/app/frontend/components/dataset/splitters/StratifiedSplitter.tsx +79 -0
  39. data/app/frontend/components/dataset/splitters/constants.ts +77 -0
  40. data/app/frontend/components/dataset/splitters/types.ts +168 -0
  41. data/app/frontend/components/dataset/splitters/utils.ts +53 -0
  42. data/app/frontend/components/features/CodeEditor.tsx +46 -0
  43. data/app/frontend/components/features/DataPreview.tsx +150 -0
  44. data/app/frontend/components/features/FeatureCard.tsx +88 -0
  45. data/app/frontend/components/features/FeatureForm.tsx +235 -0
  46. data/app/frontend/components/features/FeatureGroupCard.tsx +54 -0
  47. data/app/frontend/components/settings/PluginSettings.tsx +81 -0
  48. data/app/frontend/components/ui/badge.tsx +44 -0
  49. data/app/frontend/components/ui/collapsible.tsx +9 -0
  50. data/app/frontend/components/ui/scroll-area.tsx +46 -0
  51. data/app/frontend/components/ui/separator.tsx +29 -0
  52. data/app/frontend/entrypoints/App.tsx +40 -0
  53. data/app/frontend/entrypoints/Application.tsx +24 -0
  54. data/app/frontend/hooks/useAutosave.ts +61 -0
  55. data/app/frontend/layouts/Layout.tsx +38 -0
  56. data/app/frontend/lib/utils.ts +6 -0
  57. data/app/frontend/mockData.ts +272 -0
  58. data/app/frontend/pages/DatasetDetailsPage.tsx +103 -0
  59. data/app/frontend/pages/DatasetsPage.tsx +261 -0
  60. data/app/frontend/pages/DatasourceFormPage.tsx +147 -0
  61. data/app/frontend/pages/DatasourcesPage.tsx +261 -0
  62. data/app/frontend/pages/EditModelPage.tsx +45 -0
  63. data/app/frontend/pages/EditTransformationPage.tsx +56 -0
  64. data/app/frontend/pages/ModelsPage.tsx +115 -0
  65. data/app/frontend/pages/NewDatasetPage.tsx +366 -0
  66. data/app/frontend/pages/NewModelPage.tsx +45 -0
  67. data/app/frontend/pages/NewTransformationPage.tsx +43 -0
  68. data/app/frontend/pages/SettingsPage.tsx +272 -0
  69. data/app/frontend/pages/ShowModelPage.tsx +30 -0
  70. data/app/frontend/pages/TransformationsPage.tsx +95 -0
  71. data/app/frontend/styles/application.css +100 -0
  72. data/app/frontend/types/dataset.ts +146 -0
  73. data/app/frontend/types/datasource.ts +33 -0
  74. data/app/frontend/types/preprocessing.ts +1 -0
  75. data/app/frontend/types.ts +113 -0
  76. data/app/helpers/easy_ml/application_helper.rb +10 -0
  77. data/app/jobs/easy_ml/application_job.rb +21 -0
  78. data/app/jobs/easy_ml/batch_job.rb +46 -0
  79. data/app/jobs/easy_ml/compute_feature_job.rb +19 -0
  80. data/app/jobs/easy_ml/deploy_job.rb +13 -0
  81. data/app/jobs/easy_ml/finalize_feature_job.rb +15 -0
  82. data/app/jobs/easy_ml/refresh_dataset_job.rb +32 -0
  83. data/app/jobs/easy_ml/schedule_retraining_job.rb +11 -0
  84. data/app/jobs/easy_ml/sync_datasource_job.rb +17 -0
  85. data/app/jobs/easy_ml/training_job.rb +62 -0
  86. data/app/models/easy_ml/adapters/base_adapter.rb +45 -0
  87. data/app/models/easy_ml/adapters/polars_adapter.rb +77 -0
  88. data/app/models/easy_ml/cleaner.rb +82 -0
  89. data/app/models/easy_ml/column.rb +124 -0
  90. data/app/models/easy_ml/column_history.rb +30 -0
  91. data/app/models/easy_ml/column_list.rb +122 -0
  92. data/app/models/easy_ml/concerns/configurable.rb +61 -0
  93. data/app/models/easy_ml/concerns/versionable.rb +19 -0
  94. data/app/models/easy_ml/dataset.rb +767 -0
  95. data/app/models/easy_ml/dataset_history.rb +56 -0
  96. data/app/models/easy_ml/datasource.rb +182 -0
  97. data/app/models/easy_ml/datasource_history.rb +24 -0
  98. data/app/models/easy_ml/datasources/base_datasource.rb +54 -0
  99. data/app/models/easy_ml/datasources/file_datasource.rb +58 -0
  100. data/app/models/easy_ml/datasources/polars_datasource.rb +89 -0
  101. data/app/models/easy_ml/datasources/s3_datasource.rb +97 -0
  102. data/app/models/easy_ml/deploy.rb +114 -0
  103. data/app/models/easy_ml/event.rb +79 -0
  104. data/app/models/easy_ml/feature.rb +437 -0
  105. data/app/models/easy_ml/feature_history.rb +38 -0
  106. data/app/models/easy_ml/model.rb +575 -41
  107. data/app/models/easy_ml/model_file.rb +133 -0
  108. data/app/models/easy_ml/model_file_history.rb +24 -0
  109. data/app/models/easy_ml/model_history.rb +51 -0
  110. data/app/models/easy_ml/models/base_model.rb +58 -0
  111. data/app/models/easy_ml/models/hyperparameters/base.rb +99 -0
  112. data/app/models/easy_ml/models/hyperparameters/xgboost/dart.rb +82 -0
  113. data/app/models/easy_ml/models/hyperparameters/xgboost/gblinear.rb +82 -0
  114. data/app/models/easy_ml/models/hyperparameters/xgboost/gbtree.rb +97 -0
  115. data/app/models/easy_ml/models/hyperparameters/xgboost.rb +71 -0
  116. data/app/models/easy_ml/models/xgboost/evals_callback.rb +138 -0
  117. data/app/models/easy_ml/models/xgboost/progress_callback.rb +39 -0
  118. data/app/models/easy_ml/models/xgboost.rb +544 -5
  119. data/app/models/easy_ml/prediction.rb +44 -0
  120. data/app/models/easy_ml/retraining_job.rb +278 -0
  121. data/app/models/easy_ml/retraining_run.rb +184 -0
  122. data/app/models/easy_ml/settings.rb +37 -0
  123. data/app/models/easy_ml/splitter.rb +90 -0
  124. data/app/models/easy_ml/splitters/base_splitter.rb +28 -0
  125. data/app/models/easy_ml/splitters/date_splitter.rb +91 -0
  126. data/app/models/easy_ml/splitters/predefined_splitter.rb +74 -0
  127. data/app/models/easy_ml/splitters/random_splitter.rb +82 -0
  128. data/app/models/easy_ml/tuner_job.rb +56 -0
  129. data/app/models/easy_ml/tuner_run.rb +31 -0
  130. data/app/models/splitter_history.rb +6 -0
  131. data/app/serializers/easy_ml/column_serializer.rb +27 -0
  132. data/app/serializers/easy_ml/dataset_serializer.rb +73 -0
  133. data/app/serializers/easy_ml/datasource_serializer.rb +64 -0
  134. data/app/serializers/easy_ml/feature_serializer.rb +27 -0
  135. data/app/serializers/easy_ml/model_serializer.rb +90 -0
  136. data/app/serializers/easy_ml/retraining_job_serializer.rb +22 -0
  137. data/app/serializers/easy_ml/retraining_run_serializer.rb +39 -0
  138. data/app/serializers/easy_ml/settings_serializer.rb +9 -0
  139. data/app/views/layouts/easy_ml/application.html.erb +15 -0
  140. data/config/initializers/resque.rb +3 -0
  141. data/config/resque-pool.yml +6 -0
  142. data/config/routes.rb +39 -0
  143. data/config/spring.rb +1 -0
  144. data/config/vite.json +15 -0
  145. data/lib/easy_ml/configuration.rb +64 -0
  146. data/lib/easy_ml/core/evaluators/base_evaluator.rb +53 -0
  147. data/lib/easy_ml/core/evaluators/classification_evaluators.rb +126 -0
  148. data/lib/easy_ml/core/evaluators/regression_evaluators.rb +66 -0
  149. data/lib/easy_ml/core/model_evaluator.rb +161 -89
  150. data/lib/easy_ml/core/tuner/adapters/base_adapter.rb +28 -18
  151. data/lib/easy_ml/core/tuner/adapters/xgboost_adapter.rb +4 -25
  152. data/lib/easy_ml/core/tuner.rb +123 -62
  153. data/lib/easy_ml/core.rb +0 -3
  154. data/lib/easy_ml/core_ext/hash.rb +24 -0
  155. data/lib/easy_ml/core_ext/pathname.rb +11 -5
  156. data/lib/easy_ml/data/date_converter.rb +90 -0
  157. data/lib/easy_ml/data/filter_extensions.rb +31 -0
  158. data/lib/easy_ml/data/polars_column.rb +126 -0
  159. data/lib/easy_ml/data/polars_reader.rb +297 -0
  160. data/lib/easy_ml/data/preprocessor.rb +280 -142
  161. data/lib/easy_ml/data/simple_imputer.rb +255 -0
  162. data/lib/easy_ml/data/splits/file_split.rb +252 -0
  163. data/lib/easy_ml/data/splits/in_memory_split.rb +54 -0
  164. data/lib/easy_ml/data/splits/split.rb +95 -0
  165. data/lib/easy_ml/data/splits.rb +9 -0
  166. data/lib/easy_ml/data/statistics_learner.rb +93 -0
  167. data/lib/easy_ml/data/synced_directory.rb +341 -0
  168. data/lib/easy_ml/data.rb +6 -2
  169. data/lib/easy_ml/engine.rb +105 -6
  170. data/lib/easy_ml/feature_store.rb +227 -0
  171. data/lib/easy_ml/features.rb +61 -0
  172. data/lib/easy_ml/initializers/inflections.rb +17 -3
  173. data/lib/easy_ml/logging.rb +2 -2
  174. data/lib/easy_ml/predict.rb +74 -0
  175. data/lib/easy_ml/railtie/generators/migration/migration_generator.rb +192 -36
  176. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_column_histories.rb.tt +9 -0
  177. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_columns.rb.tt +25 -0
  178. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_dataset_histories.rb.tt +9 -0
  179. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasets.rb.tt +31 -0
  180. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasource_histories.rb.tt +9 -0
  181. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasources.rb.tt +16 -0
  182. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_deploys.rb.tt +24 -0
  183. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_events.rb.tt +20 -0
  184. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_feature_histories.rb.tt +14 -0
  185. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_features.rb.tt +32 -0
  186. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_file_histories.rb.tt +9 -0
  187. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_files.rb.tt +17 -0
  188. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_histories.rb.tt +9 -0
  189. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_models.rb.tt +20 -9
  190. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_predictions.rb.tt +17 -0
  191. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_retraining_jobs.rb.tt +77 -0
  192. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_settings.rb.tt +9 -0
  193. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_splitter_histories.rb.tt +9 -0
  194. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_splitters.rb.tt +15 -0
  195. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_tuner_jobs.rb.tt +40 -0
  196. data/lib/easy_ml/support/est.rb +5 -1
  197. data/lib/easy_ml/support/file_rotate.rb +79 -15
  198. data/lib/easy_ml/support/file_support.rb +9 -0
  199. data/lib/easy_ml/support/local_file.rb +24 -0
  200. data/lib/easy_ml/support/lockable.rb +62 -0
  201. data/lib/easy_ml/support/synced_file.rb +103 -0
  202. data/lib/easy_ml/support/utc.rb +5 -1
  203. data/lib/easy_ml/support.rb +6 -3
  204. data/lib/easy_ml/version.rb +4 -1
  205. data/lib/easy_ml.rb +7 -2
  206. metadata +355 -72
  207. data/app/models/easy_ml/models.rb +0 -5
  208. data/lib/easy_ml/core/model.rb +0 -30
  209. data/lib/easy_ml/core/model_core.rb +0 -181
  210. data/lib/easy_ml/core/models/hyperparameters/base.rb +0 -34
  211. data/lib/easy_ml/core/models/hyperparameters/xgboost.rb +0 -19
  212. data/lib/easy_ml/core/models/xgboost.rb +0 -10
  213. data/lib/easy_ml/core/models/xgboost_core.rb +0 -220
  214. data/lib/easy_ml/core/models.rb +0 -10
  215. data/lib/easy_ml/core/uploaders/model_uploader.rb +0 -24
  216. data/lib/easy_ml/core/uploaders.rb +0 -7
  217. data/lib/easy_ml/data/dataloader.rb +0 -6
  218. data/lib/easy_ml/data/dataset/data/preprocessor/statistics.json +0 -31
  219. data/lib/easy_ml/data/dataset/data/sample_info.json +0 -1
  220. data/lib/easy_ml/data/dataset/dataset/files/sample_info.json +0 -1
  221. data/lib/easy_ml/data/dataset/splits/file_split.rb +0 -140
  222. data/lib/easy_ml/data/dataset/splits/in_memory_split.rb +0 -49
  223. data/lib/easy_ml/data/dataset/splits/split.rb +0 -98
  224. data/lib/easy_ml/data/dataset/splits.rb +0 -11
  225. data/lib/easy_ml/data/dataset/splitters/date_splitter.rb +0 -43
  226. data/lib/easy_ml/data/dataset/splitters.rb +0 -9
  227. data/lib/easy_ml/data/dataset.rb +0 -430
  228. data/lib/easy_ml/data/datasource/datasource_factory.rb +0 -60
  229. data/lib/easy_ml/data/datasource/file_datasource.rb +0 -40
  230. data/lib/easy_ml/data/datasource/merged_datasource.rb +0 -64
  231. data/lib/easy_ml/data/datasource/polars_datasource.rb +0 -41
  232. data/lib/easy_ml/data/datasource/s3_datasource.rb +0 -89
  233. data/lib/easy_ml/data/datasource.rb +0 -33
  234. data/lib/easy_ml/data/preprocessor/preprocessor.rb +0 -205
  235. data/lib/easy_ml/data/preprocessor/simple_imputer.rb +0 -402
  236. data/lib/easy_ml/deployment.rb +0 -5
  237. data/lib/easy_ml/support/synced_directory.rb +0 -134
  238. data/lib/easy_ml/transforms.rb +0 -29
  239. /data/{lib/easy_ml/core → app/models/easy_ml}/models/hyperparameters.rb +0 -0
@@ -0,0 +1,255 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Activity, Calendar, Database, Settings, ExternalLink, Play, LineChart,
3
+ Trash2, Loader2, XCircle, CheckCircle2, AlertCircle, ChevronDown, ChevronUp } from 'lucide-react';
4
+ import { Link, router } from "@inertiajs/react";
5
+ import { cn } from '@/lib/utils';
6
+ import type { Model, RetrainingJob, RetrainingRun } from '../types';
7
+
8
+ interface ModelCardProps {
9
+ initialModel: Model;
10
+ onViewDetails: (modelId: number) => void;
11
+ handleDelete: (modelId: number) => void;
12
+ rootPath: string;
13
+ }
14
+
15
+ export function ModelCard({ initialModel, onViewDetails, handleDelete, rootPath }: ModelCardProps) {
16
+ const [model, setModel] = useState(initialModel);
17
+ const [showError, setShowError] = useState(false);
18
+
19
+ useEffect(() => {
20
+ let pollInterval: number | undefined;
21
+
22
+ if (model.is_training) {
23
+ pollInterval = window.setInterval(async () => {
24
+ const response = await fetch(`${rootPath}/models/${model.id}`, {
25
+ headers: {
26
+ 'Accept': 'application/json'
27
+ }
28
+ });
29
+ const data = await response.json();
30
+ setModel(data.model);
31
+ }, 2000);
32
+ }
33
+
34
+ return () => {
35
+ if (pollInterval) {
36
+ window.clearInterval(pollInterval);
37
+ }
38
+ };
39
+ }, [model.is_training, model.id, rootPath]);
40
+
41
+ const handleTrain = async () => {
42
+ try {
43
+ setModel({
44
+ ...model,
45
+ is_training: true
46
+ })
47
+ await router.post(`${rootPath}/models/${model.id}/train`, {}, {
48
+ preserveScroll: true,
49
+ preserveState: true
50
+ });
51
+ } catch (error) {
52
+ console.error('Failed to start training:', error);
53
+ }
54
+ };
55
+
56
+ const dataset = model.dataset;
57
+ const job = model.retraining_job;
58
+ const lastRun = model.last_run;
59
+
60
+ const getStatusIcon = () => {
61
+ if (model.is_training) {
62
+ return <Loader2 className="w-4 h-4 animate-spin text-yellow-500" />;
63
+ }
64
+ if (!lastRun) {
65
+ return null;
66
+ }
67
+ if (lastRun.status === 'failed') {
68
+ return <XCircle className="w-4 h-4 text-red-500" />;
69
+ }
70
+ if (lastRun.status === 'success') {
71
+ return <CheckCircle2 className="w-4 h-4 text-green-500" />;
72
+ }
73
+ return null;
74
+ };
75
+
76
+ const getStatusText = () => {
77
+ if (model.is_training) return 'Training in progress...';
78
+ if (!lastRun) return 'Never trained';
79
+ if (lastRun.status === 'failed') return 'Last run failed';
80
+ if (lastRun.status === 'success') {
81
+ return lastRun.deployable ? 'Last run succeeded' : 'Last run completed (below threshold)';
82
+ }
83
+ return 'Unknown status';
84
+ };
85
+
86
+ const getStatusClass = () => {
87
+ if (model.is_training) return 'text-yellow-700';
88
+ if (!lastRun) return 'text-gray-500';
89
+ if (lastRun.status === 'failed') return 'text-red-700';
90
+ if (lastRun.status === 'success') {
91
+ return lastRun.deployable ? 'text-green-700' : 'text-orange-700';
92
+ }
93
+ return 'text-gray-700';
94
+ };
95
+
96
+ return (
97
+ <div className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
98
+ <div className="flex flex-col gap-2">
99
+ <div className="flex items-center gap-2">
100
+ <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
101
+ ${model.deployment_status === 'inference'
102
+ ? 'bg-blue-100 text-blue-800'
103
+ : 'bg-gray-100 text-gray-800'}`}
104
+ >
105
+ {model.deployment_status}
106
+ </span>
107
+ {model.is_training && (
108
+ <span className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
109
+ <Loader2 className="w-3 h-3 animate-spin" />
110
+ training
111
+ </span>
112
+ )}
113
+ </div>
114
+
115
+ <div className="flex justify-between items-start">
116
+ <h3 className="text-lg font-semibold text-gray-900">{model.name}</h3>
117
+ <div className="flex gap-2">
118
+ <button
119
+ onClick={handleTrain}
120
+ disabled={model.is_training}
121
+ className={`text-gray-400 hover:text-green-600 transition-colors ${
122
+ model.is_training ? 'opacity-50 cursor-not-allowed' : ''
123
+ }`}
124
+ title="Train model"
125
+ >
126
+ <Play className="w-5 h-5" />
127
+ </button>
128
+ {
129
+ model.metrics_url && (
130
+ <a
131
+ href={model.metrics_url}
132
+ target="_blank"
133
+ rel="noopener noreferrer"
134
+ className="text-gray-400 hover:text-purple-600 transition-colors"
135
+ title="View metrics"
136
+ >
137
+ <LineChart className="w-5 h-5" />
138
+ </a>
139
+ )
140
+ }
141
+ <Link
142
+ href={`${rootPath}/models/${model.id}`}
143
+ className="text-gray-400 hover:text-gray-600"
144
+ title="View details"
145
+ >
146
+ <ExternalLink className="w-5 h-5" />
147
+ </Link>
148
+ <Link
149
+ href={`${rootPath}/models/${model.id}/edit`}
150
+ className="text-gray-400 hover:text-gray-600"
151
+ title="Edit model"
152
+ >
153
+ <Settings className="w-5 h-5" />
154
+ </Link>
155
+ <button
156
+ onClick={() => handleDelete(model.id)}
157
+ className="text-gray-400 hover:text-gray-600"
158
+ title="Delete model"
159
+ >
160
+ <Trash2 className="w-5 h-5" />
161
+ </button>
162
+ </div>
163
+ </div>
164
+
165
+ <p className="text-sm text-gray-500">
166
+ <span className="font-semibold">Model Type: </span>
167
+ {model.formatted_model_type}
168
+ </p>
169
+ <p className="text-sm text-gray-500">
170
+ <span className="font-semibold">Version: </span>
171
+ {model.version}
172
+ </p>
173
+ </div>
174
+
175
+ <div className="grid grid-cols-2 gap-4 mt-4">
176
+ <div className="flex items-center gap-2">
177
+ <Database className="w-4 h-4 text-gray-400" />
178
+ {dataset ? (
179
+ <Link
180
+ href={`${rootPath}/datasets/${dataset.id}`}
181
+ className="text-sm text-blue-600 hover:text-blue-800"
182
+ >
183
+ {dataset.name}
184
+ </Link>
185
+ ) : (
186
+ <span className="text-sm text-gray-600">Dataset not found</span>
187
+ )}
188
+ </div>
189
+ <div className="flex items-center gap-2">
190
+ <Calendar className="w-4 h-4 text-gray-400" />
191
+ <span className="text-sm text-gray-600">
192
+ {job?.active ? `Retrains ${model.formatted_frequency}` : 'Retrains manually'}
193
+ </span>
194
+ </div>
195
+ <div className="flex items-center gap-2">
196
+ <Activity className="w-4 h-4 text-gray-400" />
197
+ <span className="text-sm text-gray-600">
198
+ {model.last_run_at
199
+ ? `Last run: ${new Date(model.last_run_at || '').toLocaleDateString()}`
200
+ : 'Never run'}
201
+ </span>
202
+ </div>
203
+ <div className="flex items-center gap-2">
204
+ {getStatusIcon()}
205
+ <span className={cn("text-sm", getStatusClass())}>
206
+ {getStatusText()}
207
+ </span>
208
+ </div>
209
+ </div>
210
+
211
+ {lastRun?.metrics && (
212
+ <div className="mt-4 pt-4 border-t border-gray-100">
213
+ <div className="flex flex-wrap gap-2">
214
+ {Object.entries(lastRun.metrics as Record<string, number>).map(([key, value]) => (
215
+ <div
216
+ key={key}
217
+ className={`px-2 py-1 rounded-md text-xs font-medium ${
218
+ lastRun.deployable
219
+ ? 'bg-green-100 text-green-800'
220
+ : 'bg-red-100 text-red-800'
221
+ }`}
222
+ >
223
+ {key}: {value.toFixed(4)}
224
+ </div>
225
+ ))}
226
+ </div>
227
+ </div>
228
+ )}
229
+
230
+ {!model.is_training && lastRun?.status === 'failed' && lastRun.stacktrace && (
231
+ <div className="mt-4 pt-4 border-t border-gray-100">
232
+ <button
233
+ onClick={() => setShowError(!showError)}
234
+ className="flex items-center gap-2 text-sm text-red-600 hover:text-red-700"
235
+ >
236
+ <AlertCircle className="w-4 h-4" />
237
+ <span>View Error Details</span>
238
+ {showError ? (
239
+ <ChevronUp className="w-4 h-4" />
240
+ ) : (
241
+ <ChevronDown className="w-4 h-4" />
242
+ )}
243
+ </button>
244
+ {showError && (
245
+ <div className="mt-2 p-3 bg-red-50 rounded-md">
246
+ <pre className="text-xs text-red-700 whitespace-pre-wrap font-mono">
247
+ {lastRun.stacktrace}
248
+ </pre>
249
+ </div>
250
+ )}
251
+ </div>
252
+ )}
253
+ </div>
254
+ );
255
+ }
@@ -0,0 +1,334 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { Calendar, Clock, BarChart2, Database, ChevronLeft, ChevronRight, Rocket, Loader2, LineChart } from 'lucide-react';
3
+ import type { Model, RetrainingJob, RetrainingRun } from '../types';
4
+ import { router } from "@inertiajs/react";
5
+
6
+ interface ModelDetailsProps {
7
+ model: Model;
8
+ onBack: () => void;
9
+ }
10
+
11
+ interface PaginatedRuns {
12
+ runs: RetrainingRun[];
13
+ total_count: number;
14
+ limit: number;
15
+ offset: number;
16
+ }
17
+
18
+ const ITEMS_PER_PAGE = 3;
19
+
20
+ export function ModelDetails({ model, onBack, rootPath }: ModelDetailsProps) {
21
+ const [activeTab, setActiveTab] = useState<'overview' | 'dataset'>('overview');
22
+ const [runs, setRuns] = useState<RetrainingRun[]>(model.retraining_runs?.runs || []);
23
+ const [loading, setLoading] = useState(false);
24
+ const [pagination, setPagination] = useState({
25
+ offset: 0,
26
+ limit: 20,
27
+ total_count: model.retraining_runs?.total_count || 0
28
+ });
29
+ const [currentPage, setCurrentPage] = useState(1);
30
+ const dataset = model.dataset;
31
+ const job = model.retraining_job;
32
+ const hasMoreRuns = pagination.offset + pagination.limit < pagination.total_count;
33
+
34
+ useEffect(() => {
35
+ let pollInterval: number | undefined;
36
+
37
+ const deployingRun = runs.find(run => run.is_deploying);
38
+ if (deployingRun) {
39
+ pollInterval = window.setInterval(async () => {
40
+ router.get(window.location.href, {
41
+ preserveScroll: true,
42
+ preserveState: true,
43
+ only: ['runs']
44
+ })
45
+ }, 2000);
46
+ }
47
+
48
+ return () => {
49
+ if (pollInterval) {
50
+ window.clearInterval(pollInterval);
51
+ }
52
+ };
53
+ }, [runs]);
54
+
55
+ const handleDeploy = async (run: RetrainingRun) => {
56
+ if (run.is_deploying) return;
57
+
58
+ const updatedRuns = runs.map(r =>
59
+ r.id === run.id ? { ...r, is_deploying: true } : r
60
+ );
61
+ setRuns(updatedRuns);
62
+
63
+ try {
64
+ await router.post(`${rootPath}/models/${model.id}/deploys`, {
65
+ retraining_run_id: run.id
66
+ }, {
67
+ preserveScroll: true,
68
+ preserveState: true
69
+ });
70
+ } catch (error) {
71
+ console.error('Failed to deploy model:', error);
72
+ // Reset deploying state on error
73
+ const resetRuns = runs.map(r =>
74
+ r.id === run.id ? { ...r, is_deploying: false } : r
75
+ );
76
+ setRuns(resetRuns);
77
+ }
78
+ };
79
+
80
+ useEffect(() => {
81
+ const totalPages = Math.ceil(runs.length / ITEMS_PER_PAGE);
82
+ const remainingPages = totalPages - currentPage;
83
+
84
+ if (remainingPages <= 2 && hasMoreRuns) {
85
+ loadMoreRuns();
86
+ }
87
+ }, [currentPage, runs, hasMoreRuns]);
88
+
89
+ const totalPages = Math.ceil(runs.length / ITEMS_PER_PAGE);
90
+ const paginatedRuns = runs.slice(
91
+ (currentPage - 1) * ITEMS_PER_PAGE,
92
+ currentPage * ITEMS_PER_PAGE
93
+ );
94
+
95
+ const updateCurrentPage = (newPage) => {
96
+ setCurrentPage(newPage);
97
+ if (totalPages - newPage < 2 && hasMoreRuns) {
98
+ loadMoreRuns();
99
+ }
100
+ }
101
+
102
+ const isCurrentlyDeployed = (run: RetrainingRun) => {
103
+ return run.status === 'deployed';
104
+ };
105
+
106
+ return (
107
+ <div className="space-y-6">
108
+ <div className="flex items-center justify-between">
109
+ <div className="flex space-x-4 ml-auto">
110
+ <button
111
+ onClick={() => setActiveTab('overview')}
112
+ className={`px-4 py-2 text-sm font-medium rounded-md ${
113
+ activeTab === 'overview'
114
+ ? 'bg-blue-100 text-blue-700'
115
+ : 'text-gray-500 hover:text-gray-700'
116
+ }`}
117
+ >
118
+ Overview
119
+ </button>
120
+ </div>
121
+ </div>
122
+
123
+ <div className="bg-white rounded-lg shadow-lg p-6">
124
+ <div className="mb-8">
125
+ <div className="flex justify-between items-start">
126
+ <div>
127
+ <h2 className="text-2xl font-bold text-gray-900">{model.name}</h2>
128
+ <p className="text-gray-600 mt-1">
129
+ <span className="font-medium">Version:</span> {model.version} • <span className="font-medium">Type:</span> {model.formatted_model_type}
130
+ </p>
131
+ </div>
132
+ <span
133
+ className={`px-3 py-1 rounded-full text-sm font-medium ${
134
+ model.deployment_status === 'inference'
135
+ ? 'bg-blue-100 text-blue-800'
136
+ : 'bg-gray-100 text-gray-800'
137
+ }`}
138
+ >
139
+ {model.deployment_status}
140
+ </span>
141
+ </div>
142
+
143
+ {job && (
144
+ <div className="mt-6 bg-gray-50 rounded-lg p-4">
145
+ <h3 className="text-lg font-semibold mb-4">Training Schedule</h3>
146
+ <div className="grid grid-cols-2 gap-4">
147
+ <div className="flex items-center gap-2">
148
+ <Calendar className="w-5 h-5 text-gray-400" />
149
+ <span>{job.active ? `Runs ${job.formatted_frequency}` : "None (Triggered Manually)"}</span>
150
+ </div>
151
+ {
152
+ job.active && (
153
+ <div className="flex items-center gap-2">
154
+ <Clock className="w-5 h-5 text-gray-400" />
155
+ <span>at {job.at.hour}:00</span>
156
+ </div>
157
+ )
158
+ }
159
+ </div>
160
+ </div>
161
+ )}
162
+ </div>
163
+
164
+ {activeTab === 'overview' ? (
165
+ <div className="space-y-6">
166
+ <div className="flex justify-between items-center">
167
+ <h3 className="text-lg font-semibold">Retraining Runs</h3>
168
+ <div className="flex items-center gap-2">
169
+ <button
170
+ onClick={() => updateCurrentPage(p => Math.max(1, p - 1))}
171
+ disabled={currentPage === 1}
172
+ className="p-1 rounded-md hover:bg-gray-100 disabled:opacity-50"
173
+ >
174
+ <ChevronLeft className="w-5 h-5" />
175
+ </button>
176
+ <span className="text-sm text-gray-600">
177
+ Page {currentPage} of {totalPages}
178
+ </span>
179
+ <button
180
+ onClick={() => updateCurrentPage(p => Math.min(totalPages, p + 1))}
181
+ disabled={currentPage === totalPages}
182
+ className="p-1 rounded-md hover:bg-gray-100 disabled:opacity-50"
183
+ >
184
+ <ChevronRight className="w-5 h-5" />
185
+ </button>
186
+ </div>
187
+ </div>
188
+
189
+ <div className="space-y-4">
190
+ {paginatedRuns.map((run, index) => (
191
+ <div key={index} className="border border-gray-200 rounded-lg p-4 hover:border-gray-300 transition-colors">
192
+ <div className="flex justify-between items-start mb-3">
193
+ <div>
194
+ <div className="flex items-center gap-2 mt-1">
195
+ {
196
+ !isCurrentlyDeployed(run) && (
197
+ <span
198
+ className={`px-2 py-1 rounded-md text-sm font-medium ${
199
+ run.status === 'success'
200
+ ? 'bg-green-100 text-green-800'
201
+ : run.status === 'running' ? 'bg-blue-100 text-blue-800' : 'bg-red-100 text-red-800'
202
+ }`}
203
+ >
204
+ {run.status}
205
+ </span>
206
+ )
207
+ }
208
+ {isCurrentlyDeployed(run) && (
209
+ <span className="px-2 py-1 bg-purple-100 text-purple-800 rounded-md text-sm font-medium flex items-center gap-1">
210
+ <Rocket className="w-4 h-4" />
211
+ deployed
212
+ </span>
213
+ )}
214
+ {run.metrics_url && (
215
+ <a
216
+ href={run.metrics_url}
217
+ target="_blank"
218
+ rel="noopener noreferrer"
219
+ className="inline-flex items-center gap-1 px-2 py-1 bg-gray-100 text-gray-700 rounded-md text-sm font-medium hover:bg-gray-200 transition-colors"
220
+ title="View run metrics"
221
+ >
222
+ <LineChart className="w-4 h-4" />
223
+ metrics
224
+ </a>
225
+ )}
226
+ </div>
227
+ </div>
228
+ <div className="flex items-center gap-1">
229
+ <BarChart2 className="w-4 h-4 text-gray-400" />
230
+ <span className="text-sm text-gray-600">
231
+ {new Date(run.started_at).toLocaleString()}
232
+ </span>
233
+ {run.status === 'success' && run.deployable && (
234
+ <div className="flex gap-2 items-center">
235
+ {isCurrentlyDeployed(run) ? (
236
+ <span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
237
+ deployed
238
+ </span>
239
+ ) : (
240
+ <button
241
+ onClick={() => handleDeploy(run)}
242
+ disabled={run.is_deploying}
243
+ className={`ml-4 inline-flex items-center gap-2 px-3 py-1 rounded-md text-sm font-medium
244
+ ${run.is_deploying
245
+ ? 'bg-yellow-100 text-yellow-800'
246
+ : 'bg-blue-600 text-white hover:bg-blue-500'
247
+ }`}
248
+ >
249
+ {run.is_deploying ? (
250
+ <>
251
+ <Loader2 className="w-3 h-3 animate-spin" />
252
+ Deploying...
253
+ </>
254
+ ) : (
255
+ <>
256
+ <Rocket className="w-3 h-3" />
257
+ Deploy
258
+ </>
259
+ )}
260
+ </button>
261
+ )}
262
+ </div>
263
+ )}
264
+ </div>
265
+ </div>
266
+
267
+ {run && run.metrics && (
268
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
269
+ {Object.entries(
270
+ run.metrics as Record<string, number>
271
+ ).map(([key, value]) => (
272
+ <div key={key} className="bg-gray-50 rounded-md p-3">
273
+ <div className="text-sm font-medium text-gray-500">
274
+ {key}
275
+ </div>
276
+ <div className="mt-1 flex items-center gap-2">
277
+ <span className="text-lg font-semibold">
278
+ {value.toFixed(4)}
279
+ </span>
280
+ </div>
281
+ </div>
282
+ ))}
283
+ </div>
284
+ )}
285
+ </div>
286
+ ))}
287
+ </div>
288
+ </div>
289
+ ) : (
290
+ dataset && (
291
+ <div>
292
+ <div className="flex items-center gap-2 mb-4">
293
+ <Database className="w-5 h-5 text-blue-600" />
294
+ <h3 className="text-lg font-semibold">{dataset.name}</h3>
295
+ </div>
296
+ <div className="grid grid-cols-2 gap-6">
297
+ <div>
298
+ <h4 className="text-sm font-medium text-gray-700 mb-2">Columns</h4>
299
+ <div className="bg-gray-50 rounded-lg p-4">
300
+ <div className="space-y-2">
301
+ {dataset.columns.map(column => (
302
+ <div key={column.name} className="flex justify-between items-center">
303
+ <span className="text-sm text-gray-900">{column.name}</span>
304
+ <span className="text-xs text-gray-500">{column.type}</span>
305
+ </div>
306
+ ))}
307
+ </div>
308
+ </div>
309
+ </div>
310
+ <div>
311
+ <h4 className="text-sm font-medium text-gray-700 mb-2">Statistics</h4>
312
+ <div className="bg-gray-50 rounded-lg p-4">
313
+ <div className="space-y-2">
314
+ <div className="flex justify-between items-center">
315
+ <span className="text-sm text-gray-900">Total Rows</span>
316
+ <span className="text-sm font-medium">{dataset.num_rows.toLocaleString()}</span>
317
+ </div>
318
+ <div className="flex justify-between items-center">
319
+ <span className="text-sm text-gray-900">Last Updated</span>
320
+ <span className="text-sm font-medium">
321
+ {new Date(dataset.updated_at).toLocaleDateString()}
322
+ </span>
323
+ </div>
324
+ </div>
325
+ </div>
326
+ </div>
327
+ </div>
328
+ </div>
329
+ )
330
+ )}
331
+ </div>
332
+ </div>
333
+ );
334
+ }