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.
- checksums.yaml +4 -4
- data/README.md +234 -26
- data/Rakefile +45 -0
- data/app/controllers/easy_ml/application_controller.rb +67 -0
- data/app/controllers/easy_ml/columns_controller.rb +38 -0
- data/app/controllers/easy_ml/datasets_controller.rb +156 -0
- data/app/controllers/easy_ml/datasources_controller.rb +88 -0
- data/app/controllers/easy_ml/deploys_controller.rb +20 -0
- data/app/controllers/easy_ml/models_controller.rb +151 -0
- data/app/controllers/easy_ml/retraining_runs_controller.rb +19 -0
- data/app/controllers/easy_ml/settings_controller.rb +59 -0
- data/app/frontend/components/AlertProvider.tsx +108 -0
- data/app/frontend/components/DatasetPreview.tsx +161 -0
- data/app/frontend/components/EmptyState.tsx +28 -0
- data/app/frontend/components/ModelCard.tsx +255 -0
- data/app/frontend/components/ModelDetails.tsx +334 -0
- data/app/frontend/components/ModelForm.tsx +384 -0
- data/app/frontend/components/Navigation.tsx +300 -0
- data/app/frontend/components/Pagination.tsx +72 -0
- data/app/frontend/components/Popover.tsx +55 -0
- data/app/frontend/components/PredictionStream.tsx +105 -0
- data/app/frontend/components/ScheduleModal.tsx +726 -0
- data/app/frontend/components/SearchInput.tsx +23 -0
- data/app/frontend/components/SearchableSelect.tsx +132 -0
- data/app/frontend/components/dataset/AutosaveIndicator.tsx +39 -0
- data/app/frontend/components/dataset/ColumnConfigModal.tsx +431 -0
- data/app/frontend/components/dataset/ColumnFilters.tsx +256 -0
- data/app/frontend/components/dataset/ColumnList.tsx +101 -0
- data/app/frontend/components/dataset/FeatureConfigPopover.tsx +57 -0
- data/app/frontend/components/dataset/FeaturePicker.tsx +205 -0
- data/app/frontend/components/dataset/PreprocessingConfig.tsx +704 -0
- data/app/frontend/components/dataset/SplitConfigurator.tsx +120 -0
- data/app/frontend/components/dataset/splitters/DateSplitter.tsx +58 -0
- data/app/frontend/components/dataset/splitters/KFoldSplitter.tsx +68 -0
- data/app/frontend/components/dataset/splitters/LeavePOutSplitter.tsx +29 -0
- data/app/frontend/components/dataset/splitters/PredefinedSplitter.tsx +146 -0
- data/app/frontend/components/dataset/splitters/RandomSplitter.tsx +85 -0
- data/app/frontend/components/dataset/splitters/StratifiedSplitter.tsx +79 -0
- data/app/frontend/components/dataset/splitters/constants.ts +77 -0
- data/app/frontend/components/dataset/splitters/types.ts +168 -0
- data/app/frontend/components/dataset/splitters/utils.ts +53 -0
- data/app/frontend/components/features/CodeEditor.tsx +46 -0
- data/app/frontend/components/features/DataPreview.tsx +150 -0
- data/app/frontend/components/features/FeatureCard.tsx +88 -0
- data/app/frontend/components/features/FeatureForm.tsx +235 -0
- data/app/frontend/components/features/FeatureGroupCard.tsx +54 -0
- data/app/frontend/components/settings/PluginSettings.tsx +81 -0
- data/app/frontend/components/ui/badge.tsx +44 -0
- data/app/frontend/components/ui/collapsible.tsx +9 -0
- data/app/frontend/components/ui/scroll-area.tsx +46 -0
- data/app/frontend/components/ui/separator.tsx +29 -0
- data/app/frontend/entrypoints/App.tsx +40 -0
- data/app/frontend/entrypoints/Application.tsx +24 -0
- data/app/frontend/hooks/useAutosave.ts +61 -0
- data/app/frontend/layouts/Layout.tsx +38 -0
- data/app/frontend/lib/utils.ts +6 -0
- data/app/frontend/mockData.ts +272 -0
- data/app/frontend/pages/DatasetDetailsPage.tsx +103 -0
- data/app/frontend/pages/DatasetsPage.tsx +261 -0
- data/app/frontend/pages/DatasourceFormPage.tsx +147 -0
- data/app/frontend/pages/DatasourcesPage.tsx +261 -0
- data/app/frontend/pages/EditModelPage.tsx +45 -0
- data/app/frontend/pages/EditTransformationPage.tsx +56 -0
- data/app/frontend/pages/ModelsPage.tsx +115 -0
- data/app/frontend/pages/NewDatasetPage.tsx +366 -0
- data/app/frontend/pages/NewModelPage.tsx +45 -0
- data/app/frontend/pages/NewTransformationPage.tsx +43 -0
- data/app/frontend/pages/SettingsPage.tsx +272 -0
- data/app/frontend/pages/ShowModelPage.tsx +30 -0
- data/app/frontend/pages/TransformationsPage.tsx +95 -0
- data/app/frontend/styles/application.css +100 -0
- data/app/frontend/types/dataset.ts +146 -0
- data/app/frontend/types/datasource.ts +33 -0
- data/app/frontend/types/preprocessing.ts +1 -0
- data/app/frontend/types.ts +113 -0
- data/app/helpers/easy_ml/application_helper.rb +10 -0
- data/app/jobs/easy_ml/application_job.rb +21 -0
- data/app/jobs/easy_ml/batch_job.rb +46 -0
- data/app/jobs/easy_ml/compute_feature_job.rb +19 -0
- data/app/jobs/easy_ml/deploy_job.rb +13 -0
- data/app/jobs/easy_ml/finalize_feature_job.rb +15 -0
- data/app/jobs/easy_ml/refresh_dataset_job.rb +32 -0
- data/app/jobs/easy_ml/schedule_retraining_job.rb +11 -0
- data/app/jobs/easy_ml/sync_datasource_job.rb +17 -0
- data/app/jobs/easy_ml/training_job.rb +62 -0
- data/app/models/easy_ml/adapters/base_adapter.rb +45 -0
- data/app/models/easy_ml/adapters/polars_adapter.rb +77 -0
- data/app/models/easy_ml/cleaner.rb +82 -0
- data/app/models/easy_ml/column.rb +124 -0
- data/app/models/easy_ml/column_history.rb +30 -0
- data/app/models/easy_ml/column_list.rb +122 -0
- data/app/models/easy_ml/concerns/configurable.rb +61 -0
- data/app/models/easy_ml/concerns/versionable.rb +19 -0
- data/app/models/easy_ml/dataset.rb +767 -0
- data/app/models/easy_ml/dataset_history.rb +56 -0
- data/app/models/easy_ml/datasource.rb +182 -0
- data/app/models/easy_ml/datasource_history.rb +24 -0
- data/app/models/easy_ml/datasources/base_datasource.rb +54 -0
- data/app/models/easy_ml/datasources/file_datasource.rb +58 -0
- data/app/models/easy_ml/datasources/polars_datasource.rb +89 -0
- data/app/models/easy_ml/datasources/s3_datasource.rb +97 -0
- data/app/models/easy_ml/deploy.rb +114 -0
- data/app/models/easy_ml/event.rb +79 -0
- data/app/models/easy_ml/feature.rb +437 -0
- data/app/models/easy_ml/feature_history.rb +38 -0
- data/app/models/easy_ml/model.rb +575 -41
- data/app/models/easy_ml/model_file.rb +133 -0
- data/app/models/easy_ml/model_file_history.rb +24 -0
- data/app/models/easy_ml/model_history.rb +51 -0
- data/app/models/easy_ml/models/base_model.rb +58 -0
- data/app/models/easy_ml/models/hyperparameters/base.rb +99 -0
- data/app/models/easy_ml/models/hyperparameters/xgboost/dart.rb +82 -0
- data/app/models/easy_ml/models/hyperparameters/xgboost/gblinear.rb +82 -0
- data/app/models/easy_ml/models/hyperparameters/xgboost/gbtree.rb +97 -0
- data/app/models/easy_ml/models/hyperparameters/xgboost.rb +71 -0
- data/app/models/easy_ml/models/xgboost/evals_callback.rb +138 -0
- data/app/models/easy_ml/models/xgboost/progress_callback.rb +39 -0
- data/app/models/easy_ml/models/xgboost.rb +544 -5
- data/app/models/easy_ml/prediction.rb +44 -0
- data/app/models/easy_ml/retraining_job.rb +278 -0
- data/app/models/easy_ml/retraining_run.rb +184 -0
- data/app/models/easy_ml/settings.rb +37 -0
- data/app/models/easy_ml/splitter.rb +90 -0
- data/app/models/easy_ml/splitters/base_splitter.rb +28 -0
- data/app/models/easy_ml/splitters/date_splitter.rb +91 -0
- data/app/models/easy_ml/splitters/predefined_splitter.rb +74 -0
- data/app/models/easy_ml/splitters/random_splitter.rb +82 -0
- data/app/models/easy_ml/tuner_job.rb +56 -0
- data/app/models/easy_ml/tuner_run.rb +31 -0
- data/app/models/splitter_history.rb +6 -0
- data/app/serializers/easy_ml/column_serializer.rb +27 -0
- data/app/serializers/easy_ml/dataset_serializer.rb +73 -0
- data/app/serializers/easy_ml/datasource_serializer.rb +64 -0
- data/app/serializers/easy_ml/feature_serializer.rb +27 -0
- data/app/serializers/easy_ml/model_serializer.rb +90 -0
- data/app/serializers/easy_ml/retraining_job_serializer.rb +22 -0
- data/app/serializers/easy_ml/retraining_run_serializer.rb +39 -0
- data/app/serializers/easy_ml/settings_serializer.rb +9 -0
- data/app/views/layouts/easy_ml/application.html.erb +15 -0
- data/config/initializers/resque.rb +3 -0
- data/config/resque-pool.yml +6 -0
- data/config/routes.rb +39 -0
- data/config/spring.rb +1 -0
- data/config/vite.json +15 -0
- data/lib/easy_ml/configuration.rb +64 -0
- data/lib/easy_ml/core/evaluators/base_evaluator.rb +53 -0
- data/lib/easy_ml/core/evaluators/classification_evaluators.rb +126 -0
- data/lib/easy_ml/core/evaluators/regression_evaluators.rb +66 -0
- data/lib/easy_ml/core/model_evaluator.rb +161 -89
- data/lib/easy_ml/core/tuner/adapters/base_adapter.rb +28 -18
- data/lib/easy_ml/core/tuner/adapters/xgboost_adapter.rb +4 -25
- data/lib/easy_ml/core/tuner.rb +123 -62
- data/lib/easy_ml/core.rb +0 -3
- data/lib/easy_ml/core_ext/hash.rb +24 -0
- data/lib/easy_ml/core_ext/pathname.rb +11 -5
- data/lib/easy_ml/data/date_converter.rb +90 -0
- data/lib/easy_ml/data/filter_extensions.rb +31 -0
- data/lib/easy_ml/data/polars_column.rb +126 -0
- data/lib/easy_ml/data/polars_reader.rb +297 -0
- data/lib/easy_ml/data/preprocessor.rb +280 -142
- data/lib/easy_ml/data/simple_imputer.rb +255 -0
- data/lib/easy_ml/data/splits/file_split.rb +252 -0
- data/lib/easy_ml/data/splits/in_memory_split.rb +54 -0
- data/lib/easy_ml/data/splits/split.rb +95 -0
- data/lib/easy_ml/data/splits.rb +9 -0
- data/lib/easy_ml/data/statistics_learner.rb +93 -0
- data/lib/easy_ml/data/synced_directory.rb +341 -0
- data/lib/easy_ml/data.rb +6 -2
- data/lib/easy_ml/engine.rb +105 -6
- data/lib/easy_ml/feature_store.rb +227 -0
- data/lib/easy_ml/features.rb +61 -0
- data/lib/easy_ml/initializers/inflections.rb +17 -3
- data/lib/easy_ml/logging.rb +2 -2
- data/lib/easy_ml/predict.rb +74 -0
- data/lib/easy_ml/railtie/generators/migration/migration_generator.rb +192 -36
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_column_histories.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_columns.rb.tt +25 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_dataset_histories.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasets.rb.tt +31 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasource_histories.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasources.rb.tt +16 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_deploys.rb.tt +24 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_events.rb.tt +20 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_feature_histories.rb.tt +14 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_features.rb.tt +32 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_file_histories.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_files.rb.tt +17 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_histories.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_models.rb.tt +20 -9
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_predictions.rb.tt +17 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_retraining_jobs.rb.tt +77 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_settings.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_splitter_histories.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_splitters.rb.tt +15 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_tuner_jobs.rb.tt +40 -0
- data/lib/easy_ml/support/est.rb +5 -1
- data/lib/easy_ml/support/file_rotate.rb +79 -15
- data/lib/easy_ml/support/file_support.rb +9 -0
- data/lib/easy_ml/support/local_file.rb +24 -0
- data/lib/easy_ml/support/lockable.rb +62 -0
- data/lib/easy_ml/support/synced_file.rb +103 -0
- data/lib/easy_ml/support/utc.rb +5 -1
- data/lib/easy_ml/support.rb +6 -3
- data/lib/easy_ml/version.rb +4 -1
- data/lib/easy_ml.rb +7 -2
- metadata +355 -72
- data/app/models/easy_ml/models.rb +0 -5
- data/lib/easy_ml/core/model.rb +0 -30
- data/lib/easy_ml/core/model_core.rb +0 -181
- data/lib/easy_ml/core/models/hyperparameters/base.rb +0 -34
- data/lib/easy_ml/core/models/hyperparameters/xgboost.rb +0 -19
- data/lib/easy_ml/core/models/xgboost.rb +0 -10
- data/lib/easy_ml/core/models/xgboost_core.rb +0 -220
- data/lib/easy_ml/core/models.rb +0 -10
- data/lib/easy_ml/core/uploaders/model_uploader.rb +0 -24
- data/lib/easy_ml/core/uploaders.rb +0 -7
- data/lib/easy_ml/data/dataloader.rb +0 -6
- data/lib/easy_ml/data/dataset/data/preprocessor/statistics.json +0 -31
- data/lib/easy_ml/data/dataset/data/sample_info.json +0 -1
- data/lib/easy_ml/data/dataset/dataset/files/sample_info.json +0 -1
- data/lib/easy_ml/data/dataset/splits/file_split.rb +0 -140
- data/lib/easy_ml/data/dataset/splits/in_memory_split.rb +0 -49
- data/lib/easy_ml/data/dataset/splits/split.rb +0 -98
- data/lib/easy_ml/data/dataset/splits.rb +0 -11
- data/lib/easy_ml/data/dataset/splitters/date_splitter.rb +0 -43
- data/lib/easy_ml/data/dataset/splitters.rb +0 -9
- data/lib/easy_ml/data/dataset.rb +0 -430
- data/lib/easy_ml/data/datasource/datasource_factory.rb +0 -60
- data/lib/easy_ml/data/datasource/file_datasource.rb +0 -40
- data/lib/easy_ml/data/datasource/merged_datasource.rb +0 -64
- data/lib/easy_ml/data/datasource/polars_datasource.rb +0 -41
- data/lib/easy_ml/data/datasource/s3_datasource.rb +0 -89
- data/lib/easy_ml/data/datasource.rb +0 -33
- data/lib/easy_ml/data/preprocessor/preprocessor.rb +0 -205
- data/lib/easy_ml/data/preprocessor/simple_imputer.rb +0 -402
- data/lib/easy_ml/deployment.rb +0 -5
- data/lib/easy_ml/support/synced_directory.rb +0 -134
- data/lib/easy_ml/transforms.rb +0 -29
- /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
|
+
}
|