flowyml 1.7.1__py3-none-any.whl → 1.8.0__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.
- flowyml/assets/base.py +15 -0
- flowyml/assets/dataset.py +570 -17
- flowyml/assets/metrics.py +5 -0
- flowyml/assets/model.py +1052 -15
- flowyml/cli/main.py +709 -0
- flowyml/cli/stack_cli.py +138 -25
- flowyml/core/__init__.py +17 -0
- flowyml/core/executor.py +231 -37
- flowyml/core/image_builder.py +129 -0
- flowyml/core/log_streamer.py +227 -0
- flowyml/core/orchestrator.py +59 -4
- flowyml/core/pipeline.py +65 -13
- flowyml/core/routing.py +558 -0
- flowyml/core/scheduler.py +88 -5
- flowyml/core/step.py +9 -1
- flowyml/core/step_grouping.py +49 -35
- flowyml/core/types.py +407 -0
- flowyml/integrations/keras.py +247 -82
- flowyml/monitoring/alerts.py +10 -0
- flowyml/monitoring/notifications.py +104 -25
- flowyml/monitoring/slack_blocks.py +323 -0
- flowyml/plugins/__init__.py +251 -0
- flowyml/plugins/alerters/__init__.py +1 -0
- flowyml/plugins/alerters/slack.py +168 -0
- flowyml/plugins/base.py +752 -0
- flowyml/plugins/config.py +478 -0
- flowyml/plugins/deployers/__init__.py +22 -0
- flowyml/plugins/deployers/gcp_cloud_run.py +200 -0
- flowyml/plugins/deployers/sagemaker.py +306 -0
- flowyml/plugins/deployers/vertex.py +290 -0
- flowyml/plugins/integration.py +369 -0
- flowyml/plugins/manager.py +510 -0
- flowyml/plugins/model_registries/__init__.py +22 -0
- flowyml/plugins/model_registries/mlflow.py +159 -0
- flowyml/plugins/model_registries/sagemaker.py +489 -0
- flowyml/plugins/model_registries/vertex.py +386 -0
- flowyml/plugins/orchestrators/__init__.py +13 -0
- flowyml/plugins/orchestrators/sagemaker.py +443 -0
- flowyml/plugins/orchestrators/vertex_ai.py +461 -0
- flowyml/plugins/registries/__init__.py +13 -0
- flowyml/plugins/registries/ecr.py +321 -0
- flowyml/plugins/registries/gcr.py +313 -0
- flowyml/plugins/registry.py +454 -0
- flowyml/plugins/stack.py +494 -0
- flowyml/plugins/stack_config.py +537 -0
- flowyml/plugins/stores/__init__.py +13 -0
- flowyml/plugins/stores/gcs.py +460 -0
- flowyml/plugins/stores/s3.py +453 -0
- flowyml/plugins/trackers/__init__.py +11 -0
- flowyml/plugins/trackers/mlflow.py +316 -0
- flowyml/plugins/validators/__init__.py +3 -0
- flowyml/plugins/validators/deepchecks.py +119 -0
- flowyml/registry/__init__.py +2 -1
- flowyml/registry/model_environment.py +109 -0
- flowyml/registry/model_registry.py +241 -96
- flowyml/serving/__init__.py +17 -0
- flowyml/serving/model_server.py +628 -0
- flowyml/stacks/__init__.py +60 -0
- flowyml/stacks/aws.py +93 -0
- flowyml/stacks/base.py +62 -0
- flowyml/stacks/components.py +12 -0
- flowyml/stacks/gcp.py +44 -9
- flowyml/stacks/plugins.py +115 -0
- flowyml/stacks/registry.py +2 -1
- flowyml/storage/sql.py +401 -12
- flowyml/tracking/experiment.py +8 -5
- flowyml/ui/backend/Dockerfile +87 -16
- flowyml/ui/backend/auth.py +12 -2
- flowyml/ui/backend/main.py +149 -5
- flowyml/ui/backend/routers/ai_context.py +226 -0
- flowyml/ui/backend/routers/assets.py +23 -4
- flowyml/ui/backend/routers/auth.py +96 -0
- flowyml/ui/backend/routers/deployments.py +660 -0
- flowyml/ui/backend/routers/model_explorer.py +597 -0
- flowyml/ui/backend/routers/plugins.py +103 -51
- flowyml/ui/backend/routers/projects.py +91 -8
- flowyml/ui/backend/routers/runs.py +132 -1
- flowyml/ui/backend/routers/schedules.py +54 -29
- flowyml/ui/backend/routers/templates.py +319 -0
- flowyml/ui/backend/routers/websocket.py +2 -2
- flowyml/ui/frontend/Dockerfile +55 -6
- flowyml/ui/frontend/dist/assets/index-B5AsPTSz.css +1 -0
- flowyml/ui/frontend/dist/assets/index-dFbZ8wD8.js +753 -0
- flowyml/ui/frontend/dist/index.html +2 -2
- flowyml/ui/frontend/dist/logo.png +0 -0
- flowyml/ui/frontend/nginx.conf +65 -4
- flowyml/ui/frontend/package-lock.json +1415 -74
- flowyml/ui/frontend/package.json +4 -0
- flowyml/ui/frontend/public/logo.png +0 -0
- flowyml/ui/frontend/src/App.jsx +10 -7
- flowyml/ui/frontend/src/app/assets/page.jsx +890 -321
- flowyml/ui/frontend/src/app/auth/Login.jsx +90 -0
- flowyml/ui/frontend/src/app/dashboard/page.jsx +8 -8
- flowyml/ui/frontend/src/app/deployments/page.jsx +786 -0
- flowyml/ui/frontend/src/app/model-explorer/page.jsx +1031 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +12 -2
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +19 -6
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +1 -1
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +601 -101
- flowyml/ui/frontend/src/app/runs/page.jsx +8 -2
- flowyml/ui/frontend/src/app/settings/page.jsx +267 -253
- flowyml/ui/frontend/src/components/ArtifactViewer.jsx +62 -2
- flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +424 -29
- flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +119 -11
- flowyml/ui/frontend/src/components/DatasetViewer.jsx +753 -0
- flowyml/ui/frontend/src/components/Layout.jsx +6 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +79 -29
- flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +36 -6
- flowyml/ui/frontend/src/components/RunMetaPanel.jsx +113 -0
- flowyml/ui/frontend/src/components/TrainingHistoryChart.jsx +514 -0
- flowyml/ui/frontend/src/components/TrainingMetricsPanel.jsx +175 -0
- flowyml/ui/frontend/src/components/ai/AIAssistantButton.jsx +71 -0
- flowyml/ui/frontend/src/components/ai/AIAssistantPanel.jsx +420 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +22 -0
- flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +4 -4
- flowyml/ui/frontend/src/components/plugins/{ZenMLIntegration.jsx → StackImport.jsx} +38 -12
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +36 -13
- flowyml/ui/frontend/src/contexts/AIAssistantContext.jsx +245 -0
- flowyml/ui/frontend/src/contexts/AuthContext.jsx +108 -0
- flowyml/ui/frontend/src/hooks/useAIContext.js +156 -0
- flowyml/ui/frontend/src/hooks/useWebGPU.js +54 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +6 -0
- flowyml/ui/frontend/src/router/index.jsx +47 -20
- flowyml/ui/frontend/src/services/pluginService.js +3 -1
- flowyml/ui/server_manager.py +5 -5
- flowyml/ui/utils.py +157 -39
- flowyml/utils/config.py +37 -15
- flowyml/utils/model_introspection.py +123 -0
- flowyml/utils/observability.py +30 -0
- flowyml-1.8.0.dist-info/METADATA +174 -0
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/RECORD +134 -73
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/WHEEL +1 -1
- flowyml/ui/frontend/dist/assets/index-BqDQvp63.js +0 -630
- flowyml/ui/frontend/dist/assets/index-By4trVyv.css +0 -1
- flowyml-1.7.1.dist-info/METADATA +0 -477
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/entry_points.txt +0 -0
- {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,7 +2,7 @@ import React, { useEffect, useState, useMemo } from 'react';
|
|
|
2
2
|
import { fetchApi } from '../../../utils/api';
|
|
3
3
|
import { downloadArtifactById } from '../../../utils/downloads';
|
|
4
4
|
import { useParams, Link } from 'react-router-dom';
|
|
5
|
-
import { CheckCircle, XCircle, Clock, Calendar, Package, ArrowRight, BarChart2, FileText, Database, Box, ChevronRight, Activity, Layers, Code2, Terminal, Info, X, Maximize2, TrendingUp, Download, ArrowDownCircle, ArrowUpCircle, Tag, Zap, AlertCircle, FolderPlus, Cloud, Server } from 'lucide-react';
|
|
5
|
+
import { CheckCircle, XCircle, Clock, Calendar, Package, ArrowRight, BarChart2, FileText, Database, Box, ChevronRight, Activity, Layers, Code2, Terminal, Info, X, Maximize2, TrendingUp, TrendingDown, Download, ArrowDownCircle, ArrowUpCircle, Tag, Zap, AlertCircle, FolderPlus, Cloud, Server, LineChart, Minimize2 } from 'lucide-react';
|
|
6
6
|
import { Card } from '../../../components/ui/Card';
|
|
7
7
|
import { Badge } from '../../../components/ui/Badge';
|
|
8
8
|
import { Button } from '../../../components/ui/Button';
|
|
@@ -12,6 +12,10 @@ import { ArtifactViewer } from '../../../components/ArtifactViewer';
|
|
|
12
12
|
import { PipelineGraph } from '../../../components/PipelineGraph';
|
|
13
13
|
import { ProjectSelector } from '../../../components/ProjectSelector';
|
|
14
14
|
import { CodeSnippet } from '../../../components/ui/CodeSnippet';
|
|
15
|
+
import { TrainingMetricsPanel } from '../../../components/TrainingMetricsPanel';
|
|
16
|
+
import { TrainingHistoryChart } from '../../../components/TrainingHistoryChart';
|
|
17
|
+
import { RunMetaPanel } from '../../../components/RunMetaPanel';
|
|
18
|
+
import { useAIContext } from '../../../hooks/useAIContext';
|
|
15
19
|
|
|
16
20
|
export function RunDetails() {
|
|
17
21
|
const { runId } = useParams();
|
|
@@ -26,6 +30,17 @@ export function RunDetails() {
|
|
|
26
30
|
const [isPolling, setIsPolling] = useState(false);
|
|
27
31
|
const [stopping, setStopping] = useState(false);
|
|
28
32
|
const [stepLogs, setStepLogs] = useState({});
|
|
33
|
+
const [stepPanelExpanded, setStepPanelExpanded] = useState(false);
|
|
34
|
+
const [hasTrainingHistory, setHasTrainingHistory] = useState(false);
|
|
35
|
+
|
|
36
|
+
// Share run context with AI assistant
|
|
37
|
+
useAIContext({
|
|
38
|
+
pageType: 'run',
|
|
39
|
+
resourceId: runId,
|
|
40
|
+
includeLogs: true,
|
|
41
|
+
includeCode: true,
|
|
42
|
+
includeMetrics: true
|
|
43
|
+
});
|
|
29
44
|
|
|
30
45
|
const handleStopRun = async () => {
|
|
31
46
|
if (!confirm('Are you sure you want to stop this run?')) return;
|
|
@@ -227,30 +242,74 @@ export function RunDetails() {
|
|
|
227
242
|
/>
|
|
228
243
|
</div>
|
|
229
244
|
|
|
230
|
-
{/*
|
|
231
|
-
<
|
|
232
|
-
|
|
233
|
-
|
|
245
|
+
{/* Environment & Metadata Panel */}
|
|
246
|
+
<RunMetaPanel run={run} />
|
|
247
|
+
|
|
248
|
+
{/* Main Content - Dynamic Split View */}
|
|
249
|
+
|
|
250
|
+
{/* Main Content - Dynamic Split View */}
|
|
251
|
+
<div className={`grid gap-6 transition-all duration-300 ${stepPanelExpanded
|
|
252
|
+
? 'grid-cols-1 lg:grid-cols-2'
|
|
253
|
+
: 'grid-cols-1 lg:grid-cols-3'
|
|
254
|
+
}`}>
|
|
255
|
+
{/* DAG Visualization */}
|
|
256
|
+
<div className={stepPanelExpanded ? 'lg:col-span-1' : 'lg:col-span-2'}>
|
|
234
257
|
<h3 className="text-xl font-bold text-slate-900 dark:text-white mb-4 flex items-center gap-2">
|
|
235
258
|
<Activity className="text-primary-500" /> Pipeline Execution Graph
|
|
236
259
|
</h3>
|
|
237
|
-
<div className=
|
|
260
|
+
<div className={`min-h-[500px] ${stepPanelExpanded ? 'h-[500px]' : 'h-[calc(100vh-240px)]'}`}>
|
|
238
261
|
{run.dag ? (
|
|
239
262
|
<PipelineGraph
|
|
240
263
|
dag={run.dag}
|
|
241
264
|
steps={run.steps}
|
|
242
265
|
selectedStep={selectedStep}
|
|
243
266
|
onStepSelect={setSelectedStep}
|
|
244
|
-
onArtifactSelect={(
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
|
|
267
|
+
onArtifactSelect={(artifactLabel) => {
|
|
268
|
+
// Smart artifact matching:
|
|
269
|
+
// DAG labels are like "data/train", "model/trained"
|
|
270
|
+
// API names are like "train_dataset", "trained_model"
|
|
271
|
+
|
|
272
|
+
// 1. Exact name match
|
|
273
|
+
let found = artifacts.find(a => a.name === artifactLabel);
|
|
274
|
+
|
|
275
|
+
if (!found) {
|
|
276
|
+
// 2. Match by output pattern
|
|
277
|
+
// "data/train" -> look for artifacts with "train" and type Dataset
|
|
278
|
+
// "model/trained" -> look for artifacts with "trained" and type Model
|
|
279
|
+
const parts = artifactLabel.split('/');
|
|
280
|
+
const prefix = parts[0]; // "data", "model", "metrics"
|
|
281
|
+
const suffix = parts[1]; // "train", "trained", "evaluation"
|
|
282
|
+
|
|
283
|
+
const typeMap = {
|
|
284
|
+
'data': 'Dataset',
|
|
285
|
+
'model': 'Model',
|
|
286
|
+
'metrics': 'Metrics',
|
|
287
|
+
'features': 'FeatureSet',
|
|
288
|
+
};
|
|
289
|
+
const expectedType = typeMap[prefix];
|
|
290
|
+
|
|
291
|
+
// Look for artifacts with matching type and suffix in name
|
|
292
|
+
if (suffix && expectedType) {
|
|
293
|
+
found = artifacts.find(a =>
|
|
294
|
+
a.type === expectedType &&
|
|
295
|
+
(a.name.toLowerCase().includes(suffix.toLowerCase()) ||
|
|
296
|
+
suffix.toLowerCase().includes(a.name.toLowerCase().replace(/_/g, '')))
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// 3. Fallback: match by type alone if only one of that type
|
|
301
|
+
if (!found && expectedType) {
|
|
302
|
+
const typeMatches = artifacts.filter(a => a.type === expectedType);
|
|
303
|
+
if (typeMatches.length === 1) {
|
|
304
|
+
found = typeMatches[0];
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
248
309
|
if (found) {
|
|
249
310
|
setSelectedArtifact(found);
|
|
250
311
|
} else {
|
|
251
|
-
|
|
252
|
-
console.warn(`Artifact ${name} not found in assets list`);
|
|
253
|
-
// Optionally show a toast or alert
|
|
312
|
+
console.warn(`Artifact "${artifactLabel}" not found in assets list. Available:`, artifacts.map(a => a.name));
|
|
254
313
|
}
|
|
255
314
|
}}
|
|
256
315
|
/>
|
|
@@ -262,18 +321,30 @@ export function RunDetails() {
|
|
|
262
321
|
</div>
|
|
263
322
|
</div>
|
|
264
323
|
|
|
265
|
-
{/* Step Details Panel -
|
|
266
|
-
<div>
|
|
267
|
-
<
|
|
268
|
-
<
|
|
269
|
-
|
|
324
|
+
{/* Step Details Panel - Expandable */}
|
|
325
|
+
<div className={stepPanelExpanded ? 'lg:col-span-1' : ''}>
|
|
326
|
+
<div className="flex items-center justify-between mb-4">
|
|
327
|
+
<h3 className="text-xl font-bold text-slate-900 dark:text-white flex items-center gap-2">
|
|
328
|
+
<Info className="text-primary-500" /> Step Details
|
|
329
|
+
</h3>
|
|
330
|
+
<button
|
|
331
|
+
onClick={() => setStepPanelExpanded(!stepPanelExpanded)}
|
|
332
|
+
className="p-2 rounded-lg bg-slate-100 dark:bg-slate-800 hover:bg-slate-200 dark:hover:bg-slate-700 transition-colors"
|
|
333
|
+
title={stepPanelExpanded ? 'Collapse panel' : 'Expand panel'}
|
|
334
|
+
>
|
|
335
|
+
<Maximize2 size={16} className="text-slate-600 dark:text-slate-300" />
|
|
336
|
+
</button>
|
|
337
|
+
</div>
|
|
270
338
|
|
|
271
339
|
{selectedStepData ? (
|
|
272
|
-
<Card className=
|
|
340
|
+
<Card className={`overflow-hidden transition-all duration-300 ${stepPanelExpanded ? 'h-auto' : ''
|
|
341
|
+
}`}>
|
|
273
342
|
{/* Step Header */}
|
|
274
343
|
<div className="pb-4 border-b border-slate-100 dark:border-slate-700">
|
|
275
|
-
<
|
|
276
|
-
|
|
344
|
+
<div className="flex items-center justify-between">
|
|
345
|
+
<h4 className="text-lg font-bold text-slate-900 dark:text-white">{selectedStep}</h4>
|
|
346
|
+
</div>
|
|
347
|
+
<div className="flex items-center gap-2 mt-2">
|
|
277
348
|
<Badge variant={selectedStepData.success ? 'success' : 'danger'} className="text-xs">
|
|
278
349
|
{selectedStepData.success ? 'Success' : 'Failed'}
|
|
279
350
|
</Badge>
|
|
@@ -288,7 +359,7 @@ export function RunDetails() {
|
|
|
288
359
|
</div>
|
|
289
360
|
</div>
|
|
290
361
|
|
|
291
|
-
{/* Live Heartbeat Indicator
|
|
362
|
+
{/* Live Heartbeat Indicator */}
|
|
292
363
|
{selectedStepData.status === 'running' && (
|
|
293
364
|
<div className="px-4 py-2 bg-blue-50 dark:bg-blue-900/20 border-b border-blue-100 dark:border-blue-800 flex items-center justify-between">
|
|
294
365
|
<div className="flex items-center gap-2 text-sm text-blue-700 dark:text-blue-300">
|
|
@@ -312,23 +383,24 @@ export function RunDetails() {
|
|
|
312
383
|
)}
|
|
313
384
|
|
|
314
385
|
{/* Tabs */}
|
|
315
|
-
<div className="flex gap-
|
|
386
|
+
<div className="flex gap-1 border-b border-slate-100 dark:border-slate-700 mt-4 overflow-x-auto">
|
|
316
387
|
<TabButton active={activeTab === 'overview'} onClick={() => setActiveTab('overview')}>
|
|
317
|
-
<Info size={
|
|
388
|
+
<Info size={14} /> Overview
|
|
318
389
|
</TabButton>
|
|
319
390
|
<TabButton active={activeTab === 'code'} onClick={() => setActiveTab('code')}>
|
|
320
|
-
<Code2 size={
|
|
391
|
+
<Code2 size={14} /> Code
|
|
321
392
|
</TabButton>
|
|
322
393
|
<TabButton active={activeTab === 'artifacts'} onClick={() => setActiveTab('artifacts')}>
|
|
323
|
-
<Package size={
|
|
394
|
+
<Package size={14} /> Artifacts
|
|
324
395
|
</TabButton>
|
|
325
396
|
<TabButton active={activeTab === 'logs'} onClick={() => setActiveTab('logs')} data-tab="logs">
|
|
326
|
-
<Terminal size={
|
|
397
|
+
<Terminal size={14} /> Logs
|
|
327
398
|
</TabButton>
|
|
328
399
|
</div>
|
|
329
400
|
|
|
330
|
-
{/* Tab Content */}
|
|
331
|
-
<div className=
|
|
401
|
+
{/* Tab Content - Larger when expanded */}
|
|
402
|
+
<div className={`mt-4 overflow-y-auto transition-all duration-300 ${stepPanelExpanded ? 'max-h-[600px]' : 'max-h-[450px]'
|
|
403
|
+
}`}>
|
|
332
404
|
{activeTab === 'overview' && (
|
|
333
405
|
<OverviewTab
|
|
334
406
|
stepData={selectedStepData}
|
|
@@ -351,18 +423,27 @@ export function RunDetails() {
|
|
|
351
423
|
runId={runId}
|
|
352
424
|
stepName={selectedStep}
|
|
353
425
|
isRunning={run.status === 'running'}
|
|
426
|
+
maxHeight={stepPanelExpanded ? 'max-h-[500px]' : 'max-h-96'}
|
|
354
427
|
/>
|
|
355
428
|
)}
|
|
356
429
|
</div>
|
|
357
430
|
</Card>
|
|
358
431
|
) : (
|
|
359
|
-
<Card className="p-12 text-center">
|
|
360
|
-
<
|
|
432
|
+
<Card className="p-12 text-center border-2 border-dashed border-slate-200 dark:border-slate-700">
|
|
433
|
+
<Info size={32} className="mx-auto text-slate-300 dark:text-slate-600 mb-3" />
|
|
434
|
+
<p className="text-slate-500 dark:text-slate-400 font-medium">Select a step to view details</p>
|
|
435
|
+
<p className="text-xs text-slate-400 dark:text-slate-500 mt-1">Click on any step in the graph</p>
|
|
361
436
|
</Card>
|
|
362
437
|
)}
|
|
363
438
|
</div>
|
|
364
439
|
</div>
|
|
365
440
|
|
|
441
|
+
{/* Training Metrics Section - Only shown if there's training data */}
|
|
442
|
+
<TrainingMetricsSectionWrapper
|
|
443
|
+
runId={runId}
|
|
444
|
+
isRunning={run.status === 'running'}
|
|
445
|
+
/>
|
|
446
|
+
|
|
366
447
|
{/* Artifact Detail Modal */}
|
|
367
448
|
<ArtifactModal
|
|
368
449
|
artifact={selectedArtifact}
|
|
@@ -712,105 +793,342 @@ function ArtifactsTab({ artifacts, onArtifactClick }) {
|
|
|
712
793
|
}
|
|
713
794
|
|
|
714
795
|
function ArtifactModal({ artifact, onClose }) {
|
|
796
|
+
const [activeTab, setActiveTab] = useState('visualization');
|
|
797
|
+
|
|
715
798
|
if (!artifact) return null;
|
|
716
799
|
|
|
800
|
+
const isDataset = artifact.type === 'Dataset' || artifact.asset_type === 'Dataset';
|
|
801
|
+
const isModel = artifact.type === 'Model' || artifact.asset_type === 'Model';
|
|
802
|
+
const isMetrics = artifact.type === 'Metrics' || artifact.asset_type === 'Metrics';
|
|
803
|
+
|
|
804
|
+
// Get properties for display (filter out internal/large ones)
|
|
805
|
+
const displayableProps = artifact.properties
|
|
806
|
+
? Object.entries(artifact.properties).filter(([key, value]) => {
|
|
807
|
+
// Skip internal properties and very large values
|
|
808
|
+
if (key.startsWith('_')) return false;
|
|
809
|
+
if (typeof value === 'string' && value.length > 500) return false;
|
|
810
|
+
return true;
|
|
811
|
+
})
|
|
812
|
+
: [];
|
|
813
|
+
|
|
814
|
+
// Check for training history
|
|
815
|
+
const hasTrainingHistory = artifact.training_history &&
|
|
816
|
+
artifact.training_history.epochs &&
|
|
817
|
+
artifact.training_history.epochs.length > 0;
|
|
818
|
+
|
|
819
|
+
// Also check in properties for training history
|
|
820
|
+
const trainingHistoryFromProps = artifact.properties?.training_history;
|
|
821
|
+
const effectiveTrainingHistory = artifact.training_history || (
|
|
822
|
+
trainingHistoryFromProps && trainingHistoryFromProps.epochs
|
|
823
|
+
? trainingHistoryFromProps
|
|
824
|
+
: null
|
|
825
|
+
);
|
|
826
|
+
|
|
827
|
+
// Get icon and gradient based on type
|
|
828
|
+
const getTypeStyle = () => {
|
|
829
|
+
if (isDataset) return {
|
|
830
|
+
icon: <Database size={28} />,
|
|
831
|
+
gradient: 'from-blue-500 to-indigo-600',
|
|
832
|
+
bgGradient: 'from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20',
|
|
833
|
+
iconBg: 'bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400'
|
|
834
|
+
};
|
|
835
|
+
if (isModel) return {
|
|
836
|
+
icon: <Box size={28} />,
|
|
837
|
+
gradient: 'from-purple-500 to-violet-600',
|
|
838
|
+
bgGradient: 'from-purple-50 to-violet-50 dark:from-purple-900/20 dark:to-violet-900/20',
|
|
839
|
+
iconBg: 'bg-purple-100 dark:bg-purple-900/30 text-purple-600 dark:text-purple-400'
|
|
840
|
+
};
|
|
841
|
+
if (isMetrics) return {
|
|
842
|
+
icon: <BarChart2 size={28} />,
|
|
843
|
+
gradient: 'from-emerald-500 to-teal-600',
|
|
844
|
+
bgGradient: 'from-emerald-50 to-teal-50 dark:from-emerald-900/20 dark:to-teal-900/20',
|
|
845
|
+
iconBg: 'bg-emerald-100 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400'
|
|
846
|
+
};
|
|
847
|
+
return {
|
|
848
|
+
icon: <FileText size={28} />,
|
|
849
|
+
gradient: 'from-slate-500 to-slate-600',
|
|
850
|
+
bgGradient: 'from-slate-50 to-slate-100 dark:from-slate-800 dark:to-slate-900',
|
|
851
|
+
iconBg: 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-400'
|
|
852
|
+
};
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
const typeStyle = getTypeStyle();
|
|
856
|
+
|
|
857
|
+
// Get model-specific properties
|
|
858
|
+
const modelInfo = isModel ? {
|
|
859
|
+
framework: artifact.properties?.framework || 'Unknown',
|
|
860
|
+
parameters: artifact.properties?.parameters,
|
|
861
|
+
layers: artifact.properties?.num_layers,
|
|
862
|
+
optimizer: artifact.properties?.optimizer,
|
|
863
|
+
learningRate: artifact.properties?.learning_rate,
|
|
864
|
+
epochsTrained: artifact.properties?.epochs_trained || effectiveTrainingHistory?.epochs?.length,
|
|
865
|
+
} : null;
|
|
866
|
+
|
|
867
|
+
// Get dataset-specific properties
|
|
868
|
+
const datasetInfo = isDataset ? {
|
|
869
|
+
samples: artifact.properties?.num_samples || artifact.properties?.samples,
|
|
870
|
+
features: artifact.properties?.num_features,
|
|
871
|
+
source: artifact.properties?.source,
|
|
872
|
+
split: artifact.properties?.split,
|
|
873
|
+
} : null;
|
|
874
|
+
|
|
875
|
+
// Get metrics-specific data
|
|
876
|
+
const metricsData = isMetrics && artifact.properties ? Object.entries(artifact.properties)
|
|
877
|
+
.filter(([key, value]) => typeof value === 'number' && !key.startsWith('_'))
|
|
878
|
+
.sort((a, b) => a[0].localeCompare(b[0])) : [];
|
|
879
|
+
|
|
717
880
|
return (
|
|
718
881
|
<AnimatePresence>
|
|
719
882
|
<motion.div
|
|
720
883
|
initial={{ opacity: 0 }}
|
|
721
884
|
animate={{ opacity: 1 }}
|
|
722
885
|
exit={{ opacity: 0 }}
|
|
723
|
-
className="fixed inset-0 bg-black/
|
|
886
|
+
className="fixed inset-0 bg-black/60 backdrop-blur-md z-50 flex items-center justify-center p-4"
|
|
724
887
|
onClick={onClose}
|
|
725
888
|
>
|
|
726
889
|
<motion.div
|
|
727
|
-
initial={{ scale: 0.9, opacity: 0 }}
|
|
728
|
-
animate={{ scale: 1, opacity: 1 }}
|
|
729
|
-
exit={{ scale: 0.9, opacity: 0 }}
|
|
890
|
+
initial={{ scale: 0.9, opacity: 0, y: 20 }}
|
|
891
|
+
animate={{ scale: 1, opacity: 1, y: 0 }}
|
|
892
|
+
exit={{ scale: 0.9, opacity: 0, y: 20 }}
|
|
893
|
+
transition={{ type: 'spring', damping: 25, stiffness: 300 }}
|
|
730
894
|
onClick={(e) => e.stopPropagation()}
|
|
731
|
-
className="bg-white rounded-2xl shadow-2xl
|
|
895
|
+
className="bg-white dark:bg-slate-900 rounded-2xl shadow-2xl w-full max-w-5xl max-h-[90vh] overflow-hidden flex flex-col"
|
|
732
896
|
>
|
|
733
|
-
{/* Header */}
|
|
734
|
-
<div className=
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
897
|
+
{/* Header with gradient */}
|
|
898
|
+
<div className={`relative p-6 bg-gradient-to-r ${typeStyle.bgGradient} border-b border-slate-200 dark:border-slate-700`}>
|
|
899
|
+
{/* Background decoration */}
|
|
900
|
+
<div className="absolute top-0 right-0 w-64 h-64 opacity-10 pointer-events-none">
|
|
901
|
+
<div className={`w-full h-full bg-gradient-to-br ${typeStyle.gradient} rounded-full blur-3xl`} />
|
|
902
|
+
</div>
|
|
903
|
+
|
|
904
|
+
<div className="relative flex items-start justify-between">
|
|
905
|
+
<div className="flex items-center gap-4">
|
|
906
|
+
<div className={`p-4 rounded-2xl shadow-lg bg-gradient-to-br ${typeStyle.gradient}`}>
|
|
907
|
+
<div className="text-white">
|
|
908
|
+
{typeStyle.icon}
|
|
909
|
+
</div>
|
|
910
|
+
</div>
|
|
911
|
+
<div>
|
|
912
|
+
<h3 className="text-2xl font-bold text-slate-900 dark:text-white">{artifact.name}</h3>
|
|
913
|
+
<div className="flex items-center gap-3 mt-1">
|
|
914
|
+
<span className={`text-sm font-medium px-3 py-0.5 rounded-full ${typeStyle.iconBg}`}>
|
|
915
|
+
{artifact.type}
|
|
916
|
+
</span>
|
|
917
|
+
{artifact.step && (
|
|
918
|
+
<span className="text-sm text-slate-500 dark:text-slate-400">
|
|
919
|
+
from <span className="font-mono font-medium">{artifact.step}</span>
|
|
920
|
+
</span>
|
|
921
|
+
)}
|
|
922
|
+
</div>
|
|
923
|
+
</div>
|
|
741
924
|
</div>
|
|
742
|
-
<div>
|
|
743
|
-
<
|
|
744
|
-
|
|
925
|
+
<div className="flex items-center gap-2">
|
|
926
|
+
<button
|
|
927
|
+
onClick={() => downloadArtifactById(artifact.artifact_id)}
|
|
928
|
+
className={`inline-flex items-center gap-2 px-4 py-2.5 rounded-xl bg-gradient-to-r ${typeStyle.gradient} text-white text-sm font-semibold hover:shadow-lg transition-all disabled:opacity-50`}
|
|
929
|
+
disabled={!artifact.artifact_id}
|
|
930
|
+
>
|
|
931
|
+
<Download size={16} /> Download
|
|
932
|
+
</button>
|
|
933
|
+
<button
|
|
934
|
+
onClick={onClose}
|
|
935
|
+
className="p-2.5 hover:bg-white/50 dark:hover:bg-slate-800 rounded-xl transition-colors"
|
|
936
|
+
>
|
|
937
|
+
<X size={22} className="text-slate-500 dark:text-slate-400" />
|
|
938
|
+
</button>
|
|
745
939
|
</div>
|
|
746
940
|
</div>
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
<
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
941
|
+
|
|
942
|
+
{/* Quick stats bar for Models */}
|
|
943
|
+
{isModel && modelInfo && (
|
|
944
|
+
<div className="mt-4 flex flex-wrap gap-4">
|
|
945
|
+
<QuickStat label="Framework" value={modelInfo.framework} icon={<Zap size={12} />} />
|
|
946
|
+
{modelInfo.parameters && <QuickStat label="Parameters" value={modelInfo.parameters.toLocaleString()} icon={<Activity size={12} />} />}
|
|
947
|
+
{modelInfo.layers && <QuickStat label="Layers" value={modelInfo.layers} icon={<Layers size={12} />} />}
|
|
948
|
+
{modelInfo.epochsTrained && <QuickStat label="Epochs" value={modelInfo.epochsTrained} icon={<TrendingUp size={12} />} />}
|
|
949
|
+
</div>
|
|
950
|
+
)}
|
|
951
|
+
|
|
952
|
+
{/* Quick stats bar for Datasets */}
|
|
953
|
+
{isDataset && datasetInfo && (
|
|
954
|
+
<div className="mt-4 flex flex-wrap gap-4">
|
|
955
|
+
{datasetInfo.samples && <QuickStat label="Samples" value={datasetInfo.samples.toLocaleString()} icon={<Database size={12} />} />}
|
|
956
|
+
{datasetInfo.features && <QuickStat label="Features" value={datasetInfo.features} icon={<Layers size={12} />} />}
|
|
957
|
+
{datasetInfo.split && <QuickStat label="Split" value={datasetInfo.split} icon={<FileText size={12} />} />}
|
|
958
|
+
{datasetInfo.source && <QuickStat label="Source" value={datasetInfo.source} icon={<Cloud size={12} />} />}
|
|
959
|
+
</div>
|
|
960
|
+
)}
|
|
762
961
|
</div>
|
|
763
962
|
|
|
963
|
+
{/* Tabs - only show for complex types */}
|
|
964
|
+
{(isModel || isDataset || isMetrics) && (
|
|
965
|
+
<div className="flex gap-1 px-6 py-3 border-b border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/50">
|
|
966
|
+
<TabBtn active={activeTab === 'visualization'} onClick={() => setActiveTab('visualization')}>
|
|
967
|
+
{isModel ? <><LineChart size={14} /> Visualization</> :
|
|
968
|
+
isDataset ? <><BarChart2 size={14} /> Statistics</> :
|
|
969
|
+
<><Activity size={14} /> Metrics</>}
|
|
970
|
+
</TabBtn>
|
|
971
|
+
{displayableProps.length > 0 && (
|
|
972
|
+
<TabBtn active={activeTab === 'properties'} onClick={() => setActiveTab('properties')}>
|
|
973
|
+
<Tag size={14} /> Properties ({displayableProps.length})
|
|
974
|
+
</TabBtn>
|
|
975
|
+
)}
|
|
976
|
+
<TabBtn active={activeTab === 'metadata'} onClick={() => setActiveTab('metadata')}>
|
|
977
|
+
<Info size={14} /> Metadata
|
|
978
|
+
</TabBtn>
|
|
979
|
+
</div>
|
|
980
|
+
)}
|
|
981
|
+
|
|
764
982
|
{/* Content */}
|
|
765
|
-
<div className="
|
|
766
|
-
<
|
|
767
|
-
{/*
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
983
|
+
<div className="flex-1 overflow-y-auto p-6">
|
|
984
|
+
<AnimatePresence mode="wait">
|
|
985
|
+
{/* Visualization Tab */}
|
|
986
|
+
{activeTab === 'visualization' && (
|
|
987
|
+
<motion.div
|
|
988
|
+
key="viz"
|
|
989
|
+
initial={{ opacity: 0, y: 10 }}
|
|
990
|
+
animate={{ opacity: 1, y: 0 }}
|
|
991
|
+
exit={{ opacity: 0, y: -10 }}
|
|
992
|
+
className="space-y-6"
|
|
993
|
+
>
|
|
994
|
+
{/* Model with Training History */}
|
|
995
|
+
{isModel && effectiveTrainingHistory && (
|
|
996
|
+
<div>
|
|
997
|
+
<div className="flex items-center gap-2 mb-4">
|
|
998
|
+
<div className="p-2 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
|
|
999
|
+
<LineChart size={18} className="text-purple-600 dark:text-purple-400" />
|
|
1000
|
+
</div>
|
|
1001
|
+
<h4 className="text-lg font-bold text-slate-900 dark:text-white">Training History</h4>
|
|
1002
|
+
<span className="text-xs bg-slate-100 dark:bg-slate-800 px-2 py-0.5 rounded-full text-slate-500">
|
|
1003
|
+
{effectiveTrainingHistory.epochs.length} epochs
|
|
1004
|
+
</span>
|
|
1005
|
+
</div>
|
|
1006
|
+
<TrainingHistoryChart
|
|
1007
|
+
trainingHistory={effectiveTrainingHistory}
|
|
1008
|
+
compact={false}
|
|
1009
|
+
/>
|
|
1010
|
+
</div>
|
|
1011
|
+
)}
|
|
1012
|
+
|
|
1013
|
+
{/* Model without Training History */}
|
|
1014
|
+
{isModel && !effectiveTrainingHistory && (
|
|
1015
|
+
<div className="text-center py-12">
|
|
1016
|
+
<div className="inline-flex p-4 bg-slate-100 dark:bg-slate-800 rounded-full mb-4">
|
|
1017
|
+
<Box size={32} className="text-slate-400" />
|
|
1018
|
+
</div>
|
|
1019
|
+
<h4 className="text-lg font-bold text-slate-900 dark:text-white mb-2">Model Artifact</h4>
|
|
1020
|
+
<p className="text-slate-500 dark:text-slate-400">
|
|
1021
|
+
No training history available for this model.
|
|
1022
|
+
<br />
|
|
1023
|
+
Use <code className="bg-slate-100 dark:bg-slate-800 px-1.5 py-0.5 rounded text-xs">FlowymlKerasCallback</code> to capture training metrics.
|
|
1024
|
+
</p>
|
|
1025
|
+
</div>
|
|
1026
|
+
)}
|
|
1027
|
+
|
|
1028
|
+
{/* Dataset Viewer - use the full DatasetViewer */}
|
|
1029
|
+
{isDataset && (
|
|
1030
|
+
<ArtifactViewer artifact={artifact} />
|
|
1031
|
+
)}
|
|
1032
|
+
|
|
1033
|
+
{/* Metrics Display */}
|
|
1034
|
+
{isMetrics && metricsData.length > 0 && (
|
|
1035
|
+
<div className="space-y-4">
|
|
1036
|
+
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
|
1037
|
+
{metricsData.map(([key, value], idx) => (
|
|
1038
|
+
<MetricDisplayCard
|
|
1039
|
+
key={key}
|
|
1040
|
+
name={key}
|
|
1041
|
+
value={value}
|
|
1042
|
+
index={idx}
|
|
1043
|
+
/>
|
|
1044
|
+
))}
|
|
1045
|
+
</div>
|
|
1046
|
+
</div>
|
|
1047
|
+
)}
|
|
1048
|
+
|
|
1049
|
+
{/* Generic Artifact */}
|
|
1050
|
+
{!isModel && !isDataset && !isMetrics && (
|
|
1051
|
+
<ArtifactViewer artifact={artifact} />
|
|
1052
|
+
)}
|
|
1053
|
+
</motion.div>
|
|
1054
|
+
)}
|
|
1055
|
+
|
|
1056
|
+
{/* Properties Tab */}
|
|
1057
|
+
{activeTab === 'properties' && (
|
|
1058
|
+
<motion.div
|
|
1059
|
+
key="props"
|
|
1060
|
+
initial={{ opacity: 0, y: 10 }}
|
|
1061
|
+
animate={{ opacity: 1, y: 0 }}
|
|
1062
|
+
exit={{ opacity: 0, y: -10 }}
|
|
1063
|
+
>
|
|
1064
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
1065
|
+
{displayableProps.map(([key, value]) => (
|
|
1066
|
+
<div
|
|
1067
|
+
key={key}
|
|
1068
|
+
className="p-4 bg-slate-50 dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700"
|
|
1069
|
+
>
|
|
1070
|
+
<span className="text-xs text-slate-500 dark:text-slate-400 uppercase tracking-wide block mb-1">
|
|
1071
|
+
{key.replace(/_/g, ' ')}
|
|
1072
|
+
</span>
|
|
1073
|
+
<span className="text-sm font-mono font-semibold text-slate-900 dark:text-white break-all">
|
|
1074
|
+
{typeof value === 'object' ? JSON.stringify(value, null, 2) :
|
|
1075
|
+
typeof value === 'number' ? value.toLocaleString() :
|
|
1076
|
+
String(value)}
|
|
780
1077
|
</span>
|
|
781
1078
|
</div>
|
|
782
1079
|
))}
|
|
783
1080
|
</div>
|
|
784
|
-
</div>
|
|
1081
|
+
</motion.div>
|
|
785
1082
|
)}
|
|
786
1083
|
|
|
787
|
-
{/* Metadata */}
|
|
788
|
-
|
|
789
|
-
<
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
<
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
<div className="
|
|
801
|
-
<
|
|
802
|
-
<
|
|
1084
|
+
{/* Metadata Tab */}
|
|
1085
|
+
{activeTab === 'metadata' && (
|
|
1086
|
+
<motion.div
|
|
1087
|
+
key="meta"
|
|
1088
|
+
initial={{ opacity: 0, y: 10 }}
|
|
1089
|
+
animate={{ opacity: 1, y: 0 }}
|
|
1090
|
+
exit={{ opacity: 0, y: -10 }}
|
|
1091
|
+
className="space-y-4"
|
|
1092
|
+
>
|
|
1093
|
+
<div className="bg-slate-50 dark:bg-slate-800 rounded-xl p-6 border border-slate-200 dark:border-slate-700">
|
|
1094
|
+
<h5 className="text-sm font-bold text-slate-700 dark:text-slate-300 uppercase tracking-wide mb-4">
|
|
1095
|
+
Artifact Information
|
|
1096
|
+
</h5>
|
|
1097
|
+
<div className="space-y-3">
|
|
1098
|
+
<MetadataRow label="Artifact ID" value={artifact.artifact_id} mono />
|
|
1099
|
+
<MetadataRow label="Type" value={artifact.type} />
|
|
1100
|
+
<MetadataRow label="Step" value={artifact.step} />
|
|
1101
|
+
{artifact.run_id && <MetadataRow label="Run ID" value={artifact.run_id} mono />}
|
|
1102
|
+
{artifact.pipeline_name && <MetadataRow label="Pipeline" value={artifact.pipeline_name} />}
|
|
1103
|
+
{artifact.created_at && (
|
|
1104
|
+
<MetadataRow
|
|
1105
|
+
label="Created"
|
|
1106
|
+
value={format(new Date(artifact.created_at), 'MMM d, yyyy HH:mm:ss')}
|
|
1107
|
+
/>
|
|
1108
|
+
)}
|
|
803
1109
|
</div>
|
|
804
|
-
|
|
805
|
-
</div>
|
|
806
|
-
|
|
807
|
-
</
|
|
1110
|
+
</div>
|
|
1111
|
+
</motion.div>
|
|
1112
|
+
)}
|
|
1113
|
+
</AnimatePresence>
|
|
808
1114
|
</div>
|
|
809
1115
|
|
|
810
1116
|
{/* Footer */}
|
|
811
|
-
<div className="p-4 border-t border-slate-
|
|
812
|
-
<
|
|
813
|
-
|
|
1117
|
+
<div className="p-4 border-t border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-800/50 flex justify-between items-center">
|
|
1118
|
+
<p className="text-xs text-slate-500 dark:text-slate-400">
|
|
1119
|
+
💡 Click Download to save the full artifact data
|
|
1120
|
+
</p>
|
|
1121
|
+
<div className="flex gap-2">
|
|
1122
|
+
<Button variant="ghost" onClick={onClose}>Close</Button>
|
|
1123
|
+
<Button
|
|
1124
|
+
variant="primary"
|
|
1125
|
+
onClick={() => downloadArtifactById(artifact.artifact_id)}
|
|
1126
|
+
disabled={!artifact.artifact_id}
|
|
1127
|
+
>
|
|
1128
|
+
<Download size={14} className="mr-1.5" />
|
|
1129
|
+
Download
|
|
1130
|
+
</Button>
|
|
1131
|
+
</div>
|
|
814
1132
|
</div>
|
|
815
1133
|
</motion.div>
|
|
816
1134
|
</motion.div>
|
|
@@ -818,6 +1136,104 @@ function ArtifactModal({ artifact, onClose }) {
|
|
|
818
1136
|
);
|
|
819
1137
|
}
|
|
820
1138
|
|
|
1139
|
+
// Quick stat component for header
|
|
1140
|
+
function QuickStat({ label, value, icon }) {
|
|
1141
|
+
return (
|
|
1142
|
+
<div className="flex items-center gap-2 px-3 py-1.5 bg-white/50 dark:bg-slate-800/50 rounded-lg backdrop-blur-sm border border-slate-200/50 dark:border-slate-700/50">
|
|
1143
|
+
<span className="text-slate-400">{icon}</span>
|
|
1144
|
+
<span className="text-xs text-slate-500 dark:text-slate-400">{label}:</span>
|
|
1145
|
+
<span className="text-sm font-bold text-slate-900 dark:text-white">{value}</span>
|
|
1146
|
+
</div>
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// Tab button component
|
|
1151
|
+
function TabBtn({ active, onClick, children }) {
|
|
1152
|
+
return (
|
|
1153
|
+
<button
|
|
1154
|
+
onClick={onClick}
|
|
1155
|
+
className={`flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg transition-all ${active
|
|
1156
|
+
? 'bg-white dark:bg-slate-700 text-slate-900 dark:text-white shadow-sm'
|
|
1157
|
+
: 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-white/50 dark:hover:bg-slate-700/50'
|
|
1158
|
+
}`}
|
|
1159
|
+
>
|
|
1160
|
+
{children}
|
|
1161
|
+
</button>
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// Metric display card for Metrics artifacts
|
|
1166
|
+
function MetricDisplayCard({ name, value, index }) {
|
|
1167
|
+
const colors = [
|
|
1168
|
+
'from-blue-500 to-indigo-600',
|
|
1169
|
+
'from-purple-500 to-violet-600',
|
|
1170
|
+
'from-emerald-500 to-teal-600',
|
|
1171
|
+
'from-amber-500 to-orange-600',
|
|
1172
|
+
'from-rose-500 to-pink-600',
|
|
1173
|
+
'from-cyan-500 to-blue-600',
|
|
1174
|
+
];
|
|
1175
|
+
const color = colors[index % colors.length];
|
|
1176
|
+
|
|
1177
|
+
// Format name nicely
|
|
1178
|
+
const displayName = name
|
|
1179
|
+
.replace(/_/g, ' ')
|
|
1180
|
+
.replace(/\b\w/g, c => c.toUpperCase());
|
|
1181
|
+
|
|
1182
|
+
// Format value
|
|
1183
|
+
const displayValue = typeof value === 'number'
|
|
1184
|
+
? (Math.abs(value) < 0.01 || Math.abs(value) > 1000
|
|
1185
|
+
? value.toExponential(3)
|
|
1186
|
+
: value.toFixed(4))
|
|
1187
|
+
: value;
|
|
1188
|
+
|
|
1189
|
+
// Determine if this is a "good" metric (accuracy, score) or "bad" (loss, error)
|
|
1190
|
+
const isLossLike = name.toLowerCase().includes('loss') ||
|
|
1191
|
+
name.toLowerCase().includes('error') ||
|
|
1192
|
+
name.toLowerCase().includes('mse') ||
|
|
1193
|
+
name.toLowerCase().includes('mae');
|
|
1194
|
+
|
|
1195
|
+
return (
|
|
1196
|
+
<motion.div
|
|
1197
|
+
initial={{ opacity: 0, scale: 0.9 }}
|
|
1198
|
+
animate={{ opacity: 1, scale: 1 }}
|
|
1199
|
+
transition={{ delay: index * 0.05 }}
|
|
1200
|
+
className="relative overflow-hidden p-4 bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 hover:shadow-lg transition-all group"
|
|
1201
|
+
>
|
|
1202
|
+
<div className={`absolute top-0 right-0 w-16 h-16 bg-gradient-to-br ${color} opacity-10 rounded-full blur-2xl group-hover:opacity-20 transition-opacity`} />
|
|
1203
|
+
<div className="relative">
|
|
1204
|
+
<p className="text-xs text-slate-500 dark:text-slate-400 uppercase tracking-wide mb-1 truncate">
|
|
1205
|
+
{displayName}
|
|
1206
|
+
</p>
|
|
1207
|
+
<p className="text-2xl font-bold font-mono text-slate-900 dark:text-white">
|
|
1208
|
+
{displayValue}
|
|
1209
|
+
</p>
|
|
1210
|
+
<div className="mt-1 flex items-center gap-1">
|
|
1211
|
+
{isLossLike ? (
|
|
1212
|
+
<TrendingDown size={12} className="text-emerald-500" />
|
|
1213
|
+
) : (
|
|
1214
|
+
<TrendingUp size={12} className="text-emerald-500" />
|
|
1215
|
+
)}
|
|
1216
|
+
<span className="text-xs text-slate-400">
|
|
1217
|
+
{isLossLike ? 'Lower is better' : 'Higher is better'}
|
|
1218
|
+
</span>
|
|
1219
|
+
</div>
|
|
1220
|
+
</div>
|
|
1221
|
+
</motion.div>
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// Metadata row component
|
|
1226
|
+
function MetadataRow({ label, value, mono = false }) {
|
|
1227
|
+
return (
|
|
1228
|
+
<div className="flex justify-between items-center py-2 border-b border-slate-200/50 dark:border-slate-700/50 last:border-0">
|
|
1229
|
+
<span className="text-sm text-slate-500 dark:text-slate-400">{label}</span>
|
|
1230
|
+
<span className={`text-sm font-medium text-slate-900 dark:text-white ${mono ? 'font-mono text-xs' : ''}`}>
|
|
1231
|
+
{value || '—'}
|
|
1232
|
+
</span>
|
|
1233
|
+
</div>
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
821
1237
|
function LogsViewer({ runId, stepName, isRunning, maxHeight = "max-h-96", minimal = false }) {
|
|
822
1238
|
const [logs, setLogs] = useState('');
|
|
823
1239
|
const [loading, setLoading] = useState(true);
|
|
@@ -964,3 +1380,87 @@ function LogsViewer({ runId, stepName, isRunning, maxHeight = "max-h-96", minima
|
|
|
964
1380
|
</div>
|
|
965
1381
|
);
|
|
966
1382
|
}
|
|
1383
|
+
|
|
1384
|
+
/**
|
|
1385
|
+
* Training Metrics Section Wrapper
|
|
1386
|
+
* Only renders the training metrics section if there's actual training data.
|
|
1387
|
+
* This prevents showing an empty section for non-training pipelines.
|
|
1388
|
+
*/
|
|
1389
|
+
function TrainingMetricsSectionWrapper({ runId, isRunning }) {
|
|
1390
|
+
const [hasData, setHasData] = useState(false);
|
|
1391
|
+
const [isExpanded, setIsExpanded] = useState(true);
|
|
1392
|
+
const [loading, setLoading] = useState(true);
|
|
1393
|
+
|
|
1394
|
+
// Check if training data exists
|
|
1395
|
+
useEffect(() => {
|
|
1396
|
+
const checkTrainingData = async () => {
|
|
1397
|
+
try {
|
|
1398
|
+
const res = await fetchApi(`/api/runs/${runId}/training-history`);
|
|
1399
|
+
if (res.ok) {
|
|
1400
|
+
const data = await res.json();
|
|
1401
|
+
setHasData(data.has_history && data.training_history?.epochs?.length > 0);
|
|
1402
|
+
}
|
|
1403
|
+
} catch (err) {
|
|
1404
|
+
console.error('Error checking training history:', err);
|
|
1405
|
+
} finally {
|
|
1406
|
+
setLoading(false);
|
|
1407
|
+
}
|
|
1408
|
+
};
|
|
1409
|
+
|
|
1410
|
+
if (runId) {
|
|
1411
|
+
checkTrainingData();
|
|
1412
|
+
}
|
|
1413
|
+
}, [runId]);
|
|
1414
|
+
|
|
1415
|
+
// Don't render anything if no training data
|
|
1416
|
+
if (loading) return null;
|
|
1417
|
+
if (!hasData) return null;
|
|
1418
|
+
|
|
1419
|
+
return (
|
|
1420
|
+
<div className="mt-8">
|
|
1421
|
+
<Card className="overflow-hidden">
|
|
1422
|
+
<div
|
|
1423
|
+
className="p-6 cursor-pointer flex items-center justify-between bg-gradient-to-r from-slate-50 to-white dark:from-slate-800 dark:to-slate-900 border-b border-slate-100 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors"
|
|
1424
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
1425
|
+
>
|
|
1426
|
+
<div className="flex items-center gap-3">
|
|
1427
|
+
<div className="p-2.5 bg-gradient-to-br from-emerald-500 to-teal-500 rounded-xl shadow-lg">
|
|
1428
|
+
<LineChart size={20} className="text-white" />
|
|
1429
|
+
</div>
|
|
1430
|
+
<div>
|
|
1431
|
+
<h3 className="text-lg font-bold text-slate-900 dark:text-white">
|
|
1432
|
+
Training Metrics
|
|
1433
|
+
</h3>
|
|
1434
|
+
<p className="text-sm text-slate-500 dark:text-slate-400">
|
|
1435
|
+
Interactive training history visualization
|
|
1436
|
+
</p>
|
|
1437
|
+
</div>
|
|
1438
|
+
</div>
|
|
1439
|
+
<ChevronRight
|
|
1440
|
+
size={20}
|
|
1441
|
+
className={`text-slate-400 transform transition-transform duration-200 ${isExpanded ? 'rotate-90' : ''}`}
|
|
1442
|
+
/>
|
|
1443
|
+
</div>
|
|
1444
|
+
<AnimatePresence>
|
|
1445
|
+
{isExpanded && (
|
|
1446
|
+
<motion.div
|
|
1447
|
+
initial={{ height: 0, opacity: 0 }}
|
|
1448
|
+
animate={{ height: 'auto', opacity: 1 }}
|
|
1449
|
+
exit={{ height: 0, opacity: 0 }}
|
|
1450
|
+
transition={{ duration: 0.2 }}
|
|
1451
|
+
className="overflow-hidden"
|
|
1452
|
+
>
|
|
1453
|
+
<div className="p-6">
|
|
1454
|
+
<TrainingMetricsPanel
|
|
1455
|
+
runId={runId}
|
|
1456
|
+
isRunning={isRunning}
|
|
1457
|
+
autoRefresh={true}
|
|
1458
|
+
/>
|
|
1459
|
+
</div>
|
|
1460
|
+
</motion.div>
|
|
1461
|
+
)}
|
|
1462
|
+
</AnimatePresence>
|
|
1463
|
+
</Card>
|
|
1464
|
+
</div>
|
|
1465
|
+
);
|
|
1466
|
+
}
|