easy_ml 0.1.4 → 0.2.0.pre.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (239) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -26
  3. data/Rakefile +45 -0
  4. data/app/controllers/easy_ml/application_controller.rb +67 -0
  5. data/app/controllers/easy_ml/columns_controller.rb +38 -0
  6. data/app/controllers/easy_ml/datasets_controller.rb +156 -0
  7. data/app/controllers/easy_ml/datasources_controller.rb +88 -0
  8. data/app/controllers/easy_ml/deploys_controller.rb +20 -0
  9. data/app/controllers/easy_ml/models_controller.rb +151 -0
  10. data/app/controllers/easy_ml/retraining_runs_controller.rb +19 -0
  11. data/app/controllers/easy_ml/settings_controller.rb +59 -0
  12. data/app/frontend/components/AlertProvider.tsx +108 -0
  13. data/app/frontend/components/DatasetPreview.tsx +161 -0
  14. data/app/frontend/components/EmptyState.tsx +28 -0
  15. data/app/frontend/components/ModelCard.tsx +255 -0
  16. data/app/frontend/components/ModelDetails.tsx +334 -0
  17. data/app/frontend/components/ModelForm.tsx +384 -0
  18. data/app/frontend/components/Navigation.tsx +300 -0
  19. data/app/frontend/components/Pagination.tsx +72 -0
  20. data/app/frontend/components/Popover.tsx +55 -0
  21. data/app/frontend/components/PredictionStream.tsx +105 -0
  22. data/app/frontend/components/ScheduleModal.tsx +726 -0
  23. data/app/frontend/components/SearchInput.tsx +23 -0
  24. data/app/frontend/components/SearchableSelect.tsx +132 -0
  25. data/app/frontend/components/dataset/AutosaveIndicator.tsx +39 -0
  26. data/app/frontend/components/dataset/ColumnConfigModal.tsx +431 -0
  27. data/app/frontend/components/dataset/ColumnFilters.tsx +256 -0
  28. data/app/frontend/components/dataset/ColumnList.tsx +101 -0
  29. data/app/frontend/components/dataset/FeatureConfigPopover.tsx +57 -0
  30. data/app/frontend/components/dataset/FeaturePicker.tsx +205 -0
  31. data/app/frontend/components/dataset/PreprocessingConfig.tsx +704 -0
  32. data/app/frontend/components/dataset/SplitConfigurator.tsx +120 -0
  33. data/app/frontend/components/dataset/splitters/DateSplitter.tsx +58 -0
  34. data/app/frontend/components/dataset/splitters/KFoldSplitter.tsx +68 -0
  35. data/app/frontend/components/dataset/splitters/LeavePOutSplitter.tsx +29 -0
  36. data/app/frontend/components/dataset/splitters/PredefinedSplitter.tsx +146 -0
  37. data/app/frontend/components/dataset/splitters/RandomSplitter.tsx +85 -0
  38. data/app/frontend/components/dataset/splitters/StratifiedSplitter.tsx +79 -0
  39. data/app/frontend/components/dataset/splitters/constants.ts +77 -0
  40. data/app/frontend/components/dataset/splitters/types.ts +168 -0
  41. data/app/frontend/components/dataset/splitters/utils.ts +53 -0
  42. data/app/frontend/components/features/CodeEditor.tsx +46 -0
  43. data/app/frontend/components/features/DataPreview.tsx +150 -0
  44. data/app/frontend/components/features/FeatureCard.tsx +88 -0
  45. data/app/frontend/components/features/FeatureForm.tsx +235 -0
  46. data/app/frontend/components/features/FeatureGroupCard.tsx +54 -0
  47. data/app/frontend/components/settings/PluginSettings.tsx +81 -0
  48. data/app/frontend/components/ui/badge.tsx +44 -0
  49. data/app/frontend/components/ui/collapsible.tsx +9 -0
  50. data/app/frontend/components/ui/scroll-area.tsx +46 -0
  51. data/app/frontend/components/ui/separator.tsx +29 -0
  52. data/app/frontend/entrypoints/App.tsx +40 -0
  53. data/app/frontend/entrypoints/Application.tsx +24 -0
  54. data/app/frontend/hooks/useAutosave.ts +61 -0
  55. data/app/frontend/layouts/Layout.tsx +38 -0
  56. data/app/frontend/lib/utils.ts +6 -0
  57. data/app/frontend/mockData.ts +272 -0
  58. data/app/frontend/pages/DatasetDetailsPage.tsx +103 -0
  59. data/app/frontend/pages/DatasetsPage.tsx +261 -0
  60. data/app/frontend/pages/DatasourceFormPage.tsx +147 -0
  61. data/app/frontend/pages/DatasourcesPage.tsx +261 -0
  62. data/app/frontend/pages/EditModelPage.tsx +45 -0
  63. data/app/frontend/pages/EditTransformationPage.tsx +56 -0
  64. data/app/frontend/pages/ModelsPage.tsx +115 -0
  65. data/app/frontend/pages/NewDatasetPage.tsx +366 -0
  66. data/app/frontend/pages/NewModelPage.tsx +45 -0
  67. data/app/frontend/pages/NewTransformationPage.tsx +43 -0
  68. data/app/frontend/pages/SettingsPage.tsx +272 -0
  69. data/app/frontend/pages/ShowModelPage.tsx +30 -0
  70. data/app/frontend/pages/TransformationsPage.tsx +95 -0
  71. data/app/frontend/styles/application.css +100 -0
  72. data/app/frontend/types/dataset.ts +146 -0
  73. data/app/frontend/types/datasource.ts +33 -0
  74. data/app/frontend/types/preprocessing.ts +1 -0
  75. data/app/frontend/types.ts +113 -0
  76. data/app/helpers/easy_ml/application_helper.rb +10 -0
  77. data/app/jobs/easy_ml/application_job.rb +21 -0
  78. data/app/jobs/easy_ml/batch_job.rb +46 -0
  79. data/app/jobs/easy_ml/compute_feature_job.rb +19 -0
  80. data/app/jobs/easy_ml/deploy_job.rb +13 -0
  81. data/app/jobs/easy_ml/finalize_feature_job.rb +15 -0
  82. data/app/jobs/easy_ml/refresh_dataset_job.rb +32 -0
  83. data/app/jobs/easy_ml/schedule_retraining_job.rb +11 -0
  84. data/app/jobs/easy_ml/sync_datasource_job.rb +17 -0
  85. data/app/jobs/easy_ml/training_job.rb +62 -0
  86. data/app/models/easy_ml/adapters/base_adapter.rb +45 -0
  87. data/app/models/easy_ml/adapters/polars_adapter.rb +77 -0
  88. data/app/models/easy_ml/cleaner.rb +82 -0
  89. data/app/models/easy_ml/column.rb +124 -0
  90. data/app/models/easy_ml/column_history.rb +30 -0
  91. data/app/models/easy_ml/column_list.rb +122 -0
  92. data/app/models/easy_ml/concerns/configurable.rb +61 -0
  93. data/app/models/easy_ml/concerns/versionable.rb +19 -0
  94. data/app/models/easy_ml/dataset.rb +767 -0
  95. data/app/models/easy_ml/dataset_history.rb +56 -0
  96. data/app/models/easy_ml/datasource.rb +182 -0
  97. data/app/models/easy_ml/datasource_history.rb +24 -0
  98. data/app/models/easy_ml/datasources/base_datasource.rb +54 -0
  99. data/app/models/easy_ml/datasources/file_datasource.rb +58 -0
  100. data/app/models/easy_ml/datasources/polars_datasource.rb +89 -0
  101. data/app/models/easy_ml/datasources/s3_datasource.rb +97 -0
  102. data/app/models/easy_ml/deploy.rb +114 -0
  103. data/app/models/easy_ml/event.rb +79 -0
  104. data/app/models/easy_ml/feature.rb +437 -0
  105. data/app/models/easy_ml/feature_history.rb +38 -0
  106. data/app/models/easy_ml/model.rb +575 -41
  107. data/app/models/easy_ml/model_file.rb +133 -0
  108. data/app/models/easy_ml/model_file_history.rb +24 -0
  109. data/app/models/easy_ml/model_history.rb +51 -0
  110. data/app/models/easy_ml/models/base_model.rb +58 -0
  111. data/app/models/easy_ml/models/hyperparameters/base.rb +99 -0
  112. data/app/models/easy_ml/models/hyperparameters/xgboost/dart.rb +82 -0
  113. data/app/models/easy_ml/models/hyperparameters/xgboost/gblinear.rb +82 -0
  114. data/app/models/easy_ml/models/hyperparameters/xgboost/gbtree.rb +97 -0
  115. data/app/models/easy_ml/models/hyperparameters/xgboost.rb +71 -0
  116. data/app/models/easy_ml/models/xgboost/evals_callback.rb +138 -0
  117. data/app/models/easy_ml/models/xgboost/progress_callback.rb +39 -0
  118. data/app/models/easy_ml/models/xgboost.rb +544 -5
  119. data/app/models/easy_ml/prediction.rb +44 -0
  120. data/app/models/easy_ml/retraining_job.rb +278 -0
  121. data/app/models/easy_ml/retraining_run.rb +184 -0
  122. data/app/models/easy_ml/settings.rb +37 -0
  123. data/app/models/easy_ml/splitter.rb +90 -0
  124. data/app/models/easy_ml/splitters/base_splitter.rb +28 -0
  125. data/app/models/easy_ml/splitters/date_splitter.rb +91 -0
  126. data/app/models/easy_ml/splitters/predefined_splitter.rb +74 -0
  127. data/app/models/easy_ml/splitters/random_splitter.rb +82 -0
  128. data/app/models/easy_ml/tuner_job.rb +56 -0
  129. data/app/models/easy_ml/tuner_run.rb +31 -0
  130. data/app/models/splitter_history.rb +6 -0
  131. data/app/serializers/easy_ml/column_serializer.rb +27 -0
  132. data/app/serializers/easy_ml/dataset_serializer.rb +73 -0
  133. data/app/serializers/easy_ml/datasource_serializer.rb +64 -0
  134. data/app/serializers/easy_ml/feature_serializer.rb +27 -0
  135. data/app/serializers/easy_ml/model_serializer.rb +90 -0
  136. data/app/serializers/easy_ml/retraining_job_serializer.rb +22 -0
  137. data/app/serializers/easy_ml/retraining_run_serializer.rb +39 -0
  138. data/app/serializers/easy_ml/settings_serializer.rb +9 -0
  139. data/app/views/layouts/easy_ml/application.html.erb +15 -0
  140. data/config/initializers/resque.rb +3 -0
  141. data/config/resque-pool.yml +6 -0
  142. data/config/routes.rb +39 -0
  143. data/config/spring.rb +1 -0
  144. data/config/vite.json +15 -0
  145. data/lib/easy_ml/configuration.rb +64 -0
  146. data/lib/easy_ml/core/evaluators/base_evaluator.rb +53 -0
  147. data/lib/easy_ml/core/evaluators/classification_evaluators.rb +126 -0
  148. data/lib/easy_ml/core/evaluators/regression_evaluators.rb +66 -0
  149. data/lib/easy_ml/core/model_evaluator.rb +161 -89
  150. data/lib/easy_ml/core/tuner/adapters/base_adapter.rb +28 -18
  151. data/lib/easy_ml/core/tuner/adapters/xgboost_adapter.rb +4 -25
  152. data/lib/easy_ml/core/tuner.rb +123 -62
  153. data/lib/easy_ml/core.rb +0 -3
  154. data/lib/easy_ml/core_ext/hash.rb +24 -0
  155. data/lib/easy_ml/core_ext/pathname.rb +11 -5
  156. data/lib/easy_ml/data/date_converter.rb +90 -0
  157. data/lib/easy_ml/data/filter_extensions.rb +31 -0
  158. data/lib/easy_ml/data/polars_column.rb +126 -0
  159. data/lib/easy_ml/data/polars_reader.rb +297 -0
  160. data/lib/easy_ml/data/preprocessor.rb +280 -142
  161. data/lib/easy_ml/data/simple_imputer.rb +255 -0
  162. data/lib/easy_ml/data/splits/file_split.rb +252 -0
  163. data/lib/easy_ml/data/splits/in_memory_split.rb +54 -0
  164. data/lib/easy_ml/data/splits/split.rb +95 -0
  165. data/lib/easy_ml/data/splits.rb +9 -0
  166. data/lib/easy_ml/data/statistics_learner.rb +93 -0
  167. data/lib/easy_ml/data/synced_directory.rb +341 -0
  168. data/lib/easy_ml/data.rb +6 -2
  169. data/lib/easy_ml/engine.rb +105 -6
  170. data/lib/easy_ml/feature_store.rb +227 -0
  171. data/lib/easy_ml/features.rb +61 -0
  172. data/lib/easy_ml/initializers/inflections.rb +17 -3
  173. data/lib/easy_ml/logging.rb +2 -2
  174. data/lib/easy_ml/predict.rb +74 -0
  175. data/lib/easy_ml/railtie/generators/migration/migration_generator.rb +192 -36
  176. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_column_histories.rb.tt +9 -0
  177. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_columns.rb.tt +25 -0
  178. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_dataset_histories.rb.tt +9 -0
  179. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasets.rb.tt +31 -0
  180. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasource_histories.rb.tt +9 -0
  181. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_datasources.rb.tt +16 -0
  182. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_deploys.rb.tt +24 -0
  183. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_events.rb.tt +20 -0
  184. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_feature_histories.rb.tt +14 -0
  185. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_features.rb.tt +32 -0
  186. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_file_histories.rb.tt +9 -0
  187. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_files.rb.tt +17 -0
  188. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_model_histories.rb.tt +9 -0
  189. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_models.rb.tt +20 -9
  190. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_predictions.rb.tt +17 -0
  191. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_retraining_jobs.rb.tt +77 -0
  192. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_settings.rb.tt +9 -0
  193. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_splitter_histories.rb.tt +9 -0
  194. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_splitters.rb.tt +15 -0
  195. data/lib/easy_ml/railtie/templates/migration/create_easy_ml_tuner_jobs.rb.tt +40 -0
  196. data/lib/easy_ml/support/est.rb +5 -1
  197. data/lib/easy_ml/support/file_rotate.rb +79 -15
  198. data/lib/easy_ml/support/file_support.rb +9 -0
  199. data/lib/easy_ml/support/local_file.rb +24 -0
  200. data/lib/easy_ml/support/lockable.rb +62 -0
  201. data/lib/easy_ml/support/synced_file.rb +103 -0
  202. data/lib/easy_ml/support/utc.rb +5 -1
  203. data/lib/easy_ml/support.rb +6 -3
  204. data/lib/easy_ml/version.rb +4 -1
  205. data/lib/easy_ml.rb +7 -2
  206. metadata +355 -72
  207. data/app/models/easy_ml/models.rb +0 -5
  208. data/lib/easy_ml/core/model.rb +0 -30
  209. data/lib/easy_ml/core/model_core.rb +0 -181
  210. data/lib/easy_ml/core/models/hyperparameters/base.rb +0 -34
  211. data/lib/easy_ml/core/models/hyperparameters/xgboost.rb +0 -19
  212. data/lib/easy_ml/core/models/xgboost.rb +0 -10
  213. data/lib/easy_ml/core/models/xgboost_core.rb +0 -220
  214. data/lib/easy_ml/core/models.rb +0 -10
  215. data/lib/easy_ml/core/uploaders/model_uploader.rb +0 -24
  216. data/lib/easy_ml/core/uploaders.rb +0 -7
  217. data/lib/easy_ml/data/dataloader.rb +0 -6
  218. data/lib/easy_ml/data/dataset/data/preprocessor/statistics.json +0 -31
  219. data/lib/easy_ml/data/dataset/data/sample_info.json +0 -1
  220. data/lib/easy_ml/data/dataset/dataset/files/sample_info.json +0 -1
  221. data/lib/easy_ml/data/dataset/splits/file_split.rb +0 -140
  222. data/lib/easy_ml/data/dataset/splits/in_memory_split.rb +0 -49
  223. data/lib/easy_ml/data/dataset/splits/split.rb +0 -98
  224. data/lib/easy_ml/data/dataset/splits.rb +0 -11
  225. data/lib/easy_ml/data/dataset/splitters/date_splitter.rb +0 -43
  226. data/lib/easy_ml/data/dataset/splitters.rb +0 -9
  227. data/lib/easy_ml/data/dataset.rb +0 -430
  228. data/lib/easy_ml/data/datasource/datasource_factory.rb +0 -60
  229. data/lib/easy_ml/data/datasource/file_datasource.rb +0 -40
  230. data/lib/easy_ml/data/datasource/merged_datasource.rb +0 -64
  231. data/lib/easy_ml/data/datasource/polars_datasource.rb +0 -41
  232. data/lib/easy_ml/data/datasource/s3_datasource.rb +0 -89
  233. data/lib/easy_ml/data/datasource.rb +0 -33
  234. data/lib/easy_ml/data/preprocessor/preprocessor.rb +0 -205
  235. data/lib/easy_ml/data/preprocessor/simple_imputer.rb +0 -402
  236. data/lib/easy_ml/deployment.rb +0 -5
  237. data/lib/easy_ml/support/synced_directory.rb +0 -134
  238. data/lib/easy_ml/transforms.rb +0 -29
  239. /data/{lib/easy_ml/core → app/models/easy_ml}/models/hyperparameters.rb +0 -0
