easy_ml 0.2.0.pre.rc72 → 0.2.0.pre.rc76

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/easy_ml/datasets_controller.rb +33 -0
  3. data/app/controllers/easy_ml/datasources_controller.rb +7 -0
  4. data/app/controllers/easy_ml/models_controller.rb +38 -0
  5. data/app/frontend/components/DatasetCard.tsx +212 -0
  6. data/app/frontend/components/ModelCard.tsx +69 -29
  7. data/app/frontend/components/StackTrace.tsx +13 -0
  8. data/app/frontend/components/dataset/FeatureConfigPopover.tsx +10 -7
  9. data/app/frontend/components/dataset/PreprocessingConfig.tsx +2 -2
  10. data/app/frontend/components/datasets/UploadDatasetButton.tsx +51 -0
  11. data/app/frontend/components/models/DownloadModelModal.tsx +90 -0
  12. data/app/frontend/components/models/UploadModelModal.tsx +212 -0
  13. data/app/frontend/components/models/index.ts +2 -0
  14. data/app/frontend/pages/DatasetsPage.tsx +36 -130
  15. data/app/frontend/pages/DatasourcesPage.tsx +22 -2
  16. data/app/frontend/pages/ModelsPage.tsx +37 -11
  17. data/app/frontend/types/dataset.ts +1 -2
  18. data/app/frontend/types.ts +1 -1
  19. data/app/jobs/easy_ml/training_job.rb +2 -2
  20. data/app/models/easy_ml/column/imputers/base.rb +4 -0
  21. data/app/models/easy_ml/column/imputers/clip.rb +5 -3
  22. data/app/models/easy_ml/column/imputers/imputer.rb +11 -13
  23. data/app/models/easy_ml/column/imputers/mean.rb +7 -3
  24. data/app/models/easy_ml/column/imputers/null_imputer.rb +3 -0
  25. data/app/models/easy_ml/column/imputers/ordinal_encoder.rb +5 -1
  26. data/app/models/easy_ml/column/imputers.rb +3 -1
  27. data/app/models/easy_ml/column/lineage/base.rb +5 -1
  28. data/app/models/easy_ml/column/lineage/computed_by_feature.rb +1 -1
  29. data/app/models/easy_ml/column/lineage/preprocessed.rb +1 -1
  30. data/app/models/easy_ml/column/lineage/raw_dataset.rb +1 -1
  31. data/app/models/easy_ml/column/selector.rb +4 -0
  32. data/app/models/easy_ml/column.rb +79 -63
  33. data/app/models/easy_ml/column_history.rb +28 -28
  34. data/app/models/easy_ml/column_list/imputer.rb +23 -0
  35. data/app/models/easy_ml/column_list.rb +39 -26
  36. data/app/models/easy_ml/dataset/learner/base.rb +34 -0
  37. data/app/models/easy_ml/dataset/learner/eager/boolean.rb +10 -0
  38. data/app/models/easy_ml/dataset/learner/eager/categorical.rb +51 -0
  39. data/app/models/easy_ml/dataset/learner/eager/query.rb +37 -0
  40. data/app/models/easy_ml/dataset/learner/eager.rb +43 -0
  41. data/app/models/easy_ml/dataset/learner/lazy/boolean.rb +13 -0
  42. data/app/models/easy_ml/dataset/learner/lazy/categorical.rb +10 -0
  43. data/app/models/easy_ml/dataset/learner/lazy/datetime.rb +19 -0
  44. data/app/models/easy_ml/dataset/learner/lazy/null.rb +17 -0
  45. data/app/models/easy_ml/dataset/learner/lazy/numeric.rb +19 -0
  46. data/app/models/easy_ml/dataset/learner/lazy/query.rb +69 -0
  47. data/app/models/easy_ml/dataset/learner/lazy/string.rb +19 -0
  48. data/app/models/easy_ml/dataset/learner/lazy.rb +51 -0
  49. data/app/models/easy_ml/dataset/learner/query.rb +25 -0
  50. data/app/models/easy_ml/dataset/learner.rb +100 -0
  51. data/app/models/easy_ml/dataset.rb +150 -36
  52. data/app/models/easy_ml/dataset_history.rb +1 -0
  53. data/app/models/easy_ml/datasource.rb +13 -5
  54. data/app/models/easy_ml/event.rb +4 -0
  55. data/app/models/easy_ml/export/column.rb +27 -0
  56. data/app/models/easy_ml/export/dataset.rb +37 -0
  57. data/app/models/easy_ml/export/datasource.rb +12 -0
  58. data/app/models/easy_ml/export/feature.rb +24 -0
  59. data/app/models/easy_ml/export/model.rb +40 -0
  60. data/app/models/easy_ml/export/retraining_job.rb +20 -0
  61. data/app/models/easy_ml/export/splitter.rb +14 -0
  62. data/app/models/easy_ml/feature.rb +21 -0
  63. data/app/models/easy_ml/import/column.rb +35 -0
  64. data/app/models/easy_ml/import/dataset.rb +148 -0
  65. data/app/models/easy_ml/import/feature.rb +36 -0
  66. data/app/models/easy_ml/import/model.rb +136 -0
  67. data/app/models/easy_ml/import/retraining_job.rb +29 -0
  68. data/app/models/easy_ml/import/splitter.rb +34 -0
  69. data/app/models/easy_ml/lineage.rb +44 -0
  70. data/app/models/easy_ml/model.rb +93 -36
  71. data/app/models/easy_ml/model_file.rb +6 -0
  72. data/app/models/easy_ml/models/xgboost/evals_callback.rb +7 -7
  73. data/app/models/easy_ml/models/xgboost.rb +33 -9
  74. data/app/models/easy_ml/retraining_job.rb +8 -1
  75. data/app/models/easy_ml/retraining_run.rb +6 -4
  76. data/app/models/easy_ml/splitter.rb +8 -0
  77. data/app/models/lineage_history.rb +6 -0
  78. data/app/serializers/easy_ml/column_serializer.rb +7 -1
  79. data/app/serializers/easy_ml/dataset_serializer.rb +2 -1
  80. data/app/serializers/easy_ml/lineage_serializer.rb +9 -0
  81. data/config/routes.rb +13 -1
  82. data/lib/easy_ml/core/tuner/adapters/base_adapter.rb +3 -3
  83. data/lib/easy_ml/core/tuner.rb +12 -11
  84. data/lib/easy_ml/data/polars_column.rb +149 -100
  85. data/lib/easy_ml/data/polars_reader.rb +8 -5
  86. data/lib/easy_ml/data/polars_schema.rb +56 -0
  87. data/lib/easy_ml/data/splits/file_split.rb +20 -2
  88. data/lib/easy_ml/data/splits/split.rb +10 -1
  89. data/lib/easy_ml/data.rb +1 -0
  90. data/lib/easy_ml/deep_compact.rb +19 -0
  91. data/lib/easy_ml/feature_store.rb +2 -6
  92. data/lib/easy_ml/railtie/generators/migration/migration_generator.rb +6 -0
  93. data/lib/easy_ml/railtie/templates/migration/add_extra_metadata_to_columns.rb.tt +9 -0
  94. data/lib/easy_ml/railtie/templates/migration/add_raw_schema_to_datasets.rb.tt +9 -0
  95. data/lib/easy_ml/railtie/templates/migration/add_unique_constraint_to_easy_ml_model_names.rb.tt +8 -0
  96. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_lineages.rb.tt +24 -0
  97. data/lib/easy_ml/railtie/templates/migration/remove_evaluator_from_retraining_jobs.rb.tt +7 -0
  98. data/lib/easy_ml/railtie/templates/migration/update_preprocessing_steps_to_jsonb.rb.tt +18 -0
  99. data/lib/easy_ml/timing.rb +34 -0
  100. data/lib/easy_ml/version.rb +1 -1
  101. data/lib/easy_ml.rb +2 -0
  102. data/public/easy_ml/assets/.vite/manifest.json +2 -2
  103. data/public/easy_ml/assets/assets/Application-nnn_XLuL.css +1 -0
  104. data/public/easy_ml/assets/assets/entrypoints/Application.tsx-B1qLZuyu.js +522 -0
  105. data/public/easy_ml/assets/assets/entrypoints/Application.tsx-B1qLZuyu.js.map +1 -0
  106. metadata +52 -12
  107. data/app/models/easy_ml/column/learners/base.rb +0 -103
  108. data/app/models/easy_ml/column/learners/boolean.rb +0 -11
  109. data/app/models/easy_ml/column/learners/categorical.rb +0 -51
  110. data/app/models/easy_ml/column/learners/datetime.rb +0 -19
  111. data/app/models/easy_ml/column/learners/null.rb +0 -22
  112. data/app/models/easy_ml/column/learners/numeric.rb +0 -33
  113. data/app/models/easy_ml/column/learners/string.rb +0 -15
  114. data/public/easy_ml/assets/assets/Application-B3sRjyMT.css +0 -1
  115. data/public/easy_ml/assets/assets/entrypoints/Application.tsx-Dfg-nTrB.js +0 -489
  116. 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: ed02cf75773a42dbff72981b550b5b2622a059ab2d0b89584113bd526811dcc9
