easy_ml 0.2.0.pre.rc71 → 0.2.0.pre.rc75
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/app/controllers/easy_ml/datasets_controller.rb +33 -0
- data/app/controllers/easy_ml/datasources_controller.rb +7 -0
- data/app/controllers/easy_ml/models_controller.rb +46 -0
- data/app/frontend/components/DatasetCard.tsx +212 -0
- data/app/frontend/components/ModelCard.tsx +114 -29
- data/app/frontend/components/StackTrace.tsx +13 -0
- data/app/frontend/components/dataset/FeatureConfigPopover.tsx +10 -7
- data/app/frontend/components/datasets/UploadDatasetButton.tsx +51 -0
- data/app/frontend/components/models/DownloadModelModal.tsx +90 -0
- data/app/frontend/components/models/UploadModelModal.tsx +212 -0
- data/app/frontend/components/models/index.ts +2 -0
- data/app/frontend/pages/DatasetsPage.tsx +36 -130
- data/app/frontend/pages/DatasourcesPage.tsx +22 -2
- data/app/frontend/pages/ModelsPage.tsx +37 -11
- data/app/frontend/types/dataset.ts +1 -2
- data/app/frontend/types.ts +1 -1
- data/app/jobs/easy_ml/reaper.rb +55 -0
- data/app/jobs/easy_ml/training_job.rb +1 -1
- data/app/models/easy_ml/column/imputers/base.rb +4 -0
- data/app/models/easy_ml/column/imputers/clip.rb +5 -3
- data/app/models/easy_ml/column/imputers/imputer.rb +11 -13
- data/app/models/easy_ml/column/imputers/mean.rb +7 -3
- data/app/models/easy_ml/column/imputers/null_imputer.rb +3 -0
- data/app/models/easy_ml/column/imputers/ordinal_encoder.rb +5 -1
- data/app/models/easy_ml/column/imputers.rb +3 -1
- data/app/models/easy_ml/column/lineage/base.rb +5 -1
- data/app/models/easy_ml/column/lineage/computed_by_feature.rb +1 -1
- data/app/models/easy_ml/column/lineage/preprocessed.rb +1 -1
- data/app/models/easy_ml/column/lineage/raw_dataset.rb +1 -1
- data/app/models/easy_ml/column/selector.rb +4 -0
- data/app/models/easy_ml/column.rb +79 -63
- data/app/models/easy_ml/column_history.rb +28 -28
- data/app/models/easy_ml/column_list/imputer.rb +23 -0
- data/app/models/easy_ml/column_list.rb +39 -26
- data/app/models/easy_ml/dataset/learner/base.rb +34 -0
- data/app/models/easy_ml/dataset/learner/eager/boolean.rb +10 -0
- data/app/models/easy_ml/dataset/learner/eager/categorical.rb +51 -0
- data/app/models/easy_ml/dataset/learner/eager/query.rb +37 -0
- data/app/models/easy_ml/dataset/learner/eager.rb +43 -0
- data/app/models/easy_ml/dataset/learner/lazy/boolean.rb +13 -0
- data/app/models/easy_ml/dataset/learner/lazy/categorical.rb +10 -0
- data/app/models/easy_ml/dataset/learner/lazy/datetime.rb +19 -0
- data/app/models/easy_ml/dataset/learner/lazy/null.rb +17 -0
- data/app/models/easy_ml/dataset/learner/lazy/numeric.rb +19 -0
- data/app/models/easy_ml/dataset/learner/lazy/query.rb +69 -0
- data/app/models/easy_ml/dataset/learner/lazy/string.rb +19 -0
- data/app/models/easy_ml/dataset/learner/lazy.rb +51 -0
- data/app/models/easy_ml/dataset/learner/query.rb +25 -0
- data/app/models/easy_ml/dataset/learner.rb +100 -0
- data/app/models/easy_ml/dataset.rb +150 -36
- data/app/models/easy_ml/dataset_history.rb +1 -0
- data/app/models/easy_ml/datasource.rb +9 -0
- data/app/models/easy_ml/event.rb +5 -7
- data/app/models/easy_ml/export/column.rb +27 -0
- data/app/models/easy_ml/export/dataset.rb +37 -0
- data/app/models/easy_ml/export/datasource.rb +12 -0
- data/app/models/easy_ml/export/feature.rb +24 -0
- data/app/models/easy_ml/export/model.rb +40 -0
- data/app/models/easy_ml/export/retraining_job.rb +20 -0
- data/app/models/easy_ml/export/splitter.rb +14 -0
- data/app/models/easy_ml/feature.rb +21 -0
- data/app/models/easy_ml/import/column.rb +35 -0
- data/app/models/easy_ml/import/dataset.rb +148 -0
- data/app/models/easy_ml/import/feature.rb +36 -0
- data/app/models/easy_ml/import/model.rb +136 -0
- data/app/models/easy_ml/import/retraining_job.rb +29 -0
- data/app/models/easy_ml/import/splitter.rb +34 -0
- data/app/models/easy_ml/lineage.rb +44 -0
- data/app/models/easy_ml/model.rb +101 -37
- data/app/models/easy_ml/model_file.rb +6 -0
- data/app/models/easy_ml/models/xgboost/evals_callback.rb +7 -7
- data/app/models/easy_ml/models/xgboost.rb +33 -9
- data/app/models/easy_ml/retraining_job.rb +8 -1
- data/app/models/easy_ml/retraining_run.rb +7 -5
- data/app/models/easy_ml/splitter.rb +8 -0
- data/app/models/lineage_history.rb +6 -0
- data/app/serializers/easy_ml/column_serializer.rb +7 -1
- data/app/serializers/easy_ml/dataset_serializer.rb +2 -1
- data/app/serializers/easy_ml/lineage_serializer.rb +9 -0
- data/config/routes.rb +14 -1
- data/lib/easy_ml/core/tuner/adapters/base_adapter.rb +3 -3
- data/lib/easy_ml/core/tuner.rb +13 -12
- data/lib/easy_ml/data/polars_column.rb +149 -100
- data/lib/easy_ml/data/polars_reader.rb +8 -5
- data/lib/easy_ml/data/polars_schema.rb +56 -0
- data/lib/easy_ml/data/splits/file_split.rb +20 -2
- data/lib/easy_ml/data/splits/split.rb +10 -1
- data/lib/easy_ml/data.rb +1 -0
- data/lib/easy_ml/deep_compact.rb +19 -0
- data/lib/easy_ml/engine.rb +1 -0
- data/lib/easy_ml/feature_store.rb +2 -6
- data/lib/easy_ml/railtie/generators/migration/migration_generator.rb +6 -0
- data/lib/easy_ml/railtie/templates/migration/add_extra_metadata_to_columns.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/add_raw_schema_to_datasets.rb.tt +9 -0
- data/lib/easy_ml/railtie/templates/migration/add_unique_constraint_to_easy_ml_model_names.rb.tt +8 -0
- data/lib/easy_ml/railtie/templates/migration/create_easy_ml_lineages.rb.tt +24 -0
- data/lib/easy_ml/railtie/templates/migration/remove_evaluator_from_retraining_jobs.rb.tt +7 -0
- data/lib/easy_ml/railtie/templates/migration/update_preprocessing_steps_to_jsonb.rb.tt +18 -0
- data/lib/easy_ml/timing.rb +34 -0
- data/lib/easy_ml/version.rb +1 -1
- data/lib/easy_ml.rb +2 -0
- data/public/easy_ml/assets/.vite/manifest.json +2 -2
- data/public/easy_ml/assets/assets/Application-Q7L6ioxr.css +1 -0
- data/public/easy_ml/assets/assets/entrypoints/Application.tsx-Rrzo4ecT.js +522 -0
- data/public/easy_ml/assets/assets/entrypoints/Application.tsx-Rrzo4ecT.js.map +1 -0
- metadata +53 -12
- data/app/models/easy_ml/column/learners/base.rb +0 -103
- data/app/models/easy_ml/column/learners/boolean.rb +0 -11
- data/app/models/easy_ml/column/learners/categorical.rb +0 -51
- data/app/models/easy_ml/column/learners/datetime.rb +0 -19
- data/app/models/easy_ml/column/learners/null.rb +0 -22
- data/app/models/easy_ml/column/learners/numeric.rb +0 -33
- data/app/models/easy_ml/column/learners/string.rb +0 -15
- data/public/easy_ml/assets/assets/Application-BbFobaXt.css +0 -1
- data/public/easy_ml/assets/assets/entrypoints/Application.tsx-CibZcrBc.js +0 -489
- data/public/easy_ml/assets/assets/entrypoints/Application.tsx-CibZcrBc.js.map +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9c924f9d9ccee9dcad409f1dd077b55f29214aea419c4edbf3f7f7d16b8d71e6
|
4
|
+
data.tar.gz: c82401ebf3763354d77ff536d12d81bc9642ce9145bce961f9c274b535b16eea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2df9bbd0fd82067fa4ec5976ba5f5292e2c052ff1eeaad0286b44cbfe4797cf02102fba788a01963b413a33a51297c5501f623310461fda18c2f541a61c2172c
|
7
|
+
data.tar.gz: 0f131bc4143b76d1b13641716854d1bf7ed0c06751cf31b69ebd2a8f1ec96abb1417d38d72039fd8266619c3183250ef1d0b39dc4b9381fa15843a910271387e
|
@@ -129,6 +129,39 @@ module EasyML
|
|
129
129
|
redirect_to easy_ml_datasets_path, notice: "Dataset refresh has been initiated."
|
130
130
|
end
|
131
131
|
|
132
|
+
def abort
|
133
|
+
dataset = Dataset.find(params[:id])
|
134
|
+
dataset.abort!
|
135
|
+
|
136
|
+
redirect_to easy_ml_datasets_path, notice: "Dataset processing has been aborted."
|
137
|
+
end
|
138
|
+
|
139
|
+
def download
|
140
|
+
dataset = Dataset.find(params[:id])
|
141
|
+
config = dataset.to_config
|
142
|
+
|
143
|
+
send_data JSON.pretty_generate(config),
|
144
|
+
filename: "#{dataset.name.parameterize}-config.json",
|
145
|
+
type: "application/json",
|
146
|
+
disposition: "attachment"
|
147
|
+
end
|
148
|
+
|
149
|
+
def upload
|
150
|
+
dataset = Dataset.find(params[:id]) if params[:id].present?
|
151
|
+
|
152
|
+
begin
|
153
|
+
config = JSON.parse(params[:config].read)
|
154
|
+
action = dataset.present? ? :update : :create
|
155
|
+
EasyML::Dataset.from_config(config, action: action, dataset: dataset)
|
156
|
+
|
157
|
+
flash[:notice] = "Dataset configuration was successfully uploaded."
|
158
|
+
redirect_to easy_ml_datasets_path
|
159
|
+
rescue JSON::ParserError, StandardError => e
|
160
|
+
flash[:error] = "Failed to upload configuration: #{e.message}"
|
161
|
+
redirect_to easy_ml_datasets_path
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
132
165
|
private
|
133
166
|
|
134
167
|
def preprocessing_params
|
@@ -80,6 +80,13 @@ module EasyML
|
|
80
80
|
redirect_to easy_ml_datasources_path, error: "Datasource not found..."
|
81
81
|
end
|
82
82
|
|
83
|
+
def abort
|
84
|
+
datasource = Datasource.find(params[:id])
|
85
|
+
datasource.abort!
|
86
|
+
|
87
|
+
redirect_to easy_ml_datasources_path, notice: "Datasource sync has been aborted."
|
88
|
+
end
|
89
|
+
|
83
90
|
private
|
84
91
|
|
85
92
|
def datasource_params
|
@@ -23,6 +23,7 @@ module EasyML
|
|
23
23
|
|
24
24
|
render inertia: "pages/ModelsPage", props: {
|
25
25
|
models: models.map { |model| model_to_json(model) },
|
26
|
+
datasets: EasyML::Dataset.all.map { |dataset| dataset.slice(:id, :name, :num_rows) },
|
26
27
|
}
|
27
28
|
end
|
28
29
|
|
@@ -118,6 +119,51 @@ module EasyML
|
|
118
119
|
redirect_to easy_ml_models_path
|
119
120
|
end
|
120
121
|
|
122
|
+
def abort
|
123
|
+
model = Model.find(params[:id])
|
124
|
+
model.abort!
|
125
|
+
|
126
|
+
flash[:notice] = "Model training aborted!"
|
127
|
+
redirect_to easy_ml_models_path
|
128
|
+
end
|
129
|
+
|
130
|
+
def download
|
131
|
+
model = Model.find(params[:id])
|
132
|
+
config = model.to_config(include_dataset: params[:include_dataset] == "true")
|
133
|
+
|
134
|
+
send_data JSON.pretty_generate(config),
|
135
|
+
filename: "#{model.name.parameterize}-config.json",
|
136
|
+
type: "application/json",
|
137
|
+
disposition: "attachment"
|
138
|
+
end
|
139
|
+
|
140
|
+
def upload
|
141
|
+
model = Model.find(params[:id]) if params[:id].present?
|
142
|
+
|
143
|
+
begin
|
144
|
+
config = JSON.parse(params[:config].read)
|
145
|
+
dataset = if params[:dataset_id].present?
|
146
|
+
EasyML::Dataset.find(params[:dataset_id])
|
147
|
+
else
|
148
|
+
model.dataset
|
149
|
+
end
|
150
|
+
|
151
|
+
action = model.present? ? :update : :create
|
152
|
+
|
153
|
+
EasyML::Model.from_config(config,
|
154
|
+
action: action,
|
155
|
+
model: model,
|
156
|
+
include_dataset: params[:include_dataset] == "true",
|
157
|
+
dataset: dataset)
|
158
|
+
|
159
|
+
flash[:notice] = "Model configuration was successfully uploaded."
|
160
|
+
redirect_to easy_ml_models_path
|
161
|
+
rescue JSON::ParserError, StandardError => e
|
162
|
+
flash[:error] = "Failed to upload configuration: #{e.message}"
|
163
|
+
redirect_to easy_ml_models_path
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
121
167
|
private
|
122
168
|
|
123
169
|
def includes_list
|
@@ -0,0 +1,212 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Link } from '@inertiajs/react';
|
3
|
+
import { Database, Trash2, ExternalLink, Loader2, AlertCircle, ChevronDown, ChevronUp, RefreshCw, XCircle, Download, Upload } from 'lucide-react';
|
4
|
+
import { Dataset, DatasetWorkflowStatus, Column } from "@types/dataset";
|
5
|
+
import { StackTrace } from './StackTrace';
|
6
|
+
|
7
|
+
interface Props {
|
8
|
+
dataset: Dataset;
|
9
|
+
rootPath: string;
|
10
|
+
onDelete: (id: number) => void;
|
11
|
+
onRefresh: (id: number) => void;
|
12
|
+
onAbort: (id: number) => void;
|
13
|
+
isErrorExpanded: boolean;
|
14
|
+
onToggleError: (id: number) => void;
|
15
|
+
}
|
16
|
+
|
17
|
+
const STATUS_STYLES: Record<DatasetWorkflowStatus, { bg: string; text: string; icon: React.ReactNode }> = {
|
18
|
+
analyzing: {
|
19
|
+
bg: 'bg-blue-100',
|
20
|
+
text: 'text-blue-800',
|
21
|
+
icon: <Loader2 className="w-4 h-4 animate-spin" />
|
22
|
+
},
|
23
|
+
ready: {
|
24
|
+
bg: 'bg-green-100',
|
25
|
+
text: 'text-green-800',
|
26
|
+
icon: null
|
27
|
+
},
|
28
|
+
failed: {
|
29
|
+
bg: 'bg-red-100',
|
30
|
+
text: 'text-red-800',
|
31
|
+
icon: <AlertCircle className="w-4 h-4" />
|
32
|
+
},
|
33
|
+
};
|
34
|
+
|
35
|
+
export function DatasetCard({
|
36
|
+
dataset,
|
37
|
+
rootPath,
|
38
|
+
onDelete,
|
39
|
+
onRefresh,
|
40
|
+
onAbort,
|
41
|
+
isErrorExpanded,
|
42
|
+
onToggleError
|
43
|
+
}: Props) {
|
44
|
+
// Create a hidden file input for handling uploads
|
45
|
+
const fileInputRef = React.useRef<HTMLInputElement>(null);
|
46
|
+
|
47
|
+
const handleDownload = () => {
|
48
|
+
window.location.href = `${rootPath}/datasets/${dataset.id}/download`;
|
49
|
+
};
|
50
|
+
|
51
|
+
const handleUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
52
|
+
const file = event.target.files?.[0];
|
53
|
+
if (!file) return;
|
54
|
+
|
55
|
+
const formData = new FormData();
|
56
|
+
formData.append('config', file);
|
57
|
+
|
58
|
+
fetch(`${rootPath}/datasets/${dataset.id}/upload`, {
|
59
|
+
method: 'POST',
|
60
|
+
body: formData,
|
61
|
+
credentials: 'same-origin',
|
62
|
+
headers: {
|
63
|
+
'X-CSRF-Token': document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')?.content || '',
|
64
|
+
},
|
65
|
+
}).then(response => {
|
66
|
+
if (response.ok) {
|
67
|
+
window.location.reload();
|
68
|
+
} else {
|
69
|
+
console.error('Upload failed');
|
70
|
+
}
|
71
|
+
});
|
72
|
+
};
|
73
|
+
|
74
|
+
return (
|
75
|
+
<div className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
|
76
|
+
<div className="flex justify-between items-start mb-4">
|
77
|
+
<div className="flex items-start gap-3">
|
78
|
+
<Database className="w-5 h-5 text-blue-600 mt-1" />
|
79
|
+
<div>
|
80
|
+
<div className="flex items-center gap-2">
|
81
|
+
<h3 className="text-lg font-semibold text-gray-900">
|
82
|
+
{dataset.name}
|
83
|
+
</h3>
|
84
|
+
<div className={`inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium ${STATUS_STYLES[dataset.workflow_status].bg} ${STATUS_STYLES[dataset.workflow_status].text}`}>
|
85
|
+
{STATUS_STYLES[dataset.workflow_status].icon}
|
86
|
+
<span>{dataset.workflow_status.charAt(0).toUpperCase() + dataset.workflow_status.slice(1)}</span>
|
87
|
+
</div>
|
88
|
+
</div>
|
89
|
+
<p className="text-sm text-gray-500 mt-1">
|
90
|
+
{dataset.description}
|
91
|
+
</p>
|
92
|
+
</div>
|
93
|
+
</div>
|
94
|
+
<div className="flex gap-2">
|
95
|
+
<Link
|
96
|
+
href={`${rootPath}/datasets/${dataset.id}`}
|
97
|
+
className={`transition-colors ${
|
98
|
+
dataset.workflow_status === 'analyzing'
|
99
|
+
? 'text-gray-300 cursor-not-allowed pointer-events-none'
|
100
|
+
: 'text-gray-400 hover:text-blue-600'
|
101
|
+
}`}
|
102
|
+
title={dataset.workflow_status === 'analyzing' ? 'Dataset is being analyzed' : 'View details'}
|
103
|
+
>
|
104
|
+
<ExternalLink className="w-5 h-5" />
|
105
|
+
</Link>
|
106
|
+
<button
|
107
|
+
onClick={() => onRefresh(dataset.id)}
|
108
|
+
disabled={dataset.workflow_status === 'analyzing'}
|
109
|
+
className={`transition-colors ${
|
110
|
+
dataset.workflow_status === 'analyzing'
|
111
|
+
? 'text-gray-300 cursor-not-allowed'
|
112
|
+
: 'text-gray-400 hover:text-blue-600'
|
113
|
+
}`}
|
114
|
+
title={dataset.workflow_status === 'analyzing' ? 'Dataset is being analyzed' : 'Refresh dataset'}
|
115
|
+
>
|
116
|
+
<RefreshCw className="w-5 h-5" />
|
117
|
+
</button>
|
118
|
+
{dataset.workflow_status === 'analyzing' && (
|
119
|
+
<button
|
120
|
+
onClick={() => onAbort(dataset.id)}
|
121
|
+
className="text-gray-400 hover:text-red-600 transition-colors"
|
122
|
+
title="Abort analysis"
|
123
|
+
>
|
124
|
+
<XCircle className="w-5 h-5" />
|
125
|
+
</button>
|
126
|
+
)}
|
127
|
+
<button
|
128
|
+
onClick={handleDownload}
|
129
|
+
className="text-gray-400 hover:text-blue-600 transition-colors"
|
130
|
+
title="Download dataset configuration"
|
131
|
+
>
|
132
|
+
<Download className="w-5 h-5" />
|
133
|
+
</button>
|
134
|
+
<button
|
135
|
+
onClick={() => fileInputRef.current?.click()}
|
136
|
+
className="text-gray-400 hover:text-green-600 transition-colors"
|
137
|
+
title="Upload dataset configuration"
|
138
|
+
>
|
139
|
+
<Upload className="w-5 h-5" />
|
140
|
+
</button>
|
141
|
+
<input
|
142
|
+
type="file"
|
143
|
+
ref={fileInputRef}
|
144
|
+
onChange={handleUpload}
|
145
|
+
accept=".json"
|
146
|
+
className="hidden"
|
147
|
+
/>
|
148
|
+
<button
|
149
|
+
className="text-gray-400 hover:text-red-600 transition-colors"
|
150
|
+
title="Delete dataset"
|
151
|
+
onClick={() => onDelete(dataset.id)}
|
152
|
+
>
|
153
|
+
<Trash2 className="w-5 h-5" />
|
154
|
+
</button>
|
155
|
+
</div>
|
156
|
+
</div>
|
157
|
+
|
158
|
+
<div className="grid grid-cols-2 gap-4 mt-4">
|
159
|
+
<div>
|
160
|
+
<span className="text-sm text-gray-500">Columns</span>
|
161
|
+
<p className="text-sm font-medium text-gray-900">
|
162
|
+
{dataset.columns.length} columns
|
163
|
+
</p>
|
164
|
+
</div>
|
165
|
+
<div>
|
166
|
+
<span className="text-sm text-gray-500">Rows</span>
|
167
|
+
<p className="text-sm font-medium text-gray-900">
|
168
|
+
{dataset.num_rows.toLocaleString()}
|
169
|
+
</p>
|
170
|
+
</div>
|
171
|
+
</div>
|
172
|
+
|
173
|
+
<div className="mt-4 pt-4 border-t border-gray-100">
|
174
|
+
<div className="flex flex-wrap gap-2">
|
175
|
+
{dataset.columns.slice(0, 3).map((column: Column) => (
|
176
|
+
<span
|
177
|
+
key={column.name}
|
178
|
+
className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
|
179
|
+
>
|
180
|
+
{column.name}
|
181
|
+
</span>
|
182
|
+
))}
|
183
|
+
{dataset.columns.length > 3 && (
|
184
|
+
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
185
|
+
+{dataset.columns.length - 3} more
|
186
|
+
</span>
|
187
|
+
)}
|
188
|
+
</div>
|
189
|
+
</div>
|
190
|
+
|
191
|
+
{dataset.workflow_status === 'failed' && dataset.stacktrace && (
|
192
|
+
<div className="mt-4 pt-4 border-t border-gray-100">
|
193
|
+
<button
|
194
|
+
onClick={() => onToggleError(dataset.id)}
|
195
|
+
className="flex items-center gap-2 text-sm text-red-600 hover:text-red-700"
|
196
|
+
>
|
197
|
+
<AlertCircle className="w-4 h-4" />
|
198
|
+
<span>View Error Details</span>
|
199
|
+
{isErrorExpanded ? (
|
200
|
+
<ChevronUp className="w-4 h-4" />
|
201
|
+
) : (
|
202
|
+
<ChevronDown className="w-4 h-4" />
|
203
|
+
)}
|
204
|
+
</button>
|
205
|
+
{isErrorExpanded && (
|
206
|
+
<StackTrace stacktrace={dataset.stacktrace} />
|
207
|
+
)}
|
208
|
+
</div>
|
209
|
+
)}
|
210
|
+
</div>
|
211
|
+
);
|
212
|
+
}
|
@@ -1,20 +1,33 @@
|
|
1
1
|
import React, { useState, useEffect } from 'react';
|
2
|
-
import {
|
3
|
-
|
2
|
+
import {
|
3
|
+
Activity, Calendar, Database, Settings, ExternalLink, Play, LineChart,
|
4
|
+
Trash2, Loader2, XCircle, CheckCircle2, AlertCircle, ChevronDown, ChevronUp,
|
5
|
+
Download, Upload
|
6
|
+
} from 'lucide-react';
|
4
7
|
import { Link, router } from "@inertiajs/react";
|
5
8
|
import { cn } from '@/lib/utils';
|
6
9
|
import type { Model, RetrainingJob, RetrainingRun } from '../types';
|
7
|
-
|
10
|
+
import { StackTrace } from './StackTrace';
|
11
|
+
import { DownloadModelModal, UploadModelModal } from './models';
|
12
|
+
import { Dataset } from '../types';
|
8
13
|
interface ModelCardProps {
|
9
14
|
initialModel: Model;
|
10
15
|
onViewDetails: (modelId: number) => void;
|
11
16
|
handleDelete: (modelId: number) => void;
|
12
17
|
rootPath: string;
|
18
|
+
datasets: Array<Dataset>;
|
13
19
|
}
|
14
20
|
|
15
|
-
export function ModelCard({ initialModel, onViewDetails, handleDelete, rootPath }: ModelCardProps) {
|
21
|
+
export function ModelCard({ initialModel, onViewDetails, handleDelete, rootPath, datasets }: ModelCardProps) {
|
16
22
|
const [model, setModel] = useState(initialModel);
|
17
23
|
const [showError, setShowError] = useState(false);
|
24
|
+
const [showDownloadModal, setShowDownloadModal] = useState(false);
|
25
|
+
const [showUploadModal, setShowUploadModal] = useState(false);
|
26
|
+
|
27
|
+
// Update local state when initialModel changes
|
28
|
+
useEffect(() => {
|
29
|
+
setModel(initialModel);
|
30
|
+
}, [initialModel]);
|
18
31
|
|
19
32
|
useEffect(() => {
|
20
33
|
let pollInterval: number | undefined;
|
@@ -53,6 +66,25 @@ export function ModelCard({ initialModel, onViewDetails, handleDelete, rootPath
|
|
53
66
|
}
|
54
67
|
};
|
55
68
|
|
69
|
+
const handleAbort = async () => {
|
70
|
+
try {
|
71
|
+
await router.post(`${rootPath}/models/${model.id}/abort`, {}, {
|
72
|
+
preserveScroll: true,
|
73
|
+
preserveState: true
|
74
|
+
});
|
75
|
+
const response = await fetch(`${rootPath}/models/${model.id}`, {
|
76
|
+
headers: {
|
77
|
+
'Accept': 'application/json'
|
78
|
+
}
|
79
|
+
});
|
80
|
+
const data = await response.json();
|
81
|
+
setModel(data.model);
|
82
|
+
} catch (error) {
|
83
|
+
console.error('Failed to abort training:', error);
|
84
|
+
setShowError(true);
|
85
|
+
}
|
86
|
+
};
|
87
|
+
|
56
88
|
const dataset = model.dataset;
|
57
89
|
const job = model.retraining_job;
|
58
90
|
const lastRun = model.last_run;
|
@@ -77,6 +109,7 @@ export function ModelCard({ initialModel, onViewDetails, handleDelete, rootPath
|
|
77
109
|
if (model.is_training) return 'Training in progress...';
|
78
110
|
if (!lastRun) return 'Never trained';
|
79
111
|
if (lastRun.status === 'failed') return 'Last run failed';
|
112
|
+
if (lastRun.status === 'aborted') return 'Last run aborted';
|
80
113
|
if (lastRun.status === 'success') {
|
81
114
|
return lastRun.deployable ? 'Last run succeeded' : 'Last run completed (below threshold)';
|
82
115
|
}
|
@@ -93,12 +126,29 @@ export function ModelCard({ initialModel, onViewDetails, handleDelete, rootPath
|
|
93
126
|
return 'text-gray-700';
|
94
127
|
};
|
95
128
|
|
129
|
+
const renderTrainingStatus = () => {
|
130
|
+
if (model.is_training) {
|
131
|
+
return (
|
132
|
+
<div className="flex items-center space-x-2">
|
133
|
+
<button
|
134
|
+
onClick={handleAbort}
|
135
|
+
className="text-gray-400 hover:text-red-600"
|
136
|
+
title="Abort training"
|
137
|
+
>
|
138
|
+
<XCircle className="w-5 h-5" />
|
139
|
+
</button>
|
140
|
+
</div>
|
141
|
+
);
|
142
|
+
}
|
143
|
+
return null;
|
144
|
+
};
|
145
|
+
|
96
146
|
return (
|
97
147
|
<div className="bg-white rounded-lg shadow-md p-6 hover:shadow-lg transition-shadow">
|
98
148
|
<div className="flex flex-col gap-2">
|
99
149
|
<div className="flex items-center gap-2">
|
100
150
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
101
|
-
${model.deployment_status === 'inference'
|
151
|
+
${model.deployment_status === 'inference'
|
102
152
|
? 'bg-blue-100 text-blue-800'
|
103
153
|
: 'bg-gray-100 text-gray-800'}`}
|
104
154
|
>
|
@@ -118,13 +168,41 @@ export function ModelCard({ initialModel, onViewDetails, handleDelete, rootPath
|
|
118
168
|
<button
|
119
169
|
onClick={handleTrain}
|
120
170
|
disabled={model.is_training}
|
121
|
-
className={`text-gray-400 hover:text-green-600 transition-colors ${
|
122
|
-
|
123
|
-
}`}
|
171
|
+
className={`text-gray-400 hover:text-green-600 transition-colors ${model.is_training ? 'opacity-50 cursor-not-allowed' : ''
|
172
|
+
}`}
|
124
173
|
title="Train model"
|
125
174
|
>
|
126
175
|
<Play className="w-5 h-5" />
|
127
176
|
</button>
|
177
|
+
{renderTrainingStatus()}
|
178
|
+
<button
|
179
|
+
onClick={() => setShowDownloadModal(true)}
|
180
|
+
className="text-gray-400 hover:text-blue-600"
|
181
|
+
title="Download configuration"
|
182
|
+
>
|
183
|
+
<Download className="w-5 h-5" />
|
184
|
+
</button>
|
185
|
+
<button
|
186
|
+
onClick={() => setShowUploadModal(true)}
|
187
|
+
className="text-gray-400 hover:text-green-600"
|
188
|
+
title="Upload configuration"
|
189
|
+
>
|
190
|
+
<Upload className="w-5 h-5" />
|
191
|
+
</button>
|
192
|
+
<Link
|
193
|
+
href={`${rootPath}/models/${model.id}/edit`}
|
194
|
+
className="text-gray-400 hover:text-gray-600"
|
195
|
+
title="Edit model"
|
196
|
+
>
|
197
|
+
<Settings className="w-5 h-5" />
|
198
|
+
</Link>
|
199
|
+
<button
|
200
|
+
onClick={() => handleDelete(model.id)}
|
201
|
+
className="text-gray-400 hover:text-gray-600"
|
202
|
+
title="Delete model"
|
203
|
+
>
|
204
|
+
<Trash2 className="w-5 h-5" />
|
205
|
+
</button>
|
128
206
|
{
|
129
207
|
model.metrics_url && (
|
130
208
|
<a
|
@@ -145,20 +223,6 @@ export function ModelCard({ initialModel, onViewDetails, handleDelete, rootPath
|
|
145
223
|
>
|
146
224
|
<ExternalLink className="w-5 h-5" />
|
147
225
|
</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
226
|
</div>
|
163
227
|
</div>
|
164
228
|
|
@@ -176,7 +240,7 @@ export function ModelCard({ initialModel, onViewDetails, handleDelete, rootPath
|
|
176
240
|
<div className="flex items-center gap-2">
|
177
241
|
<Database className="w-4 h-4 text-gray-400" />
|
178
242
|
{dataset ? (
|
179
|
-
<Link
|
243
|
+
<Link
|
180
244
|
href={`${rootPath}/datasets/${dataset.id}`}
|
181
245
|
className="text-sm text-blue-600 hover:text-blue-800"
|
182
246
|
>
|
@@ -214,11 +278,10 @@ export function ModelCard({ initialModel, onViewDetails, handleDelete, rootPath
|
|
214
278
|
{Object.entries(lastRun.metrics as Record<string, number>).map(([key, value]) => (
|
215
279
|
<div
|
216
280
|
key={key}
|
217
|
-
className={`px-2 py-1 rounded-md text-xs font-medium ${
|
218
|
-
lastRun.deployable
|
281
|
+
className={`px-2 py-1 rounded-md text-xs font-medium ${lastRun.deployable
|
219
282
|
? 'bg-green-100 text-green-800'
|
220
283
|
: 'bg-red-100 text-red-800'
|
221
|
-
|
284
|
+
}`}
|
222
285
|
>
|
223
286
|
{key}: {value.toFixed(4)}
|
224
287
|
</div>
|
@@ -243,13 +306,35 @@ export function ModelCard({ initialModel, onViewDetails, handleDelete, rootPath
|
|
243
306
|
</button>
|
244
307
|
{showError && (
|
245
308
|
<div className="mt-2 p-3 bg-red-50 rounded-md">
|
246
|
-
<
|
247
|
-
{lastRun.stacktrace}
|
248
|
-
</pre>
|
309
|
+
<StackTrace stacktrace={lastRun.stacktrace} />
|
249
310
|
</div>
|
250
311
|
)}
|
251
312
|
</div>
|
252
313
|
)}
|
314
|
+
{showDownloadModal && (
|
315
|
+
<DownloadModelModal
|
316
|
+
isOpen={showDownloadModal}
|
317
|
+
onClose={() => setShowDownloadModal(false)}
|
318
|
+
modelId={model.id}
|
319
|
+
/>
|
320
|
+
)}
|
321
|
+
|
322
|
+
{showUploadModal && (
|
323
|
+
<UploadModelModal
|
324
|
+
isOpen={showUploadModal}
|
325
|
+
onClose={() => setShowUploadModal(false)}
|
326
|
+
modelId={model.id}
|
327
|
+
dataset_id={model.dataset_id}
|
328
|
+
datasets={datasets}
|
329
|
+
/>
|
330
|
+
)}
|
331
|
+
<div className="flex items-center space-x-4">
|
332
|
+
<button
|
333
|
+
onClick={() => onViewDetails(model.id)}
|
334
|
+
className="text-gray-400 hover:text-blue-600"
|
335
|
+
>
|
336
|
+
</button>
|
337
|
+
</div>
|
253
338
|
</div>
|
254
339
|
);
|
255
340
|
}
|
@@ -0,0 +1,13 @@
|
|
1
|
+
interface StackTraceProps {
|
2
|
+
stacktrace: string;
|
3
|
+
}
|
4
|
+
|
5
|
+
export function StackTrace({ stacktrace }: StackTraceProps) {
|
6
|
+
return (
|
7
|
+
<div className="mt-2 p-3 bg-red-50 rounded-md">
|
8
|
+
<pre className="text-xs text-red-700 whitespace-pre-wrap break-words [word-break:break-word] font-mono">
|
9
|
+
{stacktrace}
|
10
|
+
</pre>
|
11
|
+
</div>
|
12
|
+
);
|
13
|
+
}
|
@@ -37,16 +37,19 @@ module Features
|
|
37
37
|
class DidConvert
|
38
38
|
include EasyML::Features
|
39
39
|
|
40
|
-
def
|
40
|
+
def computes_columns
|
41
|
+
["did_convert"]
|
42
|
+
end
|
43
|
+
|
44
|
+
def transform(df, feature)
|
41
45
|
df.with_column(
|
42
|
-
(Polars.col("rev") > 0)
|
43
|
-
.alias("did_convert")
|
46
|
+
(Polars.col("rev") > 0).alias("did_convert")
|
44
47
|
)
|
45
48
|
end
|
46
|
-
|
47
|
-
feature :did_convert,
|
48
|
-
|
49
|
-
|
49
|
+
|
50
|
+
feature name: "did_convert",
|
51
|
+
description: "Boolean true/false, did the loan application fund?"
|
52
|
+
|
50
53
|
end
|
51
54
|
end`}
|
52
55
|
</code>
|
@@ -0,0 +1,51 @@
|
|
1
|
+
import React, { useRef } from 'react';
|
2
|
+
import { Upload } from 'lucide-react';
|
3
|
+
import { router, usePage } from '@inertiajs/react';
|
4
|
+
|
5
|
+
export function UploadDatasetButton() {
|
6
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
7
|
+
const { rootPath } = usePage().props;
|
8
|
+
|
9
|
+
const handleUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
10
|
+
const file = event.target.files?.[0];
|
11
|
+
if (!file) return;
|
12
|
+
|
13
|
+
const formData = new FormData();
|
14
|
+
formData.append('config', file);
|
15
|
+
|
16
|
+
fetch(`${rootPath}/datasets/upload`, {
|
17
|
+
method: 'POST',
|
18
|
+
body: formData,
|
19
|
+
credentials: 'same-origin',
|
20
|
+
headers: {
|
21
|
+
'X-CSRF-Token': document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')?.content || '',
|
22
|
+
},
|
23
|
+
}).then(response => {
|
24
|
+
if (response.ok) {
|
25
|
+
window.location.reload();
|
26
|
+
} else {
|
27
|
+
console.error('Upload failed');
|
28
|
+
}
|
29
|
+
});
|
30
|
+
};
|
31
|
+
|
32
|
+
return (
|
33
|
+
<>
|
34
|
+
<button
|
35
|
+
onClick={() => fileInputRef.current?.click()}
|
36
|
+
className="inline-flex items-center gap-2 px-4 py-2 bg-white border border-gray-300 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-50"
|
37
|
+
title="Import dataset"
|
38
|
+
>
|
39
|
+
<Upload className="w-4 h-4" />
|
40
|
+
Import
|
41
|
+
</button>
|
42
|
+
<input
|
43
|
+
type="file"
|
44
|
+
ref={fileInputRef}
|
45
|
+
onChange={handleUpload}
|
46
|
+
accept=".json"
|
47
|
+
className="hidden"
|
48
|
+
/>
|
49
|
+
</>
|
50
|
+
);
|
51
|
+
}
|