@@ -0,0 +1,72 @@
1
+ import React from 'react';
2
+ import { ChevronLeft, ChevronRight } from 'lucide-react';
3
+
4
+ interface PaginationProps {
5
+ currentPage: number;
6
+ totalPages: number;
7
+ onPageChange: (page: number) => void;
8
+ }
9
+
10
+ export function Pagination({ currentPage, totalPages, onPageChange }: PaginationProps) {
11
+ return (
12
+ <div className="flex items-center justify-between px-4 py-3 border-t border-gray-200">
13
+ <div className="flex-1 flex justify-between sm:hidden">
14
+ <button
15
+ onClick={() => onPageChange(currentPage - 1)}
16
+ disabled={currentPage === 1}
17
+ className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:bg-gray-100 disabled:text-gray-400"
18
+ >
19
+ Previous
20
+ </button>
21
+ <button
22
+ onClick={() => onPageChange(currentPage + 1)}
23
+ disabled={currentPage === totalPages}
24
+ className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:bg-gray-100 disabled:text-gray-400"
25
+ >
26
+ Next
27
+ </button>
28
+ </div>
29
+ <div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
30
+ <div>
31
+ <p className="text-sm text-gray-700">
32
+ Page <span className="font-medium">{currentPage}</span> of{' '}
33
+ <span className="font-medium">{totalPages}</span>
34
+ </p>
35
+ </div>
36
+ <div>
37
+ <nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
38
+ <button
39
+ onClick={() => onPageChange(currentPage - 1)}
40
+ disabled={currentPage === 1}
41
+ className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:bg-gray-100 disabled:text-gray-400"
42
+ >
43
+ <span className="sr-only">Previous</span>
44
+ <ChevronLeft className="h-5 w-5" />
45
+ </button>
46
+ {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
47
+ <button
48
+ key={page}
49
+ onClick={() => onPageChange(page)}
50
+ className={`relative inline-flex items-center px-4 py-2 border text-sm font-medium ${
51
+ page === currentPage
52
+ ? 'z-10 bg-blue-50 border-blue-500 text-blue-600'
53
+ : 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50'
54
+ }`}
55
+ >
56
+ {page}
57
+ </button>
58
+ ))}
59
+ <button
60
+ onClick={() => onPageChange(currentPage + 1)}
61
+ disabled={currentPage === totalPages}
62
+ className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:bg-gray-100 disabled:text-gray-400"
63
+ >
64
+ <span className="sr-only">Next</span>
65
+ <ChevronRight className="h-5 w-5" />
66
+ </button>
67
+ </nav>
68
+ </div>
69
+ </div>
70
+ </div>
71
+ );
72
+ }
@@ -0,0 +1,55 @@
1
+ import React, { useState, useRef, useEffect } from "react";
2
+ import { X } from "lucide-react";
3
+
4
+ interface PopoverProps {
5
+ trigger: React.ReactElement;
6
+ children: React.ReactNode;
7
+ className?: string;
8
+ }
9
+
10
+ export function Popover({ trigger, children, className = "" }: PopoverProps) {
11
+ const [isOpen, setIsOpen] = useState(false);
12
+ const popoverRef = useRef<HTMLDivElement>(null);
13
+ const triggerRef = useRef<HTMLDivElement>(null);
14
+
15
+ useEffect(() => {
16
+ const handleClickOutside = (event: MouseEvent) => {
17
+ if (
18
+ popoverRef.current &&
19
+ !popoverRef.current.contains(event.target as Node) &&
20
+ !triggerRef.current?.contains(event.target as Node)
21
+ ) {
22
+ setIsOpen(false);
23
+ }
24
+ };
25
+
26
+ document.addEventListener("mousedown", handleClickOutside);
27
+ return () => document.removeEventListener("mousedown", handleClickOutside);
28
+ }, []);
29
+
30
+ return (
31
+ <div className="relative">
32
+ <div ref={triggerRef} onClick={() => setIsOpen(!isOpen)}>
33
+ {trigger}
34
+ </div>
35
+
36
+ {isOpen && (
37
+ <div
38
+ ref={popoverRef}
39
+ className={`absolute z-50 right-0 mt-2 bg-white rounded-lg shadow-lg border border-gray-200 ${className}`}
40
+ >
41
+ <div className="flex justify-between items-center p-3 border-b border-gray-200">
42
+ <h3 className="font-medium">Feature Configuration</h3>
43
+ <button
44
+ onClick={() => setIsOpen(false)}
45
+ className="text-gray-400 hover:text-gray-600"
46
+ >
47
+ <X className="w-4 h-4" />
48
+ </button>
49
+ </div>
50
+ <div className="p-4">{children}</div>
51
+ </div>
52
+ )}
53
+ </div>
54
+ );
55
+ }
@@ -0,0 +1,105 @@
1
+ import React, { useState } from 'react';
2
+ import { ChevronLeft, ChevronRight, Clock, CheckCircle2, XCircle } from 'lucide-react';
3
+ import type { Prediction } from '../types';
4
+
5
+ interface PredictionStreamProps {
6
+ predictions: Prediction[];
7
+ }
8
+
9
+ const ITEMS_PER_PAGE = 10;
10
+
11
+ export function PredictionStream({ predictions }: PredictionStreamProps) {
12
+ const [currentPage, setCurrentPage] = useState(1);
13
+ const totalPages = Math.ceil(predictions.length / ITEMS_PER_PAGE);
14
+
15
+ const paginatedPredictions = predictions.slice(
16
+ (currentPage - 1) * ITEMS_PER_PAGE,
17
+ currentPage * ITEMS_PER_PAGE
18
+ );
19
+
20
+ return (
21
+ <div className="bg-white rounded-lg shadow-lg p-6">
22
+ <div className="flex justify-between items-center mb-6">
23
+ <h3 className="text-lg font-semibold">Live Predictions</h3>
24
+ <div className="flex items-center gap-2">
25
+ <button
26
+ onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
27
+ disabled={currentPage === 1}
28
+ className="p-1 rounded-md hover:bg-gray-100 disabled:opacity-50"
29
+ >
30
+ <ChevronLeft className="w-5 h-5" />
31
+ </button>
32
+ <span className="text-sm text-gray-600">
33
+ Page {currentPage} of {totalPages}
34
+ </span>
35
+ <button
36
+ onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
37
+ disabled={currentPage === totalPages}
38
+ className="p-1 rounded-md hover:bg-gray-100 disabled:opacity-50"
39
+ >
40
+ <ChevronRight className="w-5 h-5" />
41
+ </button>
42
+ </div>
43
+ </div>
44
+
45
+ <div className="space-y-4">
46
+ {paginatedPredictions.map((prediction) => (
47
+ <div
48
+ key={prediction.id}
49
+ className="border border-gray-200 rounded-lg p-4 hover:border-gray-300 transition-colors"
50
+ >
51
+ <div className="flex justify-between items-start mb-3">
52
+ <div className="flex items-center gap-2">
53
+ <Clock className="w-4 h-4 text-gray-400" />
54
+ <span className="text-sm text-gray-500">
55
+ {new Date(prediction.timestamp).toLocaleString()}
56
+ </span>
57
+ </div>
58
+ <div className="flex items-center gap-2">
59
+ <span className="text-sm text-gray-500">
60
+ {prediction.latencyMs.toFixed(2)}ms
61
+ </span>
62
+ {prediction.groundTruth !== undefined && (
63
+ prediction.output === prediction.groundTruth ? (
64
+ <CheckCircle2 className="w-5 h-5 text-green-500" />
65
+ ) : (
66
+ <XCircle className="w-5 h-5 text-red-500" />
67
+ )
68
+ )}
69
+ </div>
70
+ </div>
71
+
72
+ <div className="grid md:grid-cols-2 gap-6">
73
+ <div>
74
+ <h4 className="text-sm font-medium text-gray-500 mb-2">Input</h4>
75
+ <div className="bg-gray-50 rounded-md p-3">
76
+ <pre className="text-sm whitespace-pre-wrap">
77
+ {JSON.stringify(prediction.input, null, 2)}
78
+ </pre>
79
+ </div>
80
+ </div>
81
+
82
+ <div>
83
+ <h4 className="text-sm font-medium text-gray-500 mb-2">Prediction</h4>
84
+ <div className="bg-gray-50 rounded-md p-3">
85
+ <div className="flex items-center justify-between">
86
+ <span className="font-medium">
87
+ {typeof prediction.output === 'boolean'
88
+ ? prediction.output ? 'Will Churn' : 'Will Not Churn'
89
+ : prediction.output}
90
+ </span>
91
+ {prediction.groundTruth !== undefined && (
92
+ <span className="text-sm text-gray-500">
93
+ Ground Truth: {prediction.groundTruth ? 'Churned' : 'Retained'}
94
+ </span>
95
+ )}
96
+ </div>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ ))}
102
+ </div>
103
+ </div>
104
+ );
105
+ }