flowyml 1.1.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 +207 -0
- flowyml/assets/__init__.py +22 -0
- flowyml/assets/artifact.py +40 -0
- flowyml/assets/base.py +209 -0
- flowyml/assets/dataset.py +100 -0
- flowyml/assets/featureset.py +301 -0
- flowyml/assets/metrics.py +104 -0
- flowyml/assets/model.py +82 -0
- flowyml/assets/registry.py +157 -0
- flowyml/assets/report.py +315 -0
- flowyml/cli/__init__.py +5 -0
- flowyml/cli/experiment.py +232 -0
- flowyml/cli/init.py +256 -0
- flowyml/cli/main.py +327 -0
- flowyml/cli/run.py +75 -0
- flowyml/cli/stack_cli.py +532 -0
- flowyml/cli/ui.py +33 -0
- flowyml/core/__init__.py +68 -0
- flowyml/core/advanced_cache.py +274 -0
- flowyml/core/approval.py +64 -0
- flowyml/core/cache.py +203 -0
- flowyml/core/checkpoint.py +148 -0
- flowyml/core/conditional.py +373 -0
- flowyml/core/context.py +155 -0
- flowyml/core/error_handling.py +419 -0
- flowyml/core/executor.py +354 -0
- flowyml/core/graph.py +185 -0
- flowyml/core/parallel.py +452 -0
- flowyml/core/pipeline.py +764 -0
- flowyml/core/project.py +253 -0
- flowyml/core/resources.py +424 -0
- flowyml/core/scheduler.py +630 -0
- flowyml/core/scheduler_config.py +32 -0
- flowyml/core/step.py +201 -0
- flowyml/core/step_grouping.py +292 -0
- flowyml/core/templates.py +226 -0
- flowyml/core/versioning.py +217 -0
- flowyml/integrations/__init__.py +1 -0
- flowyml/integrations/keras.py +134 -0
- flowyml/monitoring/__init__.py +1 -0
- flowyml/monitoring/alerts.py +57 -0
- flowyml/monitoring/data.py +102 -0
- flowyml/monitoring/llm.py +160 -0
- flowyml/monitoring/monitor.py +57 -0
- flowyml/monitoring/notifications.py +246 -0
- flowyml/registry/__init__.py +5 -0
- flowyml/registry/model_registry.py +491 -0
- flowyml/registry/pipeline_registry.py +55 -0
- flowyml/stacks/__init__.py +27 -0
- flowyml/stacks/base.py +77 -0
- flowyml/stacks/bridge.py +288 -0
- flowyml/stacks/components.py +155 -0
- flowyml/stacks/gcp.py +499 -0
- flowyml/stacks/local.py +112 -0
- flowyml/stacks/migration.py +97 -0
- flowyml/stacks/plugin_config.py +78 -0
- flowyml/stacks/plugins.py +401 -0
- flowyml/stacks/registry.py +226 -0
- flowyml/storage/__init__.py +26 -0
- flowyml/storage/artifacts.py +246 -0
- flowyml/storage/materializers/__init__.py +20 -0
- flowyml/storage/materializers/base.py +133 -0
- flowyml/storage/materializers/keras.py +185 -0
- flowyml/storage/materializers/numpy.py +94 -0
- flowyml/storage/materializers/pandas.py +142 -0
- flowyml/storage/materializers/pytorch.py +135 -0
- flowyml/storage/materializers/sklearn.py +110 -0
- flowyml/storage/materializers/tensorflow.py +152 -0
- flowyml/storage/metadata.py +931 -0
- flowyml/tracking/__init__.py +1 -0
- flowyml/tracking/experiment.py +211 -0
- flowyml/tracking/leaderboard.py +191 -0
- flowyml/tracking/runs.py +145 -0
- flowyml/ui/__init__.py +15 -0
- flowyml/ui/backend/Dockerfile +31 -0
- flowyml/ui/backend/__init__.py +0 -0
- flowyml/ui/backend/auth.py +163 -0
- flowyml/ui/backend/main.py +187 -0
- flowyml/ui/backend/routers/__init__.py +0 -0
- flowyml/ui/backend/routers/assets.py +45 -0
- flowyml/ui/backend/routers/execution.py +179 -0
- flowyml/ui/backend/routers/experiments.py +49 -0
- flowyml/ui/backend/routers/leaderboard.py +118 -0
- flowyml/ui/backend/routers/notifications.py +72 -0
- flowyml/ui/backend/routers/pipelines.py +110 -0
- flowyml/ui/backend/routers/plugins.py +192 -0
- flowyml/ui/backend/routers/projects.py +85 -0
- flowyml/ui/backend/routers/runs.py +66 -0
- flowyml/ui/backend/routers/schedules.py +222 -0
- flowyml/ui/backend/routers/traces.py +84 -0
- flowyml/ui/frontend/Dockerfile +20 -0
- flowyml/ui/frontend/README.md +315 -0
- flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +448 -0
- flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +1 -0
- flowyml/ui/frontend/dist/index.html +16 -0
- flowyml/ui/frontend/index.html +15 -0
- flowyml/ui/frontend/nginx.conf +26 -0
- flowyml/ui/frontend/package-lock.json +3545 -0
- flowyml/ui/frontend/package.json +33 -0
- flowyml/ui/frontend/postcss.config.js +6 -0
- flowyml/ui/frontend/src/App.jsx +21 -0
- flowyml/ui/frontend/src/app/assets/page.jsx +397 -0
- flowyml/ui/frontend/src/app/dashboard/page.jsx +295 -0
- flowyml/ui/frontend/src/app/experiments/[experimentId]/page.jsx +255 -0
- flowyml/ui/frontend/src/app/experiments/page.jsx +360 -0
- flowyml/ui/frontend/src/app/leaderboard/page.jsx +133 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +454 -0
- flowyml/ui/frontend/src/app/plugins/page.jsx +48 -0
- flowyml/ui/frontend/src/app/projects/page.jsx +292 -0
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +682 -0
- flowyml/ui/frontend/src/app/runs/page.jsx +470 -0
- flowyml/ui/frontend/src/app/schedules/page.jsx +585 -0
- flowyml/ui/frontend/src/app/settings/page.jsx +314 -0
- flowyml/ui/frontend/src/app/tokens/page.jsx +456 -0
- flowyml/ui/frontend/src/app/traces/page.jsx +246 -0
- flowyml/ui/frontend/src/components/Layout.jsx +108 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +295 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +72 -0
- flowyml/ui/frontend/src/components/plugins/AddPluginDialog.jsx +121 -0
- flowyml/ui/frontend/src/components/plugins/InstalledPlugins.jsx +124 -0
- flowyml/ui/frontend/src/components/plugins/PluginBrowser.jsx +167 -0
- flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +60 -0
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +145 -0
- flowyml/ui/frontend/src/components/ui/Badge.jsx +26 -0
- flowyml/ui/frontend/src/components/ui/Button.jsx +34 -0
- flowyml/ui/frontend/src/components/ui/Card.jsx +44 -0
- flowyml/ui/frontend/src/components/ui/CodeSnippet.jsx +38 -0
- flowyml/ui/frontend/src/components/ui/CollapsibleCard.jsx +53 -0
- flowyml/ui/frontend/src/components/ui/DataView.jsx +175 -0
- flowyml/ui/frontend/src/components/ui/EmptyState.jsx +49 -0
- flowyml/ui/frontend/src/components/ui/ExecutionStatus.jsx +122 -0
- flowyml/ui/frontend/src/components/ui/KeyValue.jsx +25 -0
- flowyml/ui/frontend/src/components/ui/ProjectSelector.jsx +134 -0
- flowyml/ui/frontend/src/contexts/ProjectContext.jsx +79 -0
- flowyml/ui/frontend/src/contexts/ThemeContext.jsx +54 -0
- flowyml/ui/frontend/src/index.css +11 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +23 -0
- flowyml/ui/frontend/src/main.jsx +10 -0
- flowyml/ui/frontend/src/router/index.jsx +39 -0
- flowyml/ui/frontend/src/services/pluginService.js +90 -0
- flowyml/ui/frontend/src/utils/api.js +47 -0
- flowyml/ui/frontend/src/utils/cn.js +6 -0
- flowyml/ui/frontend/tailwind.config.js +31 -0
- flowyml/ui/frontend/vite.config.js +21 -0
- flowyml/ui/utils.py +77 -0
- flowyml/utils/__init__.py +67 -0
- flowyml/utils/config.py +308 -0
- flowyml/utils/debug.py +240 -0
- flowyml/utils/environment.py +346 -0
- flowyml/utils/git.py +319 -0
- flowyml/utils/logging.py +61 -0
- flowyml/utils/performance.py +314 -0
- flowyml/utils/stack_config.py +296 -0
- flowyml/utils/validation.py +270 -0
- flowyml-1.1.0.dist-info/METADATA +372 -0
- flowyml-1.1.0.dist-info/RECORD +159 -0
- flowyml-1.1.0.dist-info/WHEEL +4 -0
- flowyml-1.1.0.dist-info/entry_points.txt +3 -0
- flowyml-1.1.0.dist-info/licenses/LICENSE +17 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { fetchApi } from '../../utils/api';
|
|
3
|
+
import { Link } from 'react-router-dom';
|
|
4
|
+
import { Activity, Layers, Database, TrendingUp, Clock, CheckCircle, XCircle, Zap, ArrowRight } from 'lucide-react';
|
|
5
|
+
import { Card } from '../../components/ui/Card';
|
|
6
|
+
import { Badge } from '../../components/ui/Badge';
|
|
7
|
+
import { format } from 'date-fns';
|
|
8
|
+
import { motion } from 'framer-motion';
|
|
9
|
+
import { useProject } from '../../contexts/ProjectContext';
|
|
10
|
+
|
|
11
|
+
export function Dashboard() {
|
|
12
|
+
const [stats, setStats] = useState(null);
|
|
13
|
+
const [recentRuns, setRecentRuns] = useState([]);
|
|
14
|
+
const [loading, setLoading] = useState(true);
|
|
15
|
+
const { selectedProject } = useProject();
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const fetchData = async () => {
|
|
19
|
+
setLoading(true);
|
|
20
|
+
try {
|
|
21
|
+
const statsUrl = selectedProject
|
|
22
|
+
? `/api/stats?project=${encodeURIComponent(selectedProject)}`
|
|
23
|
+
: '/api/stats';
|
|
24
|
+
const runsUrl = selectedProject
|
|
25
|
+
? `/api/runs?limit=5&project=${encodeURIComponent(selectedProject)}`
|
|
26
|
+
: '/api/runs?limit=5';
|
|
27
|
+
|
|
28
|
+
const [statsData, runsData] = await Promise.all([
|
|
29
|
+
fetchApi(statsUrl).then(res => res.json()),
|
|
30
|
+
fetchApi(runsUrl).then(res => res.json())
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
setStats(statsData);
|
|
34
|
+
setRecentRuns(runsData.runs || []);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error(err);
|
|
37
|
+
} finally {
|
|
38
|
+
setLoading(false);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
fetchData();
|
|
42
|
+
}, [selectedProject]);
|
|
43
|
+
|
|
44
|
+
if (loading) {
|
|
45
|
+
return (
|
|
46
|
+
<div className="flex items-center justify-center h-96">
|
|
47
|
+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const container = {
|
|
53
|
+
hidden: { opacity: 0 },
|
|
54
|
+
show: {
|
|
55
|
+
opacity: 1,
|
|
56
|
+
transition: {
|
|
57
|
+
staggerChildren: 0.1
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const item = {
|
|
63
|
+
hidden: { opacity: 0, y: 20 },
|
|
64
|
+
show: { opacity: 1, y: 0 }
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<motion.div
|
|
69
|
+
initial="hidden"
|
|
70
|
+
animate="show"
|
|
71
|
+
variants={container}
|
|
72
|
+
className="space-y-8"
|
|
73
|
+
>
|
|
74
|
+
{/* Welcome Header */}
|
|
75
|
+
<motion.div variants={item} className="relative overflow-hidden bg-gradient-to-br from-primary-500 via-primary-600 to-purple-600 rounded-2xl p-8 text-white shadow-xl">
|
|
76
|
+
<div className="absolute top-0 right-0 w-64 h-64 bg-white/10 rounded-full -mr-32 -mt-32" />
|
|
77
|
+
<div className="absolute bottom-0 left-0 w-48 h-48 bg-white/10 rounded-full -ml-24 -mb-24" />
|
|
78
|
+
|
|
79
|
+
<div className="relative z-10">
|
|
80
|
+
<div className="flex items-center gap-3 mb-3">
|
|
81
|
+
<Zap size={32} className="text-yellow-300" />
|
|
82
|
+
<h1 className="text-4xl font-bold">Welcome to flowyml</h1>
|
|
83
|
+
</div>
|
|
84
|
+
<p className="text-primary-100 text-lg max-w-2xl">
|
|
85
|
+
Your lightweight, artifact-centric ML orchestration platform. Build, run, and track your ML pipelines with ease.
|
|
86
|
+
</p>
|
|
87
|
+
<div className="mt-6 flex gap-3">
|
|
88
|
+
<Link to="/pipelines">
|
|
89
|
+
<button className="px-6 py-2.5 bg-white text-primary-600 rounded-lg font-semibold hover:bg-primary-50 transition-colors shadow-lg">
|
|
90
|
+
View Pipelines
|
|
91
|
+
</button>
|
|
92
|
+
</Link>
|
|
93
|
+
<Link to="/runs">
|
|
94
|
+
<button className="px-6 py-2.5 bg-primary-700/50 backdrop-blur-sm text-white rounded-lg font-semibold hover:bg-primary-700/70 transition-colors border border-white/20">
|
|
95
|
+
Recent Runs
|
|
96
|
+
</button>
|
|
97
|
+
</Link>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
</motion.div>
|
|
101
|
+
|
|
102
|
+
{/* Stats Grid */}
|
|
103
|
+
<motion.div variants={item} className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
104
|
+
<MetricCard
|
|
105
|
+
icon={<Layers size={24} />}
|
|
106
|
+
label="Total Pipelines"
|
|
107
|
+
value={stats?.pipelines || 0}
|
|
108
|
+
trend="+12%"
|
|
109
|
+
color="blue"
|
|
110
|
+
/>
|
|
111
|
+
<MetricCard
|
|
112
|
+
icon={<Activity size={24} />}
|
|
113
|
+
label="Pipeline Runs"
|
|
114
|
+
value={stats?.runs || 0}
|
|
115
|
+
trend="+23%"
|
|
116
|
+
color="purple"
|
|
117
|
+
/>
|
|
118
|
+
<MetricCard
|
|
119
|
+
icon={<Database size={24} />}
|
|
120
|
+
label="Artifacts"
|
|
121
|
+
value={stats?.artifacts || 0}
|
|
122
|
+
trend="+8%"
|
|
123
|
+
color="emerald"
|
|
124
|
+
/>
|
|
125
|
+
<MetricCard
|
|
126
|
+
icon={<CheckCircle size={24} />}
|
|
127
|
+
label="Success Rate"
|
|
128
|
+
value={stats?.runs > 0 ? `${Math.round((stats.completed_runs / stats.runs) * 100)}%` : '0%'}
|
|
129
|
+
trend="+5%"
|
|
130
|
+
color="cyan"
|
|
131
|
+
/>
|
|
132
|
+
</motion.div>
|
|
133
|
+
|
|
134
|
+
{/* Recent Activity */}
|
|
135
|
+
<motion.div variants={item} className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
136
|
+
{/* Recent Runs */}
|
|
137
|
+
<div className="lg:col-span-2">
|
|
138
|
+
<div className="flex items-center justify-between mb-6">
|
|
139
|
+
<h3 className="text-xl font-bold text-slate-900 flex items-center gap-2">
|
|
140
|
+
<Clock className="text-primary-500" size={24} />
|
|
141
|
+
Recent Runs
|
|
142
|
+
</h3>
|
|
143
|
+
<Link to="/runs" className="text-sm font-semibold text-primary-600 hover:text-primary-700 flex items-center gap-1">
|
|
144
|
+
View All <ArrowRight size={16} />
|
|
145
|
+
</Link>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div className="space-y-3">
|
|
149
|
+
{recentRuns.length > 0 ? (
|
|
150
|
+
recentRuns.map((run, index) => (
|
|
151
|
+
<RecentRunCard key={run.run_id} run={run} index={index} />
|
|
152
|
+
))
|
|
153
|
+
) : (
|
|
154
|
+
<Card className="p-12 text-center border-dashed">
|
|
155
|
+
<Activity className="mx-auto text-slate-300 mb-3" size={32} />
|
|
156
|
+
<p className="text-slate-500">No recent runs</p>
|
|
157
|
+
</Card>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
{/* Quick Stats */}
|
|
163
|
+
<div>
|
|
164
|
+
<h3 className="text-xl font-bold text-slate-900 mb-6 flex items-center gap-2">
|
|
165
|
+
<TrendingUp className="text-primary-500" size={24} />
|
|
166
|
+
Quick Stats
|
|
167
|
+
</h3>
|
|
168
|
+
|
|
169
|
+
<div className="space-y-3">
|
|
170
|
+
<QuickStatCard
|
|
171
|
+
label="Completed Today"
|
|
172
|
+
value={stats?.completed_runs || 0}
|
|
173
|
+
icon={<CheckCircle size={18} />}
|
|
174
|
+
color="emerald"
|
|
175
|
+
/>
|
|
176
|
+
<QuickStatCard
|
|
177
|
+
label="Failed Runs"
|
|
178
|
+
value={stats?.failed_runs || 0}
|
|
179
|
+
icon={<XCircle size={18} />}
|
|
180
|
+
color="rose"
|
|
181
|
+
/>
|
|
182
|
+
<QuickStatCard
|
|
183
|
+
label="Avg Duration"
|
|
184
|
+
value={stats?.avg_duration ? `${stats.avg_duration.toFixed(1)}s` : '0s'}
|
|
185
|
+
icon={<Clock size={18} />}
|
|
186
|
+
color="blue"
|
|
187
|
+
/>
|
|
188
|
+
<QuickStatCard
|
|
189
|
+
label="Cache Hit Rate"
|
|
190
|
+
value="87%"
|
|
191
|
+
icon={<Zap size={18} />}
|
|
192
|
+
color="amber"
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</motion.div>
|
|
197
|
+
</motion.div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function MetricCard({ icon, label, value, trend, color }) {
|
|
202
|
+
const colorClasses = {
|
|
203
|
+
blue: "from-blue-500 to-cyan-500",
|
|
204
|
+
purple: "from-purple-500 to-pink-500",
|
|
205
|
+
emerald: "from-emerald-500 to-teal-500",
|
|
206
|
+
cyan: "from-cyan-500 to-blue-500"
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<Card className="relative overflow-hidden group hover:shadow-lg transition-all duration-200">
|
|
211
|
+
<div className={`absolute inset-0 bg-gradient-to-br ${colorClasses[color]} opacity-0 group-hover:opacity-5 transition-opacity`} />
|
|
212
|
+
|
|
213
|
+
<div className="relative">
|
|
214
|
+
<div className="flex items-start justify-between mb-4">
|
|
215
|
+
<div className={`p-3 rounded-xl bg-gradient-to-br ${colorClasses[color]} text-white shadow-lg`}>
|
|
216
|
+
{icon}
|
|
217
|
+
</div>
|
|
218
|
+
<span className="text-xs font-semibold text-emerald-600 bg-emerald-50 px-2 py-1 rounded-full">
|
|
219
|
+
{trend}
|
|
220
|
+
</span>
|
|
221
|
+
</div>
|
|
222
|
+
<p className="text-sm text-slate-500 font-medium mb-1">{label}</p>
|
|
223
|
+
<p className="text-3xl font-bold text-slate-900">{value}</p>
|
|
224
|
+
</div>
|
|
225
|
+
</Card>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function RecentRunCard({ run, index }) {
|
|
230
|
+
const statusConfig = {
|
|
231
|
+
completed: { icon: <CheckCircle size={16} />, color: 'text-emerald-500', bg: 'bg-emerald-50' },
|
|
232
|
+
failed: { icon: <XCircle size={16} />, color: 'text-rose-500', bg: 'bg-rose-50' },
|
|
233
|
+
running: { icon: <Activity size={16} className="animate-pulse" />, color: 'text-amber-500', bg: 'bg-amber-50' }
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const config = statusConfig[run.status] || statusConfig.completed;
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<motion.div
|
|
240
|
+
initial={{ opacity: 0, x: -20 }}
|
|
241
|
+
animate={{ opacity: 1, x: 0 }}
|
|
242
|
+
transition={{ delay: index * 0.1 }}
|
|
243
|
+
>
|
|
244
|
+
<Link to={`/runs/${run.run_id}`}>
|
|
245
|
+
<Card className="group hover:shadow-md hover:border-primary-200 transition-all duration-200">
|
|
246
|
+
<div className="flex items-center gap-3">
|
|
247
|
+
<div className={`p-2 rounded-lg ${config.bg} ${config.color}`}>
|
|
248
|
+
{config.icon}
|
|
249
|
+
</div>
|
|
250
|
+
<div className="flex-1 min-w-0">
|
|
251
|
+
<h4 className="font-semibold text-slate-900 truncate group-hover:text-primary-600 transition-colors">
|
|
252
|
+
{run.pipeline_name}
|
|
253
|
+
</h4>
|
|
254
|
+
<div className="flex items-center gap-2 text-xs text-slate-500 mt-0.5">
|
|
255
|
+
<span className="font-mono">{run.run_id.substring(0, 8)}</span>
|
|
256
|
+
{run.start_time && (
|
|
257
|
+
<>
|
|
258
|
+
<span>•</span>
|
|
259
|
+
<span>{format(new Date(run.start_time), 'MMM d, HH:mm')}</span>
|
|
260
|
+
</>
|
|
261
|
+
)}
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
<Badge variant={run.status === 'completed' ? 'success' : run.status === 'failed' ? 'danger' : 'warning'} className="text-xs">
|
|
265
|
+
{run.status}
|
|
266
|
+
</Badge>
|
|
267
|
+
</div>
|
|
268
|
+
</Card>
|
|
269
|
+
</Link>
|
|
270
|
+
</motion.div>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function QuickStatCard({ label, value, icon, color }) {
|
|
275
|
+
const colorClasses = {
|
|
276
|
+
emerald: "bg-emerald-50 text-emerald-600",
|
|
277
|
+
rose: "bg-rose-50 text-rose-600",
|
|
278
|
+
blue: "bg-blue-50 text-blue-600",
|
|
279
|
+
amber: "bg-amber-50 text-amber-600"
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<Card className="hover:shadow-md transition-shadow duration-200">
|
|
284
|
+
<div className="flex items-center justify-between">
|
|
285
|
+
<div>
|
|
286
|
+
<p className="text-sm text-slate-500 font-medium mb-1">{label}</p>
|
|
287
|
+
<p className="text-2xl font-bold text-slate-900">{value}</p>
|
|
288
|
+
</div>
|
|
289
|
+
<div className={`p-2.5 rounded-lg ${colorClasses[color]}`}>
|
|
290
|
+
{icon}
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
</Card>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { fetchApi } from '../../../utils/api';
|
|
3
|
+
import { useParams, Link } from 'react-router-dom';
|
|
4
|
+
import { Activity, ChevronRight, FlaskConical, TrendingUp, Calendar, BarChart3 } from 'lucide-react';
|
|
5
|
+
import { Card } from '../../../components/ui/Card';
|
|
6
|
+
import { Badge } from '../../../components/ui/Badge';
|
|
7
|
+
import { Button } from '../../../components/ui/Button';
|
|
8
|
+
import { format } from 'date-fns';
|
|
9
|
+
import { motion } from 'framer-motion';
|
|
10
|
+
|
|
11
|
+
export function ExperimentDetails() {
|
|
12
|
+
const { experimentId } = useParams();
|
|
13
|
+
const [experiment, setExperiment] = useState(null);
|
|
14
|
+
const [loading, setLoading] = useState(true);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
fetchApi(`/api/experiments/${experimentId}`)
|
|
18
|
+
.then(res => res.json())
|
|
19
|
+
.then(data => {
|
|
20
|
+
setExperiment(data);
|
|
21
|
+
setLoading(false);
|
|
22
|
+
})
|
|
23
|
+
.catch(err => {
|
|
24
|
+
console.error(err);
|
|
25
|
+
setLoading(false);
|
|
26
|
+
});
|
|
27
|
+
}, [experimentId]);
|
|
28
|
+
|
|
29
|
+
if (loading) {
|
|
30
|
+
return (
|
|
31
|
+
<div className="flex items-center justify-center h-96">
|
|
32
|
+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!experiment) {
|
|
38
|
+
return (
|
|
39
|
+
<div className="p-8 text-center">
|
|
40
|
+
<p className="text-slate-500">Experiment not found</p>
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const container = {
|
|
46
|
+
hidden: { opacity: 0 },
|
|
47
|
+
show: {
|
|
48
|
+
opacity: 1,
|
|
49
|
+
transition: {
|
|
50
|
+
staggerChildren: 0.1
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const item = {
|
|
56
|
+
hidden: { opacity: 0, y: 20 },
|
|
57
|
+
show: { opacity: 1, y: 0 }
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<motion.div
|
|
62
|
+
initial="hidden"
|
|
63
|
+
animate="show"
|
|
64
|
+
variants={container}
|
|
65
|
+
className="space-y-8"
|
|
66
|
+
>
|
|
67
|
+
{/* Header */}
|
|
68
|
+
<motion.div variants={item} className="bg-white p-6 rounded-xl border border-slate-100 shadow-sm">
|
|
69
|
+
<div className="flex items-center gap-2 mb-3">
|
|
70
|
+
<Link to="/experiments" className="text-sm text-slate-500 hover:text-slate-700 transition-colors">
|
|
71
|
+
Experiments
|
|
72
|
+
</Link>
|
|
73
|
+
<ChevronRight size={14} className="text-slate-300" />
|
|
74
|
+
<span className="text-sm text-slate-900 font-medium">{experiment.name}</span>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div className="flex items-start justify-between">
|
|
78
|
+
<div className="flex items-center gap-4">
|
|
79
|
+
<div className="p-3 bg-gradient-to-br from-purple-500 to-pink-500 rounded-xl shadow-lg">
|
|
80
|
+
<FlaskConical className="text-white" size={28} />
|
|
81
|
+
</div>
|
|
82
|
+
<div>
|
|
83
|
+
<h2 className="text-3xl font-bold text-slate-900 tracking-tight">{experiment.name}</h2>
|
|
84
|
+
<p className="text-slate-500 mt-1">{experiment.description || 'No description'}</p>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</motion.div>
|
|
89
|
+
|
|
90
|
+
{/* Stats */}
|
|
91
|
+
<motion.div variants={item} className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
92
|
+
<StatsCard
|
|
93
|
+
icon={<Activity size={24} />}
|
|
94
|
+
label="Total Runs"
|
|
95
|
+
value={experiment.runs?.length || 0}
|
|
96
|
+
color="blue"
|
|
97
|
+
/>
|
|
98
|
+
<StatsCard
|
|
99
|
+
icon={<TrendingUp size={24} />}
|
|
100
|
+
label="Best Performance"
|
|
101
|
+
value={getBestMetric(experiment.runs)}
|
|
102
|
+
color="emerald"
|
|
103
|
+
/>
|
|
104
|
+
<StatsCard
|
|
105
|
+
icon={<Calendar size={24} />}
|
|
106
|
+
label="Last Run"
|
|
107
|
+
value={getLastRunDate(experiment.runs)}
|
|
108
|
+
color="purple"
|
|
109
|
+
/>
|
|
110
|
+
</motion.div>
|
|
111
|
+
|
|
112
|
+
{/* Runs Comparison Table */}
|
|
113
|
+
<motion.div variants={item}>
|
|
114
|
+
<div className="flex items-center gap-2 mb-6">
|
|
115
|
+
<BarChart3 className="text-primary-500" size={24} />
|
|
116
|
+
<h3 className="text-xl font-bold text-slate-900">Runs Comparison</h3>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{experiment.runs && experiment.runs.length > 0 ? (
|
|
120
|
+
<Card className="p-0 overflow-hidden">
|
|
121
|
+
<div className="overflow-x-auto">
|
|
122
|
+
<table className="w-full text-left">
|
|
123
|
+
<thead className="bg-gradient-to-r from-slate-50 to-slate-100/50 border-b border-slate-200">
|
|
124
|
+
<tr>
|
|
125
|
+
<th className="px-6 py-4 font-semibold text-xs text-slate-600 uppercase tracking-wider">Run ID</th>
|
|
126
|
+
<th className="px-6 py-4 font-semibold text-xs text-slate-600 uppercase tracking-wider">Date</th>
|
|
127
|
+
<th className="px-6 py-4 font-semibold text-xs text-slate-600 uppercase tracking-wider">Metrics</th>
|
|
128
|
+
<th className="px-6 py-4 font-semibold text-xs text-slate-600 uppercase tracking-wider">Parameters</th>
|
|
129
|
+
<th className="px-6 py-4 font-semibold text-xs text-slate-600 uppercase tracking-wider"></th>
|
|
130
|
+
</tr>
|
|
131
|
+
</thead>
|
|
132
|
+
<tbody className="divide-y divide-slate-100">
|
|
133
|
+
{experiment.runs.map((run, index) => (
|
|
134
|
+
<motion.tr
|
|
135
|
+
key={run.run_id}
|
|
136
|
+
initial={{ opacity: 0, x: -20 }}
|
|
137
|
+
animate={{ opacity: 1, x: 0 }}
|
|
138
|
+
transition={{ delay: index * 0.05 }}
|
|
139
|
+
className="hover:bg-slate-50/70 transition-colors group"
|
|
140
|
+
>
|
|
141
|
+
<td className="px-6 py-4 font-mono text-sm text-slate-700 font-medium">
|
|
142
|
+
<div className="flex items-center gap-2">
|
|
143
|
+
<div className="w-2 h-2 rounded-full bg-primary-400 opacity-0 group-hover:opacity-100 transition-opacity" />
|
|
144
|
+
{run.run_id.substring(0, 12)}
|
|
145
|
+
</div>
|
|
146
|
+
</td>
|
|
147
|
+
<td className="px-6 py-4 text-sm text-slate-500">
|
|
148
|
+
{run.timestamp ? format(new Date(run.timestamp), 'MMM d, HH:mm:ss') : '-'}
|
|
149
|
+
</td>
|
|
150
|
+
<td className="px-6 py-4">
|
|
151
|
+
<div className="flex flex-wrap gap-2">
|
|
152
|
+
{Object.entries(run.metrics || {}).map(([k, v]) => (
|
|
153
|
+
<Badge key={k} variant="outline" className="font-mono text-xs bg-blue-50 text-blue-700 border-blue-200">
|
|
154
|
+
{k}: {typeof v === 'number' ? v.toFixed(4) : v}
|
|
155
|
+
</Badge>
|
|
156
|
+
))}
|
|
157
|
+
{Object.keys(run.metrics || {}).length === 0 && (
|
|
158
|
+
<span className="text-xs text-slate-400 italic">No metrics</span>
|
|
159
|
+
)}
|
|
160
|
+
</div>
|
|
161
|
+
</td>
|
|
162
|
+
<td className="px-6 py-4">
|
|
163
|
+
<div className="flex flex-wrap gap-2">
|
|
164
|
+
{Object.entries(run.parameters || {}).map(([k, v]) => (
|
|
165
|
+
<span key={k} className="text-xs text-slate-600 bg-slate-100 px-2.5 py-1 rounded-md font-medium">
|
|
166
|
+
{k}={String(v)}
|
|
167
|
+
</span>
|
|
168
|
+
))}
|
|
169
|
+
{Object.keys(run.parameters || {}).length === 0 && (
|
|
170
|
+
<span className="text-xs text-slate-400 italic">No params</span>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
</td>
|
|
174
|
+
<td className="px-6 py-4 text-right">
|
|
175
|
+
<Link to={`/runs/${run.run_id}`}>
|
|
176
|
+
<Button
|
|
177
|
+
variant="ghost"
|
|
178
|
+
size="sm"
|
|
179
|
+
className="opacity-0 group-hover:opacity-100 transition-opacity"
|
|
180
|
+
>
|
|
181
|
+
View Run
|
|
182
|
+
</Button>
|
|
183
|
+
</Link>
|
|
184
|
+
</td>
|
|
185
|
+
</motion.tr>
|
|
186
|
+
))}
|
|
187
|
+
</tbody>
|
|
188
|
+
</table>
|
|
189
|
+
</div>
|
|
190
|
+
</Card>
|
|
191
|
+
) : (
|
|
192
|
+
<Card className="p-12 text-center border-dashed">
|
|
193
|
+
<p className="text-slate-500">No runs recorded for this experiment yet.</p>
|
|
194
|
+
</Card>
|
|
195
|
+
)}
|
|
196
|
+
</motion.div>
|
|
197
|
+
</motion.div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function StatsCard({ icon, label, value, color }) {
|
|
202
|
+
const colorClasses = {
|
|
203
|
+
blue: "bg-blue-50 text-blue-600",
|
|
204
|
+
purple: "bg-purple-50 text-purple-600",
|
|
205
|
+
emerald: "bg-emerald-50 text-emerald-600",
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<Card className="hover:shadow-md transition-shadow duration-200">
|
|
210
|
+
<div className="flex items-center gap-4">
|
|
211
|
+
<div className={`p-3 rounded-xl ${colorClasses[color]}`}>
|
|
212
|
+
{icon}
|
|
213
|
+
</div>
|
|
214
|
+
<div>
|
|
215
|
+
<p className="text-sm text-slate-500 font-medium">{label}</p>
|
|
216
|
+
<p className="text-2xl font-bold text-slate-900">{value}</p>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
</Card>
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function getBestMetric(runs) {
|
|
224
|
+
if (!runs || runs.length === 0) return '-';
|
|
225
|
+
|
|
226
|
+
// Try to find a common metric like accuracy, f1_score, etc.
|
|
227
|
+
const metricKeys = ['accuracy', 'f1_score', 'precision', 'recall'];
|
|
228
|
+
|
|
229
|
+
for (const key of metricKeys) {
|
|
230
|
+
const values = runs
|
|
231
|
+
.map(r => r.metrics?.[key])
|
|
232
|
+
.filter(v => typeof v === 'number');
|
|
233
|
+
|
|
234
|
+
if (values.length > 0) {
|
|
235
|
+
const best = Math.max(...values);
|
|
236
|
+
return `${best.toFixed(4)} (${key})`;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return 'N/A';
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function getLastRunDate(runs) {
|
|
244
|
+
if (!runs || runs.length === 0) return '-';
|
|
245
|
+
|
|
246
|
+
const sorted = [...runs].sort((a, b) => {
|
|
247
|
+
const dateA = a.timestamp ? new Date(a.timestamp) : new Date(0);
|
|
248
|
+
const dateB = b.timestamp ? new Date(b.timestamp) : new Date(0);
|
|
249
|
+
return dateB - dateA;
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return sorted[0]?.timestamp
|
|
253
|
+
? format(new Date(sorted[0].timestamp), 'MMM d, HH:mm')
|
|
254
|
+
: '-';
|
|
255
|
+
}
|