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,360 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Link } from 'react-router-dom';
|
|
3
|
+
import { FlaskConical, ArrowRight, Sparkles, Calendar, Activity, FolderPlus } from 'lucide-react';
|
|
4
|
+
import { Card } from '../../components/ui/Card';
|
|
5
|
+
import { Badge } from '../../components/ui/Badge';
|
|
6
|
+
import { Button } from '../../components/ui/Button';
|
|
7
|
+
import { format } from 'date-fns';
|
|
8
|
+
import { motion } from 'framer-motion';
|
|
9
|
+
import { DataView } from '../../components/ui/DataView';
|
|
10
|
+
import { useProject } from '../../contexts/ProjectContext';
|
|
11
|
+
import { EmptyState } from '../../components/ui/EmptyState';
|
|
12
|
+
|
|
13
|
+
export function Experiments() {
|
|
14
|
+
const [experiments, setExperiments] = useState([]);
|
|
15
|
+
const [loading, setLoading] = useState(true);
|
|
16
|
+
const [selectedExperiments, setSelectedExperiments] = useState([]);
|
|
17
|
+
const { selectedProject } = useProject();
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const fetchExperiments = async () => {
|
|
21
|
+
setLoading(true);
|
|
22
|
+
try {
|
|
23
|
+
const url = selectedProject
|
|
24
|
+
? `/api/experiments?project=${encodeURIComponent(selectedProject)}`
|
|
25
|
+
: '/api/experiments';
|
|
26
|
+
const res = await fetch(url);
|
|
27
|
+
const data = await res.json();
|
|
28
|
+
setExperiments(data.experiments || []);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.error(err);
|
|
31
|
+
} finally {
|
|
32
|
+
setLoading(false);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
fetchExperiments();
|
|
36
|
+
}, [selectedProject]);
|
|
37
|
+
|
|
38
|
+
const columns = [
|
|
39
|
+
{
|
|
40
|
+
header: (
|
|
41
|
+
<input
|
|
42
|
+
type="checkbox"
|
|
43
|
+
checked={selectedExperiments.length === experiments.length && experiments.length > 0}
|
|
44
|
+
onChange={(e) => {
|
|
45
|
+
if (e.target.checked) {
|
|
46
|
+
setSelectedExperiments(experiments.map(e => e.name));
|
|
47
|
+
} else {
|
|
48
|
+
setSelectedExperiments([]);
|
|
49
|
+
}
|
|
50
|
+
}}
|
|
51
|
+
className="w-4 h-4 rounded border-slate-300 text-primary-600 focus:ring-primary-500"
|
|
52
|
+
/>
|
|
53
|
+
),
|
|
54
|
+
key: 'select',
|
|
55
|
+
render: (exp) => (
|
|
56
|
+
<input
|
|
57
|
+
type="checkbox"
|
|
58
|
+
checked={selectedExperiments.includes(exp.name)}
|
|
59
|
+
onChange={(e) => {
|
|
60
|
+
if (e.target.checked) {
|
|
61
|
+
setSelectedExperiments([...selectedExperiments, exp.name]);
|
|
62
|
+
} else {
|
|
63
|
+
setSelectedExperiments(selectedExperiments.filter(n => n !== exp.name));
|
|
64
|
+
}
|
|
65
|
+
}}
|
|
66
|
+
onClick={(e) => e.stopPropagation()}
|
|
67
|
+
className="w-4 h-4 rounded border-slate-300 text-primary-600 focus:ring-primary-500"
|
|
68
|
+
/>
|
|
69
|
+
)
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
header: 'Experiment',
|
|
73
|
+
key: 'name',
|
|
74
|
+
sortable: true,
|
|
75
|
+
render: (exp) => (
|
|
76
|
+
<div className="flex items-center gap-3">
|
|
77
|
+
<div className="p-2 bg-purple-50 dark:bg-purple-900/20 rounded-lg text-purple-600 dark:text-purple-400">
|
|
78
|
+
<FlaskConical size={16} />
|
|
79
|
+
</div>
|
|
80
|
+
<div>
|
|
81
|
+
<div className="font-medium text-slate-900 dark:text-white">{exp.name}</div>
|
|
82
|
+
{exp.description && (
|
|
83
|
+
<div className="text-xs text-slate-500 truncate max-w-[200px]">{exp.description}</div>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
header: 'Project',
|
|
91
|
+
key: 'project',
|
|
92
|
+
sortable: true,
|
|
93
|
+
render: (exp) => (
|
|
94
|
+
<span className="text-sm text-slate-600 dark:text-slate-400">
|
|
95
|
+
{exp.project || '-'}
|
|
96
|
+
</span>
|
|
97
|
+
)
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
header: 'Pipeline',
|
|
101
|
+
key: 'pipeline',
|
|
102
|
+
sortable: true,
|
|
103
|
+
render: (exp) => (
|
|
104
|
+
<span className="text-sm text-slate-600 dark:text-slate-400">
|
|
105
|
+
{exp.pipeline_name || '-'}
|
|
106
|
+
</span>
|
|
107
|
+
)
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
header: 'Runs',
|
|
111
|
+
key: 'run_count',
|
|
112
|
+
sortable: true,
|
|
113
|
+
render: (exp) => (
|
|
114
|
+
<Badge variant="secondary" className="bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300">
|
|
115
|
+
{exp.run_count || 0} runs
|
|
116
|
+
</Badge>
|
|
117
|
+
)
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
header: 'Created',
|
|
121
|
+
key: 'created_at',
|
|
122
|
+
sortable: true,
|
|
123
|
+
render: (exp) => (
|
|
124
|
+
<div className="flex items-center gap-2 text-slate-500">
|
|
125
|
+
<Calendar size={14} />
|
|
126
|
+
{exp.created_at ? format(new Date(exp.created_at), 'MMM d, HH:mm') : '-'}
|
|
127
|
+
</div>
|
|
128
|
+
)
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
header: 'Actions',
|
|
132
|
+
key: 'actions',
|
|
133
|
+
render: (exp) => (
|
|
134
|
+
<Link to={`/experiments/${exp.experiment_id}`}>
|
|
135
|
+
<Button variant="ghost" size="sm" className="text-primary-600 hover:text-primary-700 hover:bg-primary-50 dark:hover:bg-primary-900/20">
|
|
136
|
+
View Details <ArrowRight size={14} className="ml-1" />
|
|
137
|
+
</Button>
|
|
138
|
+
</Link>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
const renderGrid = (exp) => (
|
|
144
|
+
<Link to={`/experiments/${exp.experiment_id}`}>
|
|
145
|
+
<Card className="group cursor-pointer hover:border-primary-300 hover:shadow-lg h-full transition-all duration-200 overflow-hidden relative">
|
|
146
|
+
<div className="absolute inset-0 bg-gradient-to-br from-purple-50/50 to-pink-50/50 dark:from-purple-900/10 dark:to-pink-900/10 opacity-0 group-hover:opacity-100 transition-opacity duration-200" />
|
|
147
|
+
|
|
148
|
+
<div className="relative">
|
|
149
|
+
<div className="flex items-start justify-between mb-4">
|
|
150
|
+
<div className="p-3 bg-purple-50 dark:bg-purple-900/20 rounded-xl text-purple-600 dark:text-purple-400 group-hover:bg-purple-600 group-hover:text-white transition-all duration-200 group-hover:scale-110">
|
|
151
|
+
<FlaskConical size={24} />
|
|
152
|
+
</div>
|
|
153
|
+
<Badge variant="default" className="bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 group-hover:bg-purple-100 dark:group-hover:bg-purple-900/30 group-hover:text-purple-700 dark:group-hover:text-purple-300 transition-colors">
|
|
154
|
+
{exp.run_count || 0} runs
|
|
155
|
+
</Badge>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<h3 className="text-lg font-bold text-slate-900 dark:text-white mb-2 group-hover:text-purple-700 dark:group-hover:text-purple-400 transition-colors">
|
|
159
|
+
{exp.name}
|
|
160
|
+
</h3>
|
|
161
|
+
<p className="text-sm text-slate-500 dark:text-slate-400 mb-4 line-clamp-2 min-h-[2.5rem]">
|
|
162
|
+
{exp.description || "No description provided"}
|
|
163
|
+
</p>
|
|
164
|
+
|
|
165
|
+
<div className="flex items-center justify-between pt-4 border-t border-slate-100 dark:border-slate-700">
|
|
166
|
+
<span className="text-xs text-slate-400 font-medium flex items-center gap-1">
|
|
167
|
+
<Calendar size={12} />
|
|
168
|
+
{exp.created_at ? format(new Date(exp.created_at), 'MMM d, yyyy') : '-'}
|
|
169
|
+
</span>
|
|
170
|
+
<span className="text-sm font-semibold text-primary-600 group-hover:text-primary-700 dark:text-primary-400 dark:group-hover:text-primary-300 flex items-center gap-1 group-hover:gap-2 transition-all">
|
|
171
|
+
View <ArrowRight size={16} className="group-hover:translate-x-1 transition-transform" />
|
|
172
|
+
</span>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</Card>
|
|
176
|
+
</Link>
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
if (loading) {
|
|
180
|
+
return (
|
|
181
|
+
<div className="flex items-center justify-center h-96">
|
|
182
|
+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<div className="p-6 max-w-7xl mx-auto">
|
|
189
|
+
<DataView
|
|
190
|
+
title="Experiments"
|
|
191
|
+
subtitle="Track and compare your ML experiments with detailed metrics and parameters"
|
|
192
|
+
items={experiments}
|
|
193
|
+
loading={loading}
|
|
194
|
+
columns={columns}
|
|
195
|
+
renderGrid={renderGrid}
|
|
196
|
+
actions={
|
|
197
|
+
<ExperimentProjectSelector
|
|
198
|
+
selectedExperiments={selectedExperiments}
|
|
199
|
+
onComplete={() => {
|
|
200
|
+
window.location.reload();
|
|
201
|
+
}}
|
|
202
|
+
/>
|
|
203
|
+
}
|
|
204
|
+
emptyState={
|
|
205
|
+
<EmptyState
|
|
206
|
+
icon={FlaskConical}
|
|
207
|
+
title="No experiments yet"
|
|
208
|
+
description="Start tracking your ML experiments using the Experiment API to compare runs and optimize your models."
|
|
209
|
+
action={
|
|
210
|
+
<div className="inline-block px-4 py-2 bg-slate-100 dark:bg-slate-800 rounded-lg text-sm font-mono text-slate-600 dark:text-slate-300 border border-slate-200 dark:border-slate-700">
|
|
211
|
+
<code>from flowy.tracking import Experiment</code>
|
|
212
|
+
</div>
|
|
213
|
+
}
|
|
214
|
+
/>
|
|
215
|
+
}
|
|
216
|
+
/>
|
|
217
|
+
</div>
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function ExperimentProjectSelector({ selectedExperiments, onComplete }) {
|
|
222
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
223
|
+
const [projects, setProjects] = useState([]);
|
|
224
|
+
const [updating, setUpdating] = useState(false);
|
|
225
|
+
|
|
226
|
+
useEffect(() => {
|
|
227
|
+
if (isOpen) {
|
|
228
|
+
fetch('/api/projects/')
|
|
229
|
+
.then(res => res.json())
|
|
230
|
+
.then(data => setProjects(data))
|
|
231
|
+
.catch(err => console.error('Failed to load projects:', err));
|
|
232
|
+
}
|
|
233
|
+
}, [isOpen]);
|
|
234
|
+
|
|
235
|
+
const handleSelectProject = async (projectName) => {
|
|
236
|
+
setUpdating(true);
|
|
237
|
+
try {
|
|
238
|
+
const updates = selectedExperiments.map(expName =>
|
|
239
|
+
fetch(`/api/experiments/${expName}/project`, {
|
|
240
|
+
method: 'PUT',
|
|
241
|
+
headers: { 'Content-Type': 'application/json' },
|
|
242
|
+
body: JSON.stringify({ project_name: projectName })
|
|
243
|
+
})
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
await Promise.all(updates);
|
|
247
|
+
|
|
248
|
+
const toast = document.createElement('div');
|
|
249
|
+
toast.className = 'fixed top-4 right-4 px-4 py-3 rounded-lg shadow-lg z-50 bg-green-500 text-white';
|
|
250
|
+
toast.textContent = `Added ${selectedExperiments.length} experiment(s) to project ${projectName}`;
|
|
251
|
+
document.body.appendChild(toast);
|
|
252
|
+
setTimeout(() => document.body.removeChild(toast), 3000);
|
|
253
|
+
|
|
254
|
+
setIsOpen(false);
|
|
255
|
+
if (onComplete) onComplete();
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error('Failed to update projects:', error);
|
|
258
|
+
} finally {
|
|
259
|
+
setUpdating(false);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
return (
|
|
264
|
+
<div className="relative">
|
|
265
|
+
<button
|
|
266
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
267
|
+
disabled={updating || selectedExperiments.length === 0}
|
|
268
|
+
className="flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-slate-600 dark:text-slate-300 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
269
|
+
>
|
|
270
|
+
<FolderPlus size={16} />
|
|
271
|
+
{updating ? 'Updating...' : `Add to Project (${selectedExperiments.length})`}
|
|
272
|
+
</button>
|
|
273
|
+
|
|
274
|
+
{isOpen && (
|
|
275
|
+
<>
|
|
276
|
+
<div className="fixed inset-0 z-10" onClick={() => setIsOpen(false)} />
|
|
277
|
+
<div className="absolute right-0 mt-2 w-56 bg-white dark:bg-slate-800 rounded-xl shadow-xl border border-slate-200 dark:border-slate-700 z-20">
|
|
278
|
+
<div className="p-2 border-b border-slate-100 dark:border-slate-700">
|
|
279
|
+
<span className="text-xs font-semibold text-slate-500 px-2">Select Project</span>
|
|
280
|
+
</div>
|
|
281
|
+
<div className="max-h-64 overflow-y-auto p-1">
|
|
282
|
+
{projects.length > 0 ? (
|
|
283
|
+
projects.map(p => (
|
|
284
|
+
<button
|
|
285
|
+
key={p.name}
|
|
286
|
+
onClick={() => handleSelectProject(p.name)}
|
|
287
|
+
disabled={updating}
|
|
288
|
+
className="w-full text-left px-3 py-2 text-sm text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-700 rounded-lg transition-colors disabled:opacity-50"
|
|
289
|
+
>
|
|
290
|
+
{p.name}
|
|
291
|
+
</button>
|
|
292
|
+
))
|
|
293
|
+
) : (
|
|
294
|
+
<div className="px-3 py-2 text-sm text-slate-400 italic">No projects found</div>
|
|
295
|
+
)}
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
</>
|
|
299
|
+
)}
|
|
300
|
+
</div>
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function ProjectSelector({ onSelect }) {
|
|
305
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
306
|
+
const [projects, setProjects] = useState([]);
|
|
307
|
+
|
|
308
|
+
useEffect(() => {
|
|
309
|
+
if (isOpen) {
|
|
310
|
+
fetch('/api/projects/')
|
|
311
|
+
.then(res => res.json())
|
|
312
|
+
.then(data => setProjects(data))
|
|
313
|
+
.catch(err => console.error('Failed to load projects:', err));
|
|
314
|
+
}
|
|
315
|
+
}, [isOpen]);
|
|
316
|
+
|
|
317
|
+
return (
|
|
318
|
+
<div className="relative">
|
|
319
|
+
<button
|
|
320
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
321
|
+
className="flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-slate-600 dark:text-slate-300 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
|
|
322
|
+
>
|
|
323
|
+
<FolderPlus size={16} />
|
|
324
|
+
Add to Project
|
|
325
|
+
</button>
|
|
326
|
+
|
|
327
|
+
{isOpen && (
|
|
328
|
+
<>
|
|
329
|
+
<div
|
|
330
|
+
className="fixed inset-0 z-10"
|
|
331
|
+
onClick={() => setIsOpen(false)}
|
|
332
|
+
/>
|
|
333
|
+
<div className="absolute right-0 mt-2 w-56 bg-white dark:bg-slate-800 rounded-xl shadow-xl border border-slate-200 dark:border-slate-700 z-20 overflow-hidden animate-in fade-in zoom-in-95 duration-100">
|
|
334
|
+
<div className="p-2 border-b border-slate-100 dark:border-slate-700">
|
|
335
|
+
<span className="text-xs font-semibold text-slate-500 px-2">Select Project</span>
|
|
336
|
+
</div>
|
|
337
|
+
<div className="max-h-64 overflow-y-auto p-1">
|
|
338
|
+
{projects.length > 0 ? (
|
|
339
|
+
projects.map(p => (
|
|
340
|
+
<button
|
|
341
|
+
key={p.name}
|
|
342
|
+
onClick={() => {
|
|
343
|
+
onSelect(p.name);
|
|
344
|
+
setIsOpen(false);
|
|
345
|
+
}}
|
|
346
|
+
className="w-full text-left px-3 py-2 text-sm text-slate-700 dark:text-slate-200 hover:bg-slate-50 dark:hover:bg-slate-700 rounded-lg transition-colors"
|
|
347
|
+
>
|
|
348
|
+
{p.name}
|
|
349
|
+
</button>
|
|
350
|
+
))
|
|
351
|
+
) : (
|
|
352
|
+
<div className="px-3 py-2 text-sm text-slate-400 italic">No projects found</div>
|
|
353
|
+
)}
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
</>
|
|
357
|
+
)}
|
|
358
|
+
</div>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { fetchApi } from '../../utils/api';
|
|
3
|
+
import { Trophy, TrendingUp, TrendingDown, Filter, RefreshCw } from 'lucide-react';
|
|
4
|
+
import { format } from 'date-fns';
|
|
5
|
+
import { EmptyState } from '../../components/ui/EmptyState';
|
|
6
|
+
import { Button } from '../../components/ui/Button';
|
|
7
|
+
|
|
8
|
+
export function Leaderboard() {
|
|
9
|
+
const [metric, setMetric] = useState('accuracy');
|
|
10
|
+
const [data, setData] = useState(null);
|
|
11
|
+
const [loading, setLoading] = useState(true);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
fetchLeaderboard();
|
|
15
|
+
}, [metric]);
|
|
16
|
+
|
|
17
|
+
const fetchLeaderboard = async () => {
|
|
18
|
+
setLoading(true);
|
|
19
|
+
try {
|
|
20
|
+
const response = await fetchApi(`/api/leaderboard/${metric}`);
|
|
21
|
+
const result = await response.json();
|
|
22
|
+
setData(result);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error('Failed to fetch leaderboard:', error);
|
|
25
|
+
} finally {
|
|
26
|
+
setLoading(false);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="p-6">
|
|
32
|
+
<div className="flex justify-between items-center mb-6">
|
|
33
|
+
<h1 className="text-2xl font-bold flex items-center gap-2 text-slate-900 dark:text-white">
|
|
34
|
+
<Trophy className="text-yellow-500" />
|
|
35
|
+
Model Leaderboard
|
|
36
|
+
</h1>
|
|
37
|
+
|
|
38
|
+
<div className="flex items-center gap-2 bg-white dark:bg-slate-800 rounded-lg p-1 border border-slate-200 dark:border-slate-700">
|
|
39
|
+
<Filter className="w-4 h-4 ml-2 text-slate-400 dark:text-slate-500" />
|
|
40
|
+
<select
|
|
41
|
+
value={metric}
|
|
42
|
+
onChange={(e) => setMetric(e.target.value)}
|
|
43
|
+
className="bg-transparent border-none outline-none text-sm px-2 py-1 text-slate-700 dark:text-slate-200"
|
|
44
|
+
>
|
|
45
|
+
<option value="accuracy">Accuracy</option>
|
|
46
|
+
<option value="loss">Loss</option>
|
|
47
|
+
<option value="f1_score">F1 Score</option>
|
|
48
|
+
<option value="latency">Latency</option>
|
|
49
|
+
</select>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
{loading ? (
|
|
54
|
+
<div className="text-center py-12">
|
|
55
|
+
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-yellow-500"></div>
|
|
56
|
+
</div>
|
|
57
|
+
) : !data || data.models.length === 0 ? (
|
|
58
|
+
<EmptyState
|
|
59
|
+
icon={Trophy}
|
|
60
|
+
title={`No models found for metric: ${metric}`}
|
|
61
|
+
description="Generate sample data to populate the leaderboard."
|
|
62
|
+
action={
|
|
63
|
+
<Button
|
|
64
|
+
onClick={async () => {
|
|
65
|
+
setLoading(true);
|
|
66
|
+
try {
|
|
67
|
+
await fetchApi('/api/leaderboard/generate_sample_data', { method: 'POST' });
|
|
68
|
+
fetchLeaderboard();
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error('Failed to generate sample data:', error);
|
|
71
|
+
setLoading(false);
|
|
72
|
+
}
|
|
73
|
+
}}
|
|
74
|
+
className="flex items-center gap-2"
|
|
75
|
+
>
|
|
76
|
+
<RefreshCw size={16} />
|
|
77
|
+
Generate Sample Data
|
|
78
|
+
</Button>
|
|
79
|
+
}
|
|
80
|
+
/>
|
|
81
|
+
) : (
|
|
82
|
+
<div className="bg-white dark:bg-slate-800/50 rounded-lg border border-slate-200 dark:border-slate-700 overflow-hidden">
|
|
83
|
+
<table className="w-full">
|
|
84
|
+
<thead>
|
|
85
|
+
<tr className="bg-slate-50 dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700">
|
|
86
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">Rank</th>
|
|
87
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">Model</th>
|
|
88
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">Score</th>
|
|
89
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">Run ID</th>
|
|
90
|
+
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase tracking-wider">Date</th>
|
|
91
|
+
</tr>
|
|
92
|
+
</thead>
|
|
93
|
+
<tbody className="divide-y divide-slate-100 dark:divide-slate-700">
|
|
94
|
+
{data.models.map((model, idx) => (
|
|
95
|
+
<tr key={model.run_id} className="hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
|
|
96
|
+
<td className="px-6 py-4 whitespace-nowrap">
|
|
97
|
+
<div className="flex items-center gap-2">
|
|
98
|
+
{idx === 0 && <Trophy className="w-4 h-4 text-yellow-500" />}
|
|
99
|
+
{idx === 1 && <Trophy className="w-4 h-4 text-slate-400" />}
|
|
100
|
+
{idx === 2 && <Trophy className="w-4 h-4 text-amber-600" />}
|
|
101
|
+
<span className={`font-bold ${idx < 3 ? 'text-slate-900 dark:text-white' : 'text-slate-500 dark:text-slate-400'}`}>
|
|
102
|
+
#{model.rank}
|
|
103
|
+
</span>
|
|
104
|
+
</div>
|
|
105
|
+
</td>
|
|
106
|
+
<td className="px-6 py-4 whitespace-nowrap font-medium text-slate-900 dark:text-white">{model.model_name}</td>
|
|
107
|
+
<td className="px-6 py-4 whitespace-nowrap">
|
|
108
|
+
<div className="flex items-center gap-2">
|
|
109
|
+
<span className="text-lg font-bold text-blue-600 dark:text-blue-400">
|
|
110
|
+
{model.score.toFixed(4)}
|
|
111
|
+
</span>
|
|
112
|
+
{data.higher_is_better ? (
|
|
113
|
+
<TrendingUp className="w-4 h-4 text-green-500" />
|
|
114
|
+
) : (
|
|
115
|
+
<TrendingDown className="w-4 h-4 text-green-500" />
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
</td>
|
|
119
|
+
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-slate-400 font-mono">
|
|
120
|
+
{model.run_id.substring(0, 8)}
|
|
121
|
+
</td>
|
|
122
|
+
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-slate-400">
|
|
123
|
+
{model.timestamp ? format(new Date(model.timestamp), 'MMM d, HH:mm') : '-'}
|
|
124
|
+
</td>
|
|
125
|
+
</tr>
|
|
126
|
+
))}
|
|
127
|
+
</tbody>
|
|
128
|
+
</table>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|