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