flowyml 1.1.0__py3-none-any.whl → 1.3.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/__init__.py +3 -0
- flowyml/assets/base.py +10 -0
- flowyml/assets/metrics.py +6 -0
- flowyml/cli/main.py +108 -2
- flowyml/cli/run.py +9 -2
- flowyml/core/execution_status.py +52 -0
- flowyml/core/hooks.py +106 -0
- flowyml/core/observability.py +210 -0
- flowyml/core/orchestrator.py +274 -0
- flowyml/core/pipeline.py +193 -231
- flowyml/core/project.py +34 -2
- flowyml/core/remote_orchestrator.py +109 -0
- flowyml/core/resources.py +22 -5
- flowyml/core/retry_policy.py +80 -0
- flowyml/core/step.py +18 -1
- flowyml/core/submission_result.py +53 -0
- flowyml/core/versioning.py +2 -2
- flowyml/integrations/keras.py +95 -22
- flowyml/monitoring/alerts.py +2 -2
- flowyml/stacks/__init__.py +15 -0
- flowyml/stacks/aws.py +599 -0
- flowyml/stacks/azure.py +295 -0
- flowyml/stacks/components.py +24 -2
- flowyml/stacks/gcp.py +158 -11
- flowyml/stacks/local.py +5 -0
- flowyml/storage/artifacts.py +15 -5
- flowyml/storage/materializers/__init__.py +2 -0
- flowyml/storage/materializers/cloudpickle.py +74 -0
- flowyml/storage/metadata.py +166 -5
- flowyml/ui/backend/main.py +41 -1
- flowyml/ui/backend/routers/assets.py +356 -15
- flowyml/ui/backend/routers/client.py +46 -0
- flowyml/ui/backend/routers/execution.py +13 -2
- flowyml/ui/backend/routers/experiments.py +48 -12
- flowyml/ui/backend/routers/metrics.py +213 -0
- flowyml/ui/backend/routers/pipelines.py +63 -7
- flowyml/ui/backend/routers/projects.py +33 -7
- flowyml/ui/backend/routers/runs.py +150 -8
- flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +1 -0
- flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +592 -0
- flowyml/ui/frontend/dist/index.html +2 -2
- flowyml/ui/frontend/src/App.jsx +4 -1
- flowyml/ui/frontend/src/app/assets/page.jsx +260 -230
- flowyml/ui/frontend/src/app/dashboard/page.jsx +38 -7
- flowyml/ui/frontend/src/app/experiments/page.jsx +61 -314
- flowyml/ui/frontend/src/app/observability/page.jsx +277 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +79 -402
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectArtifactsList.jsx +151 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +145 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHeader.jsx +45 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHierarchy.jsx +467 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +253 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectPipelinesList.jsx +105 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRelations.jsx +189 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRunsList.jsx +136 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectTabs.jsx +95 -0
- flowyml/ui/frontend/src/app/projects/[projectId]/page.jsx +326 -0
- flowyml/ui/frontend/src/app/projects/page.jsx +13 -3
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +79 -10
- flowyml/ui/frontend/src/app/runs/page.jsx +82 -424
- flowyml/ui/frontend/src/app/settings/page.jsx +1 -0
- flowyml/ui/frontend/src/app/tokens/page.jsx +62 -16
- flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +373 -0
- flowyml/ui/frontend/src/components/AssetLineageGraph.jsx +291 -0
- flowyml/ui/frontend/src/components/AssetStatsDashboard.jsx +302 -0
- flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +477 -0
- flowyml/ui/frontend/src/components/ExperimentDetailsPanel.jsx +227 -0
- flowyml/ui/frontend/src/components/NavigationTree.jsx +401 -0
- flowyml/ui/frontend/src/components/PipelineDetailsPanel.jsx +239 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +67 -3
- flowyml/ui/frontend/src/components/ProjectSelector.jsx +115 -0
- flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +298 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +48 -1
- flowyml/ui/frontend/src/components/plugins/ZenMLIntegration.jsx +106 -0
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +52 -26
- flowyml/ui/frontend/src/components/ui/DataView.jsx +35 -17
- flowyml/ui/frontend/src/components/ui/ErrorBoundary.jsx +118 -0
- flowyml/ui/frontend/src/contexts/ProjectContext.jsx +2 -2
- flowyml/ui/frontend/src/contexts/ToastContext.jsx +116 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +5 -1
- flowyml/ui/frontend/src/router/index.jsx +4 -0
- flowyml/ui/frontend/src/utils/date.js +10 -0
- flowyml/ui/frontend/src/utils/downloads.js +11 -0
- flowyml/utils/config.py +6 -0
- flowyml/utils/stack_config.py +45 -3
- {flowyml-1.1.0.dist-info → flowyml-1.3.0.dist-info}/METADATA +113 -12
- {flowyml-1.1.0.dist-info → flowyml-1.3.0.dist-info}/RECORD +90 -53
- {flowyml-1.1.0.dist-info → flowyml-1.3.0.dist-info}/licenses/LICENSE +1 -1
- flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +0 -448
- flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +0 -1
- {flowyml-1.1.0.dist-info → flowyml-1.3.0.dist-info}/WHEEL +0 -0
- {flowyml-1.1.0.dist-info → flowyml-1.3.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
|
+
}
|