flowyml 1.7.1__py3-none-any.whl → 1.7.2__py3-none-any.whl

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 (30) hide show
  1. flowyml/assets/dataset.py +570 -17
  2. flowyml/assets/model.py +1052 -15
  3. flowyml/core/executor.py +70 -11
  4. flowyml/core/orchestrator.py +37 -2
  5. flowyml/core/pipeline.py +32 -4
  6. flowyml/core/scheduler.py +88 -5
  7. flowyml/integrations/keras.py +247 -82
  8. flowyml/ui/backend/routers/runs.py +112 -0
  9. flowyml/ui/backend/routers/schedules.py +35 -15
  10. flowyml/ui/frontend/dist/assets/index-B40RsQDq.css +1 -0
  11. flowyml/ui/frontend/dist/assets/index-CjI0zKCn.js +685 -0
  12. flowyml/ui/frontend/dist/index.html +2 -2
  13. flowyml/ui/frontend/package-lock.json +11 -0
  14. flowyml/ui/frontend/package.json +1 -0
  15. flowyml/ui/frontend/src/app/assets/page.jsx +890 -321
  16. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +1 -1
  17. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +589 -101
  18. flowyml/ui/frontend/src/components/ArtifactViewer.jsx +62 -2
  19. flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +401 -28
  20. flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +119 -11
  21. flowyml/ui/frontend/src/components/DatasetViewer.jsx +753 -0
  22. flowyml/ui/frontend/src/components/TrainingHistoryChart.jsx +514 -0
  23. flowyml/ui/frontend/src/components/TrainingMetricsPanel.jsx +175 -0
  24. {flowyml-1.7.1.dist-info → flowyml-1.7.2.dist-info}/METADATA +1 -1
  25. {flowyml-1.7.1.dist-info → flowyml-1.7.2.dist-info}/RECORD +28 -25
  26. flowyml/ui/frontend/dist/assets/index-BqDQvp63.js +0 -630
  27. flowyml/ui/frontend/dist/assets/index-By4trVyv.css +0 -1
  28. {flowyml-1.7.1.dist-info → flowyml-1.7.2.dist-info}/WHEEL +0 -0
  29. {flowyml-1.7.1.dist-info → flowyml-1.7.2.dist-info}/entry_points.txt +0 -0
  30. {flowyml-1.7.1.dist-info → flowyml-1.7.2.dist-info}/licenses/LICENSE +0 -0
@@ -10,9 +10,13 @@ import {
10
10
  Code,
11
11
  Table as TableIcon,
12
12
  AlertCircle,
13
- Download
13
+ Download,
14
+ TrendingUp,
15
+ Database,
14
16
  } from 'lucide-react';
15
- import { Button } from './ui/Button'; // Assuming you have a Button component, or use native button if not
17
+ import { Button } from './ui/Button';
18
+ import { TrainingHistoryChart } from './TrainingHistoryChart';
19
+ import { DatasetViewer } from './DatasetViewer';
16
20
 
17
21
  SyntaxHighlighter.registerLanguage('json', json);
18
22
  SyntaxHighlighter.registerLanguage('python', python);
