flowyml 1.2.0__py3-none-any.whl → 1.4.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.
Files changed (104) hide show
  1. flowyml/__init__.py +3 -0
  2. flowyml/assets/base.py +10 -0
  3. flowyml/assets/metrics.py +6 -0
  4. flowyml/cli/main.py +108 -2
  5. flowyml/cli/run.py +9 -2
  6. flowyml/core/execution_status.py +52 -0
  7. flowyml/core/hooks.py +106 -0
  8. flowyml/core/observability.py +210 -0
  9. flowyml/core/orchestrator.py +274 -0
  10. flowyml/core/pipeline.py +193 -231
  11. flowyml/core/project.py +34 -2
  12. flowyml/core/remote_orchestrator.py +109 -0
  13. flowyml/core/resources.py +34 -17
  14. flowyml/core/retry_policy.py +80 -0
  15. flowyml/core/scheduler.py +9 -9
  16. flowyml/core/scheduler_config.py +2 -3
  17. flowyml/core/step.py +18 -1
  18. flowyml/core/submission_result.py +53 -0
  19. flowyml/integrations/keras.py +95 -22
  20. flowyml/monitoring/alerts.py +2 -2
  21. flowyml/stacks/__init__.py +15 -0
  22. flowyml/stacks/aws.py +599 -0
  23. flowyml/stacks/azure.py +295 -0
  24. flowyml/stacks/bridge.py +9 -9
  25. flowyml/stacks/components.py +24 -2
  26. flowyml/stacks/gcp.py +158 -11
  27. flowyml/stacks/local.py +5 -0
  28. flowyml/stacks/plugins.py +2 -2
  29. flowyml/stacks/registry.py +21 -0
  30. flowyml/storage/artifacts.py +15 -5
  31. flowyml/storage/materializers/__init__.py +2 -0
  32. flowyml/storage/materializers/base.py +33 -0
  33. flowyml/storage/materializers/cloudpickle.py +74 -0
  34. flowyml/storage/metadata.py +3 -881
  35. flowyml/storage/remote.py +590 -0
  36. flowyml/storage/sql.py +911 -0
  37. flowyml/ui/backend/dependencies.py +28 -0
  38. flowyml/ui/backend/main.py +43 -80
  39. flowyml/ui/backend/routers/assets.py +483 -17
  40. flowyml/ui/backend/routers/client.py +46 -0
  41. flowyml/ui/backend/routers/execution.py +13 -2
  42. flowyml/ui/backend/routers/experiments.py +97 -14
  43. flowyml/ui/backend/routers/metrics.py +168 -0
  44. flowyml/ui/backend/routers/pipelines.py +77 -12
  45. flowyml/ui/backend/routers/projects.py +33 -7
  46. flowyml/ui/backend/routers/runs.py +221 -12
  47. flowyml/ui/backend/routers/schedules.py +5 -21
  48. flowyml/ui/backend/routers/stats.py +14 -0
  49. flowyml/ui/backend/routers/traces.py +37 -53
  50. flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +1 -0
  51. flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +592 -0
  52. flowyml/ui/frontend/dist/index.html +2 -2
  53. flowyml/ui/frontend/src/App.jsx +4 -1
  54. flowyml/ui/frontend/src/app/assets/page.jsx +260 -230
  55. flowyml/ui/frontend/src/app/dashboard/page.jsx +38 -7
  56. flowyml/ui/frontend/src/app/experiments/page.jsx +61 -314
  57. flowyml/ui/frontend/src/app/observability/page.jsx +277 -0
  58. flowyml/ui/frontend/src/app/pipelines/page.jsx +79 -402
  59. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectArtifactsList.jsx +151 -0
  60. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +145 -0
  61. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHeader.jsx +45 -0
  62. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHierarchy.jsx +467 -0
  63. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +253 -0
  64. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectPipelinesList.jsx +105 -0
  65. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRelations.jsx +189 -0
  66. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRunsList.jsx +136 -0
  67. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectTabs.jsx +95 -0
  68. flowyml/ui/frontend/src/app/projects/[projectId]/page.jsx +326 -0
  69. flowyml/ui/frontend/src/app/projects/page.jsx +13 -3
  70. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +79 -10
  71. flowyml/ui/frontend/src/app/runs/page.jsx +82 -424
  72. flowyml/ui/frontend/src/app/settings/page.jsx +1 -0
  73. flowyml/ui/frontend/src/app/tokens/page.jsx +62 -16
  74. flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +373 -0
  75. flowyml/ui/frontend/src/components/AssetLineageGraph.jsx +291 -0
  76. flowyml/ui/frontend/src/components/AssetStatsDashboard.jsx +302 -0
  77. flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +477 -0
  78. flowyml/ui/frontend/src/components/ExperimentDetailsPanel.jsx +227 -0
  79. flowyml/ui/frontend/src/components/NavigationTree.jsx +401 -0
  80. flowyml/ui/frontend/src/components/PipelineDetailsPanel.jsx +239 -0
  81. flowyml/ui/frontend/src/components/PipelineGraph.jsx +67 -3
  82. flowyml/ui/frontend/src/components/ProjectSelector.jsx +115 -0
  83. flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +298 -0
  84. flowyml/ui/frontend/src/components/header/Header.jsx +48 -1
  85. flowyml/ui/frontend/src/components/plugins/ZenMLIntegration.jsx +106 -0
  86. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +52 -26
  87. flowyml/ui/frontend/src/components/ui/DataView.jsx +35 -17
  88. flowyml/ui/frontend/src/components/ui/ErrorBoundary.jsx +118 -0
  89. flowyml/ui/frontend/src/contexts/ProjectContext.jsx +2 -2
  90. flowyml/ui/frontend/src/contexts/ToastContext.jsx +116 -0
  91. flowyml/ui/frontend/src/layouts/MainLayout.jsx +5 -1
  92. flowyml/ui/frontend/src/router/index.jsx +4 -0
  93. flowyml/ui/frontend/src/utils/date.js +10 -0
  94. flowyml/ui/frontend/src/utils/downloads.js +11 -0
  95. flowyml/utils/config.py +6 -0
  96. flowyml/utils/stack_config.py +45 -3
  97. {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/METADATA +44 -4
  98. flowyml-1.4.0.dist-info/RECORD +200 -0
  99. {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/licenses/LICENSE +1 -1
  100. flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +0 -448
  101. flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +0 -1
  102. flowyml-1.2.0.dist-info/RECORD +0 -159
  103. {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/WHEEL +0 -0
  104. {flowyml-1.2.0.dist-info → flowyml-1.4.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,467 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { fetchApi } from '../../../../utils/api';
3
+ import {
4
+ ChevronRight,
5
+ ChevronDown,
6
+ Box,
7
+ Activity,
8
+ PlayCircle,
9
+ FileBox,
10
+ CheckCircle,
11
+ XCircle,
12
+ Clock,
13
+ Database,
14
+ Layers,
15
+ X,
16
+ TrendingUp,
17
+ HardDrive,
18
+ Info,
19
+ Download
20
+ } from 'lucide-react';
21
+ import { Link } from 'react-router-dom';
22
+ import { formatDate } from '../../../../utils/date';
23
+ import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
24
+ import { downloadArtifactById } from '../../../../utils/downloads';
25
+
26
+ const StatusIcon = ({ status }) => {
27
+ switch (status?.toLowerCase()) {
28
+ case 'completed':
29
+ case 'success':
30
+ return <CheckCircle className="w-4 h-4 text-green-500" />;
31
+ case 'failed':
32
+ return <XCircle className="w-4 h-4 text-red-500" />;
33
+ case 'running':
34
+ return <Activity className="w-4 h-4 text-blue-500 animate-spin" />;
35
+ default:
36
+ return <Clock className="w-4 h-4 text-slate-400" />;
37
+ }
38
+ };
39
+
40
+ const ArtifactIcon = ({ type }) => {
41
+ switch (type?.toLowerCase()) {
42
+ case 'model':
43
+ return <Box className="w-4 h-4 text-purple-500" />;
44
+ case 'dataset':
45
+ case 'data':
46
+ return <Database className="w-4 h-4 text-emerald-500" />;
47
+ default:
48
+ return <FileBox className="w-4 h-4 text-slate-400" />;
49
+ }
50
+ };
51
+
52
+ const TreeNode = ({ label, icon: Icon, children, defaultExpanded = false, actions, status, level = 0 }) => {
53
+ const [isExpanded, setIsExpanded] = useState(defaultExpanded);
54
+ const hasChildren = children && children.length > 0;
55
+
56
+ return (
57
+ <div className="select-none">
58
+ <div
59
+ className={`
60
+ flex items-center gap-2 p-2 rounded-lg cursor-pointer transition-colors
61
+ hover:bg-slate-100 dark:hover:bg-slate-800
62
+ ${level === 0 ? 'bg-slate-50 dark:bg-slate-800/50 mb-1' : ''}
63
+ `}
64
+ style={{ paddingLeft: `${level * 1.5 + 0.5}rem` }}
65
+ onClick={() => hasChildren && setIsExpanded(!isExpanded)}
66
+ >
67
+ <div className="flex items-center gap-1 text-slate-400">
68
+ {hasChildren ? (
69
+ isExpanded ? <ChevronDown className="w-4 h-4" /> : <ChevronRight className="w-4 h-4" />
70
+ ) : (
71
+ <div className="w-4" />
72
+ )}
73
+ </div>
74
+
75
+ {Icon && <Icon className={`w-4 h-4 ${level === 0 ? 'text-blue-500' : 'text-slate-500'}`} />}
76
+
77
+ <div className="flex-1 flex items-center justify-between">
78
+ <span className={`text-sm ${level === 0 ? 'font-semibold text-slate-900 dark:text-white' : 'text-slate-700 dark:text-slate-300'}`}>
79
+ {label}
80
+ </span>
81
+ <div className="flex items-center gap-3">
82
+ {status && <StatusIcon status={status} />}
83
+ {actions}
84
+ </div>
85
+ </div>
86
+ </div>
87
+
88
+ {isExpanded && hasChildren && (
89
+ <div className="animate-in slide-in-from-top-2 duration-200">
90
+ {children}
91
+ </div>
92
+ )}
93
+ </div>
94
+ );
95
+ };
96
+
97
+ export function ProjectHierarchy({ projectId }) {
98
+ const [data, setData] = useState({ pipelines: [], runs: [], artifacts: [] });
99
+ const [loading, setLoading] = useState(true);
100
+ const [selectedArtifact, setSelectedArtifact] = useState(null);
101
+
102
+ useEffect(() => {
103
+ const fetchData = async () => {
104
+ try {
105
+ // Fetch with higher limits to ensure we get the full tree
106
+ const [pipelinesRes, runsRes, artifactsRes] = await Promise.all([
107
+ fetchApi(`/api/pipelines?project=${projectId}`),
108
+ fetchApi(`/api/runs?project=${projectId}&limit=100`),
109
+ fetchApi(`/api/assets?project=${projectId}&limit=100`)
110
+ ]);
111
+
112
+ const pipelinesData = await pipelinesRes.json();
113
+ const runsData = await runsRes.json();
114
+ const artifactsData = await artifactsRes.json();
115
+
116
+ setData({
117
+ pipelines: Array.isArray(pipelinesData?.pipelines) ? pipelinesData.pipelines : [],
118
+ runs: Array.isArray(runsData?.runs) ? runsData.runs : [],
119
+ artifacts: Array.isArray(artifactsData?.assets) ? artifactsData.assets : (Array.isArray(artifactsData?.artifacts) ? artifactsData.artifacts : [])
120
+ });
121
+ } catch (error) {
122
+ console.error('Failed to fetch hierarchy data:', error);
123
+ } finally {
124
+ setLoading(false);
125
+ }
126
+ };
127
+
128
+ if (projectId) {
129
+ fetchData();
130
+ }
131
+ }, [projectId]);
132
+
133
+ if (loading) {
134
+ return <div className="h-64 w-full bg-slate-50 dark:bg-slate-800/50 rounded-xl animate-pulse" />;
135
+ }
136
+
137
+ // Group runs by pipeline
138
+ const getRunsForPipeline = (pipelineName) => {
139
+ return data.runs.filter(r => r.pipeline_name === pipelineName);
140
+ };
141
+
142
+ // Group artifacts by run
143
+ const getArtifactsForRun = (runId) => {
144
+ return data.artifacts.filter(a => a.run_id === runId);
145
+ };
146
+
147
+ return (
148
+ <div className="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 overflow-hidden">
149
+ <div className="p-4 border-b border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50">
150
+ <h3 className="font-semibold text-slate-900 dark:text-white flex items-center gap-2">
151
+ <Layers className="w-4 h-4 text-blue-500" />
152
+ Project Structure
153
+ </h3>
154
+ </div>
155
+
156
+ <div className="p-2 max-h-[600px] overflow-y-auto">
157
+ <TreeNode
158
+ label="Project Root"
159
+ icon={Box}
160
+ defaultExpanded={true}
161
+ level={0}
162
+ actions={<span className="text-xs text-slate-400">{data.pipelines.length} pipelines</span>}
163
+ >
164
+ {data.pipelines.length === 0 && (
165
+ <div className="p-4 text-center text-sm text-slate-500 italic">
166
+ No pipelines found. Run a pipeline to see it here.
167
+ </div>
168
+ )}
169
+
170
+ {data.pipelines.map(pipeline => {
171
+ const pipelineRuns = getRunsForPipeline(pipeline.name);
172
+
173
+ return (
174
+ <TreeNode
175
+ key={pipeline.name}
176
+ label={pipeline.name}
177
+ icon={Activity}
178
+ level={1}
179
+ actions={
180
+ <Link
181
+ to={`/pipelines/${pipeline.name}`}
182
+ className="text-xs text-blue-500 hover:underline px-2 py-1 rounded hover:bg-blue-50 dark:hover:bg-blue-900/20"
183
+ onClick={(e) => e.stopPropagation()}
184
+ >
185
+ View Details
186
+ </Link>
187
+ }
188
+ >
189
+ {pipelineRuns.length === 0 && (
190
+ <div className="pl-12 py-2 text-xs text-slate-400 italic">No runs yet</div>
191
+ )}
192
+
193
+ {pipelineRuns.map(run => {
194
+ const runArtifacts = getArtifactsForRun(run.run_id);
195
+ const modelCount = runArtifacts.filter(a => a.type === 'model').length;
196
+
197
+ return (
198
+ <TreeNode
199
+ key={run.run_id}
200
+ label={run.name || `Run ${run.run_id.slice(0, 8)}`}
201
+ icon={PlayCircle}
202
+ level={2}
203
+ status={run.status}
204
+ actions={
205
+ <div className="flex items-center gap-2">
206
+ {runArtifacts.length > 0 && (
207
+ <div className="flex gap-1">
208
+ {modelCount > 0 && (
209
+ <span className="flex items-center gap-0.5 text-[10px] bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400 px-1.5 py-0.5 rounded-full">
210
+ <Box className="w-3 h-3" /> {modelCount}
211
+ </span>
212
+ )}
213
+ <span className="flex items-center gap-0.5 text-[10px] bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-400 px-1.5 py-0.5 rounded-full">
214
+ <FileBox className="w-3 h-3" /> {runArtifacts.length}
215
+ </span>
216
+ </div>
217
+ )}
218
+ <span className="text-xs text-slate-400">{formatDate(run.created)}</span>
219
+ <Link
220
+ to={`/runs/${run.run_id}`}
221
+ className="text-xs text-blue-500 hover:underline"
222
+ onClick={(e) => e.stopPropagation()}
223
+ >
224
+ Details
225
+ </Link>
226
+ </div>
227
+ }
228
+ >
229
+ {runArtifacts.length === 0 && (
230
+ <div className="pl-16 py-1 text-xs text-slate-400 italic">No artifacts</div>
231
+ )}
232
+
233
+ {runArtifacts.map(artifact => (
234
+ <div
235
+ key={artifact.artifact_id}
236
+ onClick={(e) => {
237
+ e.stopPropagation();
238
+ setSelectedArtifact(artifact);
239
+ }}
240
+ className="cursor-pointer hover:bg-slate-50 dark:hover:bg-slate-800/50 rounded transition-colors"
241
+ >
242
+ <TreeNode
243
+ label={artifact.name}
244
+ icon={() => <ArtifactIcon type={artifact.type} />}
245
+ level={3}
246
+ actions={
247
+ <div className="flex items-center gap-2">
248
+ <span className={`text-xs px-1.5 py-0.5 rounded ${artifact.type === 'model'
249
+ ? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300'
250
+ : 'bg-slate-100 text-slate-600 dark:bg-slate-800 dark:text-slate-400'
251
+ }`}>
252
+ {artifact.type}
253
+ </span>
254
+ <button
255
+ onClick={(e) => {
256
+ e.stopPropagation();
257
+ downloadArtifactById(artifact.artifact_id);
258
+ }}
259
+ className="p-1 rounded hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors disabled:opacity-40"
260
+ disabled={!artifact.artifact_id}
261
+ >
262
+ <Download className="w-3 h-3 text-slate-400" />
263
+ </button>
264
+ <Info className="w-3 h-3 text-slate-400" />
265
+ </div>
266
+ }
267
+ />
268
+ </div>
269
+ ))}
270
+ </TreeNode>
271
+ );
272
+ })}
273
+ </TreeNode>
274
+ );
275
+ })}
276
+ </TreeNode>
277
+ </div>
278
+
279
+ {/* Artifact Details Modal */}
280
+ <ArtifactDetailsModal
281
+ artifact={selectedArtifact}
282
+ onClose={() => setSelectedArtifact(null)}
283
+ />
284
+ </div>
285
+ );
286
+ }
287
+
288
+ function ArtifactDetailsModal({ artifact, onClose }) {
289
+ if (!artifact) return null;
290
+
291
+ const hasTrainingHistory = artifact.training_history &&
292
+ artifact.training_history.epochs &&
293
+ artifact.training_history.epochs.length > 0;
294
+
295
+ // Prepare data for charts
296
+ const chartData = hasTrainingHistory ? artifact.training_history.epochs.map((epoch, idx) => ({
297
+ epoch,
298
+ 'Train Loss': artifact.training_history.train_loss[idx],
299
+ 'Val Loss': artifact.training_history.val_loss[idx],
300
+ 'Train Accuracy': artifact.training_history.train_accuracy[idx],
301
+ 'Val Accuracy': artifact.training_history.val_accuracy[idx]
302
+ })) : [];
303
+
304
+ return (
305
+ <div
306
+ className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
307
+ onClick={onClose}
308
+ >
309
+ <div
310
+ className="bg-white dark:bg-slate-900 rounded-2xl shadow-2xl max-w-4xl w-full max-h-[85vh] overflow-hidden"
311
+ onClick={(e) => e.stopPropagation()}
312
+ >
313
+ {/* Header */}
314
+ <div className="flex items-center justify-between p-6 border-b border-slate-200 dark:border-slate-800 bg-gradient-to-r from-blue-50 to-purple-50 dark:from-slate-800 dark:to-slate-800">
315
+ <div className="flex items-center gap-3">
316
+ <div className="p-3 bg-white dark:bg-slate-700 rounded-xl shadow-sm">
317
+ <ArtifactIcon type={artifact.type} />
318
+ </div>
319
+ <div>
320
+ <h3 className="text-xl font-bold text-slate-900 dark:text-white">{artifact.name}</h3>
321
+ <p className="text-sm text-slate-500 dark:text-slate-400 capitalize">{artifact.type}</p>
322
+ </div>
323
+ </div>
324
+ <button
325
+ onClick={onClose}
326
+ className="p-2 hover:bg-white/50 dark:hover:bg-slate-700 rounded-lg transition-colors"
327
+ >
328
+ <X size={20} className="text-slate-400" />
329
+ </button>
330
+ </div>
331
+
332
+ {/* Content */}
333
+ <div className="p-6 overflow-y-auto max-h-[calc(85vh-140px)]">
334
+ <div className="space-y-6">
335
+ {/* Properties Grid */}
336
+ {artifact.properties && Object.keys(artifact.properties).length > 0 && (
337
+ <div>
338
+ <h4 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3 flex items-center gap-2">
339
+ <Info className="w-4 h-4" />
340
+ Properties
341
+ </h4>
342
+ <div className="grid grid-cols-2 md:grid-cols-3 gap-3">
343
+ {Object.entries(artifact.properties).map(([key, value]) => (
344
+ <div key={key} className="p-3 bg-slate-50 dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700">
345
+ <span className="text-xs text-slate-500 dark:text-slate-400 block mb-1 capitalize">
346
+ {key.replace(/_/g, ' ')}
347
+ </span>
348
+ <span className="text-sm font-mono font-semibold text-slate-900 dark:text-white">
349
+ {typeof value === 'number' ? value.toLocaleString() : String(value)}
350
+ </span>
351
+ </div>
352
+ ))}
353
+ </div>
354
+ </div>
355
+ )}
356
+
357
+ {/* Training History Charts */}
358
+ {hasTrainingHistory && (
359
+ <div>
360
+ <h4 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3 flex items-center gap-2">
361
+ <TrendingUp className="w-4 h-4" />
362
+ Training History
363
+ </h4>
364
+
365
+ {/* Loss Chart */}
366
+ <div className="mb-6 bg-slate-50 dark:bg-slate-800 p-4 rounded-xl border border-slate-200 dark:border-slate-700">
367
+ <h5 className="text-xs font-medium text-slate-600 dark:text-slate-400 mb-3">Loss over Epochs</h5>
368
+ <ResponsiveContainer width="100%" height={200}>
369
+ <LineChart data={chartData}>
370
+ <CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
371
+ <XAxis
372
+ dataKey="epoch"
373
+ label={{ value: 'Epoch', position: 'insideBottom', offset: -5 }}
374
+ stroke="#94a3b8"
375
+ />
376
+ <YAxis stroke="#94a3b8" />
377
+ <Tooltip
378
+ contentStyle={{
379
+ backgroundColor: '#fff',
380
+ border: '1px solid #e2e8f0',
381
+ borderRadius: '8px'
382
+ }}
383
+ />
384
+ <Legend />
385
+ <Line type="monotone" dataKey="Train Loss" stroke="#3b82f6" strokeWidth={2} dot={false} />
386
+ <Line type="monotone" dataKey="Val Loss" stroke="#8b5cf6" strokeWidth={2} dot={false} />
387
+ </LineChart>
388
+ </ResponsiveContainer>
389
+ </div>
390
+
391
+ {/* Accuracy Chart */}
392
+ <div className="bg-slate-50 dark:bg-slate-800 p-4 rounded-xl border border-slate-200 dark:border-slate-700">
393
+ <h5 className="text-xs font-medium text-slate-600 dark:text-slate-400 mb-3">Accuracy over Epochs</h5>
394
+ <ResponsiveContainer width="100%" height={200}>
395
+ <LineChart data={chartData}>
396
+ <CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" />
397
+ <XAxis
398
+ dataKey="epoch"
399
+ label={{ value: 'Epoch', position: 'insideBottom', offset: -5 }}
400
+ stroke="#94a3b8"
401
+ />
402
+ <YAxis stroke="#94a3b8" domain={[0, 1]} />
403
+ <Tooltip
404
+ contentStyle={{
405
+ backgroundColor: '#fff',
406
+ border: '1px solid #e2e8f0',
407
+ borderRadius: '8px'
408
+ }}
409
+ />
410
+ <Legend />
411
+ <Line type="monotone" dataKey="Train Accuracy" stroke="#10b981" strokeWidth={2} dot={false} />
412
+ <Line type="monotone" dataKey="Val Accuracy" stroke="#f59e0b" strokeWidth={2} dot={false} />
413
+ </LineChart>
414
+ </ResponsiveContainer>
415
+ </div>
416
+ </div>
417
+ )}
418
+
419
+ {/* Metadata */}
420
+ <div>
421
+ <h4 className="text-sm font-semibold text-slate-700 dark:text-slate-300 mb-3 flex items-center gap-2">
422
+ <HardDrive className="w-4 h-4" />
423
+ Metadata
424
+ </h4>
425
+ <div className="space-y-2 text-sm">
426
+ <div className="flex justify-between py-2 border-b border-slate-100 dark:border-slate-800">
427
+ <span className="text-slate-500 dark:text-slate-400">Artifact ID:</span>
428
+ <span className="font-mono text-xs text-slate-700 dark:text-slate-300">{artifact.artifact_id}</span>
429
+ </div>
430
+ {artifact.step && (
431
+ <div className="flex justify-between py-2 border-b border-slate-100 dark:border-slate-800">
432
+ <span className="text-slate-500 dark:text-slate-400">Pipeline Step:</span>
433
+ <span className="font-medium text-slate-700 dark:text-slate-300">{artifact.step}</span>
434
+ </div>
435
+ )}
436
+ {artifact.path && (
437
+ <div className="flex justify-between py-2 border-b border-slate-100 dark:border-slate-800">
438
+ <span className="text-slate-500 dark:text-slate-400">Path:</span>
439
+ <span className="font-mono text-xs text-slate-700 dark:text-slate-300">{artifact.path}</span>
440
+ </div>
441
+ )}
442
+ {artifact.size_bytes && (
443
+ <div className="flex justify-between py-2 border-b border-slate-100 dark:border-slate-800">
444
+ <span className="text-slate-500 dark:text-slate-400">Size:</span>
445
+ <span className="text-slate-700 dark:text-slate-300">
446
+ {(artifact.size_bytes / 1024 / 1024).toFixed(2)} MB
447
+ </span>
448
+ </div>
449
+ )}
450
+ </div>
451
+ </div>
452
+ </div>
453
+ </div>
454
+
455
+ {/* Footer */}
456
+ <div className="p-4 border-t border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-900/50 flex justify-end">
457
+ <button
458
+ onClick={onClose}
459
+ className="px-4 py-2 text-sm font-medium text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 rounded-lg transition-colors"
460
+ >
461
+ Close
462
+ </button>
463
+ </div>
464
+ </div>
465
+ </div>
466
+ );
467
+ }