4
- data.tar.gz: '095c4973dd471f64f0175e11788fafd1618c3ef4e23d8354b5a64d9fecd0a470'
3
+ metadata.gz: 1eebc157e0f33c3da40ef2b1bdb7cc0ed1c2b6f73615cdf26a6898cb60e60d2d
4
+ data.tar.gz: a12b441fe0736f251de773574858316346ba19c5b3784d73f3db200af0e619e4
5
5
  SHA512:
6
- metadata.gz: 03dde99dc4afb11da099b0b7ae11aaebc2150b76c7248ed779f02d4ca0adcefb04397de8d0bb61041ea5b1bbc2d1f17c74d0e2d257f5d8f96ac8b52397ab4461
7
- data.tar.gz: 364f932614a638d372abbeb73501dc2940c03ea81ece27bd0c97c4412f1587af0e5c9e36a769ecbc5f7f55c768265c8410f67e31b98a16f1f8afa17fa5b7952a
6
+ metadata.gz: 4aabb816a9d02a6f2bd870cde3db3eaaf00a314cf5e0d50a11bf707534b9d93eddee648d62304f48976916ea9d5942269dbeded81d49df23199ffcc13d6ae0eb
7
+ data.tar.gz: 284973f49424ac622ceb3e44071e88336ea316154dee788b0e7c865441eeb01939192289deea84283b691bf8f5a3b79f708d3d62ab9fcec3d596f67ff4c093a9
@@ -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 { Activity, Calendar, Database, Settings, ExternalLink, Play, LineChart,
3
- Trash2, Loader2, XCircle, CheckCircle2, AlertCircle, ChevronDown, ChevronUp } from 'lucide-react';
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
- model.is_training ? 'opacity-50 cursor-not-allowed' : ''
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
- <pre className="text-xs text-red-700 whitespace-pre-wrap break-words [word-break:break-word] font-mono">
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 did_convert(df)
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
- name: "Did Convert",
49
- description: "Boolean true/false..."
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>
@@ -314,9 +314,9 @@ export function PreprocessingConfig({
314
314
  </p>
315
315
  </div>
316
316
  ) : (
317
- <div className="flex-3/4 flex items-start gap-1">
317
+ <div className="flex items-start gap-1 max-w-[100%]">
318
318
  <p
319
- className="text-sm text-gray-500 cursor-pointer flex-grow truncate"
319
+ className="text-sm text-gray-500 cursor-pointer flex-grow line-clamp-3"
320
320
  onClick={handleDescriptionClick}
321
321
  >
322
322
  {column.description || 'No description provided'}
@@ -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
+ }