@@ -23,6 +27,17 @@ export function ArtifactViewer({ artifact }) {
23
27
  const [loading, setLoading] = useState(false);
24
28
  const [error, setError] = useState(null);
25
29
 
30
+ // Check if this is a Dataset artifact FIRST - before any async operations
31
+ // This ensures Dataset gets its own specialized viewer with histograms
32
+ const isDataset = artifact?.type?.toLowerCase() === 'dataset' ||
33
+ artifact?.asset_type?.toLowerCase() === 'dataset';
34
+
35
+ // Render Dataset viewer immediately if it's a dataset
36
+ // Don't wait for content fetch - dataset data is in properties
37
+ if (isDataset) {
38
+ return <DatasetViewer artifact={artifact} />;
39
+ }
40
+
26
41
  useEffect(() => {
27
42
  if (!artifact?.artifact_id) return;
28
43
  fetchContent();
@@ -110,6 +125,51 @@ export function ArtifactViewer({ artifact }) {
110
125
  );
111
126
  }
112
127
 
128
+ // Check if artifact has training history (from Keras callback)
129
+ const hasTrainingHistory = artifact?.training_history &&
130
+ artifact.training_history.epochs &&
131
+ artifact.training_history.epochs.length > 0;
132
+
133
+ // Render training history chart for model artifacts with training data
134
+ if (hasTrainingHistory) {
135
+ return (
136
+ <div className="space-y-6">
137
+ {/* Training History Visualization */}
138
+ <div>
139
+ <div className="flex items-center gap-2 mb-4 pb-3 border-b border-slate-200 dark:border-slate-700">
140
+ <TrendingUp className="text-primary-500" size={20} />
141
+ <h4 className="text-lg font-bold text-slate-900 dark:text-white">Training History</h4>
142
+ <span className="text-xs text-slate-500 bg-slate-100 dark:bg-slate-800 px-2 py-1 rounded-full">
143
+ {artifact.training_history.epochs.length} epochs
144
+ </span>
145
+ </div>
146
+ <TrainingHistoryChart trainingHistory={artifact.training_history} compact={false} />
147
+ </div>
148
+
149
+ {/* Also show JSON content if available */}
150
+ {content && contentType === 'json' && (
151
+ <div className="mt-6 pt-6 border-t border-slate-200 dark:border-slate-700">
152
+ <h5 className="text-sm font-semibold text-slate-600 dark:text-slate-400 mb-3 flex items-center gap-2">
153
+ <Code size={14} />
154
+ Raw Metadata
155
+ </h5>
156
+ <div className="rounded-lg overflow-hidden border border-slate-200 dark:border-slate-700 shadow-sm">
157
+ <div className="max-h-[30vh] overflow-y-auto bg-white dark:bg-slate-900">
158
+ <SyntaxHighlighter
159
+ language="json"
160
+ style={docco}
161
+ customStyle={{ margin: 0, padding: '1rem', fontSize: '0.75rem' }}
162
+ >
163
+ {JSON.stringify(content, null, 2)}
164
+ </SyntaxHighlighter>
165
+ </div>
166
+ </div>
167
+ </div>
168
+ )}
169
+ </div>
170
+ );
171
+ }
172
+
113
173
  if (contentType === 'image') {
114
174
  return (
115
175
  <div className="flex flex-col items-center bg-slate-50 dark:bg-slate-900 p-4 rounded-lg border border-slate-200 dark:border-slate-700">
@@ -14,7 +14,17 @@ import {
14
14
  Layers,
15
15
  HardDrive,
16
16
  ExternalLink,
17
- Info
17
+ Info,
18
+ Cpu,
19
+ Zap,
20
+ Settings,
21
+ Table,
22
+ PieChart,
23
+ TrendingUp,
24
+ Hash,
25
+ Gauge,
26
+ LineChart,
27
+ Eye
18
28
  } from 'lucide-react';
19
29
  import { Card } from './ui/Card';
20
30
  import { Badge } from './ui/Badge';
@@ -25,8 +35,10 @@ import { Link } from 'react-router-dom';
25
35
  import { motion } from 'framer-motion';
26
36
  import { ProjectSelector } from './ProjectSelector';
27
37
  import { fetchApi } from '../utils/api';
38
+ import { DatasetViewer } from './DatasetViewer';
39
+ import { TrainingHistoryChart } from './TrainingHistoryChart';
28
40
 
29
- export function AssetDetailsPanel({ asset, onClose }) {
41
+ export function AssetDetailsPanel({ asset, onClose, hideHeader = false }) {
30
42
  const [activeTab, setActiveTab] = useState('overview');
31
43
  const [currentProject, setCurrentProject] = useState(asset?.project);
32
44
 
@@ -98,43 +110,54 @@ export function AssetDetailsPanel({ asset, onClose }) {
98
110
  className="h-full flex flex-col"
99
111
  >
100
112
  <Card className="flex-1 flex flex-col overflow-hidden">
101
- {/* Header */}
102
- <div className={`p-6 ${config.bgColor} border-b ${config.borderColor}`}>
103
- <div className="flex items-start justify-between mb-4">
104
- <div className={`p-3 rounded-xl bg-gradient-to-br ${config.color} text-white shadow-lg`}>
105
- <Icon size={32} />
113
+ {/* Header - can be hidden when embedded */}
114
+ {!hideHeader && (
115
+ <div className={`p-6 ${config.bgColor} border-b ${config.borderColor}`}>
116
+ <div className="flex items-start justify-between mb-4">
117
+ <div className={`p-3 rounded-xl bg-gradient-to-br ${config.color} text-white shadow-lg`}>
118
+ <Icon size={32} />
119
+ </div>
120
+ <button
121
+ onClick={onClose}
122
+ className="p-2 hover:bg-white/50 dark:hover:bg-slate-800/50 rounded-lg transition-colors"
123
+ >
124
+ <X size={20} className="text-slate-400" />
125
+ </button>
106
126
  </div>
107
- <button
108
- onClick={onClose}
109
- className="p-2 hover:bg-white/50 dark:hover:bg-slate-800/50 rounded-lg transition-colors"
110
- >
111
- <X size={20} className="text-slate-400" />
112
- </button>
113
- </div>
114
127
 
115
- <h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">
116
- {asset.name}
117
- </h2>
118
-
119
- <div className="flex items-center gap-2 flex-wrap">
120
- <Badge className={`bg-gradient-to-r ${config.color} text-white border-0`}>
121
- {asset.type}
122
- </Badge>
123
- <ProjectSelector
124
- currentProject={currentProject}
125
- onUpdate={handleProjectUpdate}
126
- />
128
+ <h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">
129
+ {asset.name}
130
+ </h2>
131
+
132
+ <div className="flex items-center gap-2 flex-wrap">
133
+ <Badge className={`bg-gradient-to-r ${config.color} text-white border-0`}>
134
+ {asset.type}
135
+ </Badge>
136
+ <ProjectSelector
137
+ currentProject={currentProject}
138
+ onUpdate={handleProjectUpdate}
139
+ />
140
+ </div>
127
141
  </div>
128
- </div>
142
+ )}
129
143
 
130
144
  {/* Tabs */}
131
- <div className="flex items-center gap-1 p-2 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900">
145
+ <div className="flex items-center gap-1 p-2 border-b border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 overflow-x-auto">
132
146
  <TabButton
133
147
  active={activeTab === 'overview'}
134
148
  onClick={() => setActiveTab('overview')}
135
149
  icon={Info}
136
150
  label="Overview"
137
151
  />
152
+ {/* Show Visualization tab for Datasets and Models */}
153
+ {(asset.type?.toLowerCase() === 'dataset' || asset.type?.toLowerCase() === 'model') && (
154
+ <TabButton
155
+ active={activeTab === 'visualization'}
156
+ onClick={() => setActiveTab('visualization')}
157
+ icon={asset.type?.toLowerCase() === 'model' ? LineChart : BarChart2}
158
+ label={asset.type?.toLowerCase() === 'model' ? 'Training' : 'Statistics'}
159
+ />
160
+ )}
138
161
  <TabButton
139
162
  active={activeTab === 'properties'}
140
163
  onClick={() => setActiveTab('properties')}
@@ -151,8 +174,38 @@ export function AssetDetailsPanel({ asset, onClose }) {
151
174
 
152
175
  {/* Content */}
153
176
  <div className="flex-1 overflow-y-auto p-6 space-y-6">
177
+ {/* Visualization Tab - Rich interactive visualizations */}
178
+ {activeTab === 'visualization' && (
179
+ <div className="space-y-6">
180
+ {/* Dataset Visualization with full DatasetViewer */}
181
+ {asset.type?.toLowerCase() === 'dataset' && (
182
+ <DatasetViewer artifact={asset} />
183
+ )}
184
+
185
+ {/* Model Visualization with Training History */}
186
+ {asset.type?.toLowerCase() === 'model' && (
187
+ <ModelVisualization asset={asset} />
188
+ )}
189
+ </div>
190
+ )}
191
+
154
192
  {activeTab === 'overview' && (
155
193
  <>
194
+ {/* Model-specific Auto-extracted Info */}
195
+ {asset.type?.toLowerCase() === 'model' && asset.properties && (
196
+ <ModelOverview properties={asset.properties} />
197
+ )}
198
+
199
+ {/* Dataset-specific Auto-extracted Info */}
200
+ {asset.type?.toLowerCase() === 'dataset' && asset.properties && (
201
+ <DatasetOverview properties={asset.properties} />
202
+ )}
203
+
204
+ {/* Metrics-specific Info */}
205
+ {asset.type?.toLowerCase() === 'metrics' && asset.properties && (
206
+ <MetricsOverview properties={asset.properties} />
207
+ )}
208
+
156
209
  {/* Key Information */}
157
210
  <div>
158
211
  <h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300 uppercase tracking-wider mb-3 flex items-center gap-2">
@@ -371,3 +424,323 @@ function TabButton({ active, onClick, icon: Icon, label }) {
371
424
  </button>
372
425
  );
373
426
  }
427
+
428
+ // Model-specific overview with auto-extracted metadata
429
+ function ModelOverview({ properties }) {
430
+ const framework = properties.framework;
431
+ const parameters = properties.parameters;
432
+ const trainableParams = properties.trainable_parameters;
433
+ const optimizer = properties.optimizer;
434
+ const learningRate = properties.learning_rate;
435
+ const lossFunction = properties.loss_function;
436
+ const numLayers = properties.num_layers;
437
+ const layerTypes = properties.layer_types;
438
+ const architecture = properties.architecture || properties.model_class;
439
+ const isAutoExtracted = properties._auto_extracted;
440
+
441
+ if (!framework && !parameters) return null;
442
+
443
+ const formatNumber = (num) => {
444
+ if (!num) return 'N/A';
445
+ if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
446
+ if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
447
+ return num.toLocaleString();
448
+ };
449
+
450
+ return (
451
+ <div className="space-y-4">
452
+ <div className="flex items-center justify-between">
453
+ <h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300 uppercase tracking-wider flex items-center gap-2">
454
+ <Cpu size={16} className="text-purple-500" />
455
+ Model Details
456
+ </h3>
457
+ {isAutoExtracted && (
458
+ <span className="text-[10px] bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400 px-2 py-0.5 rounded-full">
459
+ Auto-extracted
460
+ </span>
461
+ )}
462
+ </div>
463
+
464
+ {/* Framework & Architecture */}
465
+ <div className="grid grid-cols-2 gap-3">
466
+ {framework && (
467
+ <div className="bg-gradient-to-br from-purple-50 to-pink-50 dark:from-purple-900/20 dark:to-pink-900/20 rounded-lg p-3 border border-purple-100 dark:border-purple-800">
468
+ <div className="flex items-center gap-2 mb-1">
469
+ <Zap size={12} className="text-purple-500" />
470
+ <span className="text-[10px] text-purple-600 dark:text-purple-400 font-medium uppercase">Framework</span>
471
+ </div>
472
+ <p className="text-lg font-bold text-purple-700 dark:text-purple-300 capitalize">{framework}</p>
473
+ </div>
474
+ )}
475
+ {architecture && (
476
+ <div className="bg-slate-50 dark:bg-slate-800/50 rounded-lg p-3 border border-slate-200 dark:border-slate-700">
477
+ <div className="flex items-center gap-2 mb-1">
478
+ <Settings size={12} className="text-slate-500" />
479
+ <span className="text-[10px] text-slate-500 font-medium uppercase">Architecture</span>
480
+ </div>
481
+ <p className="text-sm font-semibold text-slate-900 dark:text-white truncate">{architecture}</p>
482
+ </div>
483
+ )}
484
+ </div>
485
+
486
+ {/* Parameters */}
487
+ {parameters && (
488
+ <div className="grid grid-cols-2 gap-3">
489
+ <div className="bg-slate-50 dark:bg-slate-800/50 rounded-lg p-3 border border-slate-200 dark:border-slate-700">
490
+ <div className="flex items-center gap-2 mb-1">
491
+ <Hash size={12} className="text-slate-500" />
492
+ <span className="text-[10px] text-slate-500 font-medium uppercase">Parameters</span>
493
+ </div>
494
+ <p className="text-xl font-bold text-slate-900 dark:text-white">{formatNumber(parameters)}</p>
495
+ </div>
496
+ {trainableParams && (
497
+ <div className="bg-slate-50 dark:bg-slate-800/50 rounded-lg p-3 border border-slate-200 dark:border-slate-700">
498
+ <div className="flex items-center gap-2 mb-1">
499
+ <TrendingUp size={12} className="text-slate-500" />
500
+ <span className="text-[10px] text-slate-500 font-medium uppercase">Trainable</span>
501
+ </div>
502
+ <p className="text-xl font-bold text-slate-900 dark:text-white">{formatNumber(trainableParams)}</p>
503
+ </div>
504
+ )}
505
+ </div>
506
+ )}
507
+
508
+ {/* Training Config */}
509
+ {(optimizer || lossFunction || learningRate) && (
510
+ <div className="bg-blue-50 dark:bg-blue-900/20 rounded-lg p-3 border border-blue-100 dark:border-blue-800">
511
+ <h4 className="text-[10px] text-blue-600 dark:text-blue-400 font-medium uppercase mb-2">Training Configuration</h4>
512
+ <div className="grid grid-cols-3 gap-2 text-xs">
513
+ {optimizer && (
514
+ <div>
515
+ <span className="text-blue-500">Optimizer:</span>
516
+ <span className="ml-1 font-medium text-blue-700 dark:text-blue-300">{optimizer}</span>
517
+ </div>
518
+ )}
519
+ {learningRate && (
520
+ <div>
521
+ <span className="text-blue-500">LR:</span>
522
+ <span className="ml-1 font-medium text-blue-700 dark:text-blue-300">
523
+ {typeof learningRate === 'number' ? learningRate.toExponential(2) : learningRate}
524
+ </span>
525
+ </div>
526
+ )}
527
+ {lossFunction && (
528
+ <div>
529
+ <span className="text-blue-500">Loss:</span>
530
+ <span className="ml-1 font-medium text-blue-700 dark:text-blue-300">{lossFunction}</span>
531
+ </div>
532
+ )}
533
+ </div>
534
+ </div>
535
+ )}
536
+
537
+ {/* Layers */}
538
+ {(numLayers || layerTypes) && (
539
+ <div className="bg-slate-50 dark:bg-slate-800/50 rounded-lg p-3 border border-slate-200 dark:border-slate-700">
540
+ <div className="flex items-center justify-between mb-2">
541
+ <h4 className="text-[10px] text-slate-500 font-medium uppercase">Layers</h4>
542
+ {numLayers && <span className="text-sm font-bold text-slate-900 dark:text-white">{numLayers}</span>}
543
+ </div>
544
+ {layerTypes && (
545
+ <div className="flex flex-wrap gap-1">
546
+ {(Array.isArray(layerTypes) ? layerTypes : JSON.parse(layerTypes || '[]')).map((type, i) => (
547
+ <span key={i} className="text-[10px] bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-slate-300 px-2 py-0.5 rounded">
548
+ {type}
549
+ </span>
550
+ ))}
551
+ </div>
552
+ )}
553
+ </div>
554
+ )}
555
+ </div>
556
+ );
557
+ }
558
+
559
+ // Dataset-specific overview with auto-extracted metadata
560
+ function DatasetOverview({ properties }) {
561
+ const samples = properties.samples || properties.num_samples;
562
+ const features = properties.num_features;
563
+ const columns = properties.columns || properties.feature_columns;
564
+ const framework = properties.framework;
565
+ const labelColumn = properties.label_column;
566
+ const columnStats = properties.column_stats;
567
+ const isAutoExtracted = properties._auto_extracted;
568
+
569
+ if (!samples && !features && !columns) return null;
570
+
571
+ return (
572
+ <div className="space-y-4">
573
+ <div className="flex items-center justify-between">
574
+ <h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300 uppercase tracking-wider flex items-center gap-2">
575
+ <Database size={16} className="text-blue-500" />
576
+ Dataset Details
577
+ </h3>
578
+ {isAutoExtracted && (
579
+ <span className="text-[10px] bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 px-2 py-0.5 rounded-full">
580
+ Auto-extracted
581
+ </span>
582
+ )}
583
+ </div>
584
+
585
+ {/* Key Stats */}
586
+ <div className="grid grid-cols-3 gap-3">
587
+ {samples && (
588
+ <div className="bg-gradient-to-br from-blue-50 to-cyan-50 dark:from-blue-900/20 dark:to-cyan-900/20 rounded-lg p-3 border border-blue-100 dark:border-blue-800">
589
+ <div className="flex items-center gap-2 mb-1">
590
+ <Table size={12} className="text-blue-500" />
591
+ <span className="text-[10px] text-blue-600 dark:text-blue-400 font-medium uppercase">Samples</span>
592
+ </div>
593
+ <p className="text-xl font-bold text-blue-700 dark:text-blue-300">{samples.toLocaleString()}</p>
594
+ </div>
595
+ )}
596
+ {features && (
597
+ <div className="bg-slate-50 dark:bg-slate-800/50 rounded-lg p-3 border border-slate-200 dark:border-slate-700">
598
+ <div className="flex items-center gap-2 mb-1">
599
+ <PieChart size={12} className="text-slate-500" />
600
+ <span className="text-[10px] text-slate-500 font-medium uppercase">Features</span>
601
+ </div>
602
+ <p className="text-xl font-bold text-slate-900 dark:text-white">{features}</p>
603
+ </div>
604
+ )}
605
+ {framework && (
606
+ <div className="bg-slate-50 dark:bg-slate-800/50 rounded-lg p-3 border border-slate-200 dark:border-slate-700">
607
+ <div className="flex items-center gap-2 mb-1">
608
+ <Zap size={12} className="text-slate-500" />
609
+ <span className="text-[10px] text-slate-500 font-medium uppercase">Format</span>
610
+ </div>
611
+ <p className="text-sm font-bold text-slate-900 dark:text-white capitalize">{framework}</p>
612
+ </div>
613
+ )}
614
+ </div>
615
+
616
+ {/* Columns */}
617
+ {columns && (
618
+ <div className="bg-slate-50 dark:bg-slate-800/50 rounded-lg p-3 border border-slate-200 dark:border-slate-700">
619
+ <div className="flex items-center justify-between mb-2">
620
+ <h4 className="text-[10px] text-slate-500 font-medium uppercase">Columns</h4>
621
+ <span className="text-xs text-slate-400">{Array.isArray(columns) ? columns.length : 0} total</span>
622
+ </div>
623
+ <div className="flex flex-wrap gap-1">
624
+ {(Array.isArray(columns) ? columns : []).slice(0, 10).map((col, i) => (
625
+ <span
626
+ key={i}
627
+ className={`text-[10px] px-2 py-0.5 rounded ${
628
+ col === labelColumn
629
+ ? 'bg-emerald-100 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400 font-medium'
630
+ : 'bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-slate-300'
631
+ }`}
632
+ >
633
+ {col}{col === labelColumn && ' (target)'}
634
+ </span>
635
+ ))}
636
+ {Array.isArray(columns) && columns.length > 10 && (
637
+ <span className="text-[10px] text-slate-400">+{columns.length - 10} more</span>
638
+ )}
639
+ </div>
640
+ </div>
641
+ )}
642
+
643
+ {/* Column Stats Preview */}
644
+ {columnStats && Object.keys(columnStats).length > 0 && (
645
+ <div className="bg-emerald-50 dark:bg-emerald-900/20 rounded-lg p-3 border border-emerald-100 dark:border-emerald-800">
646
+ <h4 className="text-[10px] text-emerald-600 dark:text-emerald-400 font-medium uppercase mb-2">Column Statistics Available</h4>
647
+ <p className="text-xs text-emerald-700 dark:text-emerald-300">
648
+ {Object.keys(columnStats).length} columns with statistics (mean, std, min, max, etc.)
649
+ </p>
650
+ </div>
651
+ )}
652
+ </div>
653
+ );
654
+ }
655
+
656
+ // Metrics-specific overview
657
+ function MetricsOverview({ properties }) {
658
+ // Filter out internal properties
659
+ const metricEntries = Object.entries(properties).filter(([key]) =>
660
+ !key.startsWith('_') && !['data', 'samples', 'source'].includes(key)
661
+ );
662
+
663
+ if (metricEntries.length === 0) return null;
664
+
665
+ const formatValue = (value) => {
666
+ if (typeof value === 'number') {
667
+ if (value < 0.01 && value !== 0) return value.toExponential(3);
668
+ if (value > 1000) return value.toLocaleString();
669
+ return value.toFixed(4);
670
+ }
671
+ return String(value);
672
+ };
673
+
674
+ return (
675
+ <div className="space-y-4">
676
+ <h3 className="text-sm font-semibold text-slate-700 dark:text-slate-300 uppercase tracking-wider flex items-center gap-2">
677
+ <Gauge size={16} className="text-emerald-500" />
678
+ Evaluation Metrics
679
+ </h3>
680
+
681
+ <div className="grid grid-cols-2 gap-3">
682
+ {metricEntries.slice(0, 8).map(([key, value]) => (
683
+ <div
684
+ key={key}
685
+ className="bg-gradient-to-br from-emerald-50 to-teal-50 dark:from-emerald-900/20 dark:to-teal-900/20 rounded-lg p-3 border border-emerald-100 dark:border-emerald-800"
686
+ >
687
+ <span className="text-[10px] text-emerald-600 dark:text-emerald-400 font-medium uppercase block mb-1">
688
+ {key.replace(/_/g, ' ')}
689
+ </span>
690
+ <p className="text-lg font-bold text-emerald-700 dark:text-emerald-300 font-mono">
691
+ {formatValue(value)}
692
+ </p>
693
+ </div>
694
+ ))}
695
+ </div>
696
+ </div>
697
+ );
698
+ }
699
+
700
+ // Model Visualization with Training History
701
+ function ModelVisualization({ asset }) {
702
+ // Check for training history in different locations
703
+ const trainingHistory = asset.training_history ||
704
+ asset.properties?.training_history ||
705
+ null;
706
+
707
+ const hasTrainingHistory = trainingHistory &&
708
+ trainingHistory.epochs &&
709
+ trainingHistory.epochs.length > 0;
710
+
711
+ if (!hasTrainingHistory) {
712
+ return (
713
+ <div className="text-center py-12 bg-slate-50 dark:bg-slate-800/50 rounded-xl border border-slate-200 dark:border-slate-700">
714
+ <div className="inline-flex p-4 bg-white dark:bg-slate-800 rounded-full mb-4 shadow-sm">
715
+ <LineChart size={32} className="text-slate-400" />
716
+ </div>
717
+ <h4 className="text-lg font-bold text-slate-900 dark:text-white mb-2">No Training History Available</h4>
718
+ <p className="text-sm text-slate-500 dark:text-slate-400 max-w-md mx-auto">
719
+ This model artifact doesn't have training history data.
720
+ <br />
721
+ Use <code className="bg-slate-100 dark:bg-slate-700 px-1.5 py-0.5 rounded text-xs">FlowymlKerasCallback</code> during training to capture metrics.
722
+ </p>
723
+ </div>
724
+ );
725
+ }
726
+
727
+ return (
728
+ <div className="space-y-6">
729
+ {/* Header */}
730
+ <div className="flex items-center gap-3 pb-4 border-b border-slate-200 dark:border-slate-700">
731
+ <div className="p-3 bg-gradient-to-br from-purple-500 to-pink-600 rounded-xl shadow-lg">
732
+ <LineChart size={24} className="text-white" />
733
+ </div>
734
+ <div>
735
+ <h3 className="text-xl font-bold text-slate-900 dark:text-white">Training History</h3>
736
+ <p className="text-sm text-slate-500 dark:text-slate-400">
737
+ {trainingHistory.epochs.length} epochs recorded
738
+ </p>
739
+ </div>
740
+ </div>
741
+
742
+ {/* Training History Chart */}
743
+ <TrainingHistoryChart trainingHistory={trainingHistory} compact={false} />
744
+ </div>
745
+ );
746
+ }