easy_ml 0.2.0.pre.rc72 → 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 +38 -0
- data/app/frontend/components/DatasetCard.tsx +212 -0
- data/app/frontend/components/ModelCard.tsx +69 -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/training_job.rb +2 -2
- 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 +4 -0
- 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 +93 -36
- 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 +6 -4
- 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 +13 -1
- data/lib/easy_ml/core/tuner/adapters/base_adapter.rb +3 -3
- data/lib/easy_ml/core/tuner.rb +12 -11
- 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/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 +52 -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-B3sRjyMT.css +0 -1
- data/public/easy_ml/assets/assets/entrypoints/Application.tsx-Dfg-nTrB.js +0 -489
- data/public/easy_ml/assets/assets/entrypoints/Application.tsx-Dfg-nTrB.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
|
|
@@ -126,6 +127,43 @@ module EasyML
|
|
126
127
|
redirect_to easy_ml_models_path
|
127
128
|
end
|
128
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
|
+
|
129
167
|
private
|
130
168
|
|
131
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;
|
@@ -135,7 +148,7 @@ export function ModelCard({ initialModel, onViewDetails, handleDelete, rootPath
|
|
135
148
|
<div className="flex flex-col gap-2">
|
136
149
|
<div className="flex items-center gap-2">
|
137
150
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
|
138
|
-
${model.deployment_status === 'inference'
|
151
|
+
${model.deployment_status === 'inference'
|
139
152
|
? 'bg-blue-100 text-blue-800'
|
140
153
|
: 'bg-gray-100 text-gray-800'}`}
|
141
154
|
>
|
@@ -155,14 +168,41 @@ export function ModelCard({ initialModel, onViewDetails, handleDelete, rootPath
|
|
155
168
|
<button
|
156
169
|
onClick={handleTrain}
|
157
170
|
disabled={model.is_training}
|
158
|
-
className={`text-gray-400 hover:text-green-600 transition-colors ${
|
159
|
-
|
160
|
-
}`}
|
171
|
+
className={`text-gray-400 hover:text-green-600 transition-colors ${model.is_training ? 'opacity-50 cursor-not-allowed' : ''
|
172
|
+
}`}
|
161
173
|
title="Train model"
|
162
174
|
>
|
163
175
|
<Play className="w-5 h-5" />
|
164
176
|
</button>
|
165
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>
|
166
206
|
{
|
167
207
|
model.metrics_url && (
|
168
208
|
<a
|
@@ -183,20 +223,6 @@ export function ModelCard({ initialModel, onViewDetails, handleDelete, rootPath
|
|
183
223
|
>
|
184
224
|
<ExternalLink className="w-5 h-5" />
|
185
225
|
</Link>
|
186
|
-
<Link
|
187
|
-
href={`${rootPath}/models/${model.id}/edit`}
|
188
|
-
className="text-gray-400 hover:text-gray-600"
|
189
|
-
title="Edit model"
|
190
|
-
>
|
191
|
-
<Settings className="w-5 h-5" />
|
192
|
-
</Link>
|
193
|
-
<button
|
194
|
-
onClick={() => handleDelete(model.id)}
|
195
|
-
className="text-gray-400 hover:text-gray-600"
|
196
|
-
title="Delete model"
|
197
|
-
>
|
198
|
-
<Trash2 className="w-5 h-5" />
|
199
|
-
</button>
|
200
226
|
</div>
|
201
227
|
</div>
|
202
228
|
|
@@ -214,7 +240,7 @@ export function ModelCard({ initialModel, onViewDetails, handleDelete, rootPath
|
|
214
240
|
<div className="flex items-center gap-2">
|
215
241
|
<Database className="w-4 h-4 text-gray-400" />
|
216
242
|
{dataset ? (
|
217
|
-
<Link
|
243
|
+
<Link
|
218
244
|
href={`${rootPath}/datasets/${dataset.id}`}
|
219
245
|
className="text-sm text-blue-600 hover:text-blue-800"
|
220
246
|
>
|
@@ -252,11 +278,10 @@ export function ModelCard({ initialModel, onViewDetails, handleDelete, rootPath
|
|
252
278
|
{Object.entries(lastRun.metrics as Record<string, number>).map(([key, value]) => (
|
253
279
|
<div
|
254
280
|
key={key}
|
255
|
-
className={`px-2 py-1 rounded-md text-xs font-medium ${
|
256
|
-
lastRun.deployable
|
281
|
+
className={`px-2 py-1 rounded-md text-xs font-medium ${lastRun.deployable
|
257
282
|
? 'bg-green-100 text-green-800'
|
258
283
|
: 'bg-red-100 text-red-800'
|
259
|
-
|
284
|
+
}`}
|
260
285
|
>
|
261
286
|
{key}: {value.toFixed(4)}
|
262
287
|
</div>
|
@@ -281,13 +306,28 @@ export function ModelCard({ initialModel, onViewDetails, handleDelete, rootPath
|
|
281
306
|
</button>
|
282
307
|
{showError && (
|
283
308
|
<div className="mt-2 p-3 bg-red-50 rounded-md">
|
284
|
-
<
|
285
|
-
{lastRun.stacktrace}
|
286
|
-
</pre>
|
309
|
+
<StackTrace stacktrace={lastRun.stacktrace} />
|
287
310
|
</div>
|
288
311
|
)}
|
289
312
|
</div>
|
290
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
|
+
)}
|
291
331
|
<div className="flex items-center space-x-4">
|
292
332
|
<button
|
293
333
|
onClick={() => onViewDetails(model.id)}
|
@@ -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
|
+
}
|
@@ -0,0 +1,90 @@
|
|
1
|
+
import React, { useState } from 'react';
|
2
|
+
import { FileDown, FileJson, Database, ArrowRight } from 'lucide-react';
|
3
|
+
import { router, usePage } from '@inertiajs/react';
|
4
|
+
|
5
|
+
interface DownloadModelModalProps {
|
6
|
+
isOpen: boolean;
|
7
|
+
onClose: () => void;
|
8
|
+
modelId: number;
|
9
|
+
}
|
10
|
+
|
11
|
+
export function DownloadModelModal({ isOpen, onClose, modelId }: DownloadModelModalProps) {
|
12
|
+
const { rootPath } = usePage().props;
|
13
|
+
const [selectedOption, setSelectedOption] = useState<'model' | 'both' | null>(null);
|
14
|
+
|
15
|
+
if (!isOpen) return null;
|
16
|
+
|
17
|
+
const handleDownload = async () => {
|
18
|
+
if (!selectedOption) return;
|
19
|
+
|
20
|
+
const includeDataset = selectedOption === 'both';
|
21
|
+
window.location.href = `${rootPath}/models/${modelId}/download?include_dataset=${includeDataset}`;
|
22
|
+
onClose();
|
23
|
+
};
|
24
|
+
|
25
|
+
return (
|
26
|
+
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50">
|
27
|
+
<div className="bg-white rounded-xl p-6 w-full max-w-md shadow-2xl">
|
28
|
+
<div className="flex items-center justify-between mb-6">
|
29
|
+
<h3 className="text-lg font-semibold text-gray-900">Download Configuration</h3>
|
30
|
+
<div className="p-2 bg-blue-50 rounded-lg">
|
31
|
+
<FileDown className="w-5 h-5 text-blue-600" />
|
32
|
+
</div>
|
33
|
+
</div>
|
34
|
+
|
35
|
+
<div className="space-y-3">
|
36
|
+
<button
|
37
|
+
onClick={() => setSelectedOption('model')}
|
38
|
+
className={`w-full px-4 py-3 rounded-lg text-left transition-all duration-200 ${
|
39
|
+
selectedOption === 'model'
|
40
|
+
? 'bg-blue-50 border-2 border-blue-500 ring-2 ring-blue-200'
|
41
|
+
: 'bg-white border-2 border-gray-200 hover:border-blue-200'
|
42
|
+
}`}
|
43
|
+
>
|
44
|
+
<div className="flex items-center justify-between">
|
45
|
+
<div>
|
46
|
+
<div className="font-medium text-gray-900">Model Only</div>
|
47
|
+
<div className="text-sm text-gray-500">Download model configuration without dataset details</div>
|
48
|
+
</div>
|
49
|
+
<FileJson className={`w-5 h-5 ${selectedOption === 'model' ? 'text-blue-600' : 'text-gray-400'}`} />
|
50
|
+
</div>
|
51
|
+
</button>
|
52
|
+
|
53
|
+
<button
|
54
|
+
onClick={() => setSelectedOption('both')}
|
55
|
+
className={`w-full px-4 py-3 rounded-lg text-left transition-all duration-200 ${
|
56
|
+
selectedOption === 'both'
|
57
|
+
? 'bg-blue-50 border-2 border-blue-500 ring-2 ring-blue-200'
|
58
|
+
: 'bg-white border-2 border-gray-200 hover:border-blue-200'
|
59
|
+
}`}
|
60
|
+
>
|
61
|
+
<div className="flex items-center justify-between">
|
62
|
+
<div>
|
63
|
+
<div className="font-medium text-gray-900">Model + Dataset</div>
|
64
|
+
<div className="text-sm text-gray-500">Download complete configuration including dataset details</div>
|
65
|
+
</div>
|
66
|
+
<Database className={`w-5 h-5 ${selectedOption === 'both' ? 'text-blue-600' : 'text-gray-400'}`} />
|
67
|
+
</div>
|
68
|
+
</button>
|
69
|
+
</div>
|
70
|
+
|
71
|
+
<div className="mt-6 flex justify-end gap-3">
|
72
|
+
<button
|
73
|
+
onClick={onClose}
|
74
|
+
className="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900"
|
75
|
+
>
|
76
|
+
Cancel
|
77
|
+
</button>
|
78
|
+
<button
|
79
|
+
onClick={handleDownload}
|
80
|
+
disabled={!selectedOption}
|
81
|
+
className="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed inline-flex items-center gap-2"
|
82
|
+
>
|
83
|
+
Download
|
84
|
+
<ArrowRight className="w-4 h-4" />
|
85
|
+
</button>
|
86
|
+
</div>
|
87
|
+
</div>
|
88
|
+
</div>
|
89
|
+
);
|
90
|
+
}
|