flowyml 1.2.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.
Files changed (91) 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 +22 -5
  14. flowyml/core/retry_policy.py +80 -0
  15. flowyml/core/step.py +18 -1
  16. flowyml/core/submission_result.py +53 -0
  17. flowyml/integrations/keras.py +95 -22
  18. flowyml/monitoring/alerts.py +2 -2
  19. flowyml/stacks/__init__.py +15 -0
  20. flowyml/stacks/aws.py +599 -0
  21. flowyml/stacks/azure.py +295 -0
  22. flowyml/stacks/components.py +24 -2
  23. flowyml/stacks/gcp.py +158 -11
  24. flowyml/stacks/local.py +5 -0
  25. flowyml/storage/artifacts.py +15 -5
  26. flowyml/storage/materializers/__init__.py +2 -0
  27. flowyml/storage/materializers/cloudpickle.py +74 -0
  28. flowyml/storage/metadata.py +166 -5
  29. flowyml/ui/backend/main.py +41 -1
  30. flowyml/ui/backend/routers/assets.py +356 -15
  31. flowyml/ui/backend/routers/client.py +46 -0
  32. flowyml/ui/backend/routers/execution.py +13 -2
  33. flowyml/ui/backend/routers/experiments.py +48 -12
  34. flowyml/ui/backend/routers/metrics.py +213 -0
  35. flowyml/ui/backend/routers/pipelines.py +63 -7
  36. flowyml/ui/backend/routers/projects.py +33 -7
  37. flowyml/ui/backend/routers/runs.py +150 -8
  38. flowyml/ui/frontend/dist/assets/index-DcYwrn2j.css +1 -0
  39. flowyml/ui/frontend/dist/assets/index-Dlz_ygOL.js +592 -0
  40. flowyml/ui/frontend/dist/index.html +2 -2
  41. flowyml/ui/frontend/src/App.jsx +4 -1
  42. flowyml/ui/frontend/src/app/assets/page.jsx +260 -230
  43. flowyml/ui/frontend/src/app/dashboard/page.jsx +38 -7
  44. flowyml/ui/frontend/src/app/experiments/page.jsx +61 -314
  45. flowyml/ui/frontend/src/app/observability/page.jsx +277 -0
  46. flowyml/ui/frontend/src/app/pipelines/page.jsx +79 -402
  47. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectArtifactsList.jsx +151 -0
  48. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +145 -0
  49. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHeader.jsx +45 -0
  50. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectHierarchy.jsx +467 -0
  51. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +253 -0
  52. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectPipelinesList.jsx +105 -0
  53. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRelations.jsx +189 -0
  54. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectRunsList.jsx +136 -0
  55. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectTabs.jsx +95 -0
  56. flowyml/ui/frontend/src/app/projects/[projectId]/page.jsx +326 -0
  57. flowyml/ui/frontend/src/app/projects/page.jsx +13 -3
  58. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +79 -10
  59. flowyml/ui/frontend/src/app/runs/page.jsx +82 -424
  60. flowyml/ui/frontend/src/app/settings/page.jsx +1 -0
  61. flowyml/ui/frontend/src/app/tokens/page.jsx +62 -16
  62. flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +373 -0
  63. flowyml/ui/frontend/src/components/AssetLineageGraph.jsx +291 -0
  64. flowyml/ui/frontend/src/components/AssetStatsDashboard.jsx +302 -0
  65. flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +477 -0
  66. flowyml/ui/frontend/src/components/ExperimentDetailsPanel.jsx +227 -0
  67. flowyml/ui/frontend/src/components/NavigationTree.jsx +401 -0
  68. flowyml/ui/frontend/src/components/PipelineDetailsPanel.jsx +239 -0
  69. flowyml/ui/frontend/src/components/PipelineGraph.jsx +67 -3
  70. flowyml/ui/frontend/src/components/ProjectSelector.jsx +115 -0
  71. flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +298 -0
  72. flowyml/ui/frontend/src/components/header/Header.jsx +48 -1
  73. flowyml/ui/frontend/src/components/plugins/ZenMLIntegration.jsx +106 -0
  74. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +52 -26
  75. flowyml/ui/frontend/src/components/ui/DataView.jsx +35 -17
  76. flowyml/ui/frontend/src/components/ui/ErrorBoundary.jsx +118 -0
  77. flowyml/ui/frontend/src/contexts/ProjectContext.jsx +2 -2
  78. flowyml/ui/frontend/src/contexts/ToastContext.jsx +116 -0
  79. flowyml/ui/frontend/src/layouts/MainLayout.jsx +5 -1
  80. flowyml/ui/frontend/src/router/index.jsx +4 -0
  81. flowyml/ui/frontend/src/utils/date.js +10 -0
  82. flowyml/ui/frontend/src/utils/downloads.js +11 -0
  83. flowyml/utils/config.py +6 -0
  84. flowyml/utils/stack_config.py +45 -3
  85. {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/METADATA +42 -4
  86. {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/RECORD +89 -52
  87. {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/licenses/LICENSE +1 -1
  88. flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +0 -448
  89. flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +0 -1
  90. {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/WHEEL +0 -0
  91. {flowyml-1.2.0.dist-info → flowyml-1.3.0.dist-info}/entry_points.txt +0 -0
@@ -1,39 +1,52 @@
1
- import React, { useState, useEffect } from 'react';
1
+ import React, { useEffect, useState } from 'react';
2
2
  import { fetchApi } from '../../utils/api';
3
3
  import { Link } from 'react-router-dom';
4
- import { Activity, Layers, Database, TrendingUp, Clock, CheckCircle, XCircle, Zap, ArrowRight } from 'lucide-react';
4
+ import { PlayCircle, Package, GitBranch, TrendingUp, Activity, Clock, CheckCircle, CheckCircle2, XCircle, Loader2, Zap, ArrowRight, Database, Layers } from 'lucide-react';
5
5
  import { Card } from '../../components/ui/Card';
6
6
  import { Badge } from '../../components/ui/Badge';
7
+ import { StatusBadge } from '../../components/ui/ExecutionStatus';
7
8
  import { format } from 'date-fns';
8
9
  import { motion } from 'framer-motion';
9
10
  import { useProject } from '../../contexts/ProjectContext';
11
+ import { useToast } from '../../contexts/ToastContext';
10
12
 
11
13
  export function Dashboard() {
12
14
  const [stats, setStats] = useState(null);
13
15
  const [recentRuns, setRecentRuns] = useState([]);
14
16
  const [loading, setLoading] = useState(true);
17
+ const [error, setError] = useState(null);
15
18
  const { selectedProject } = useProject();
19
+ const toast = useToast();
16
20
 
17
21
  useEffect(() => {
18
22
  const fetchData = async () => {
19
23
  setLoading(true);
24
+ setError(null);
20
25
  try {
21
26
  const statsUrl = selectedProject
22
27
  ? `/api/stats?project=${encodeURIComponent(selectedProject)}`
23
28
  : '/api/stats';
24
29
  const runsUrl = selectedProject
25
- ? `/api/runs?limit=5&project=${encodeURIComponent(selectedProject)}`
26
- : '/api/runs?limit=5';
30
+ ? `/api/runs/?limit=5&project=${encodeURIComponent(selectedProject)}`
31
+ : '/api/runs/?limit=5';
27
32
 
28
- const [statsData, runsData] = await Promise.all([
29
- fetchApi(statsUrl).then(res => res.json()),
30
- fetchApi(runsUrl).then(res => res.json())
33
+ const [statsRes, runsRes] = await Promise.all([
34
+ fetchApi(statsUrl),
35
+ fetchApi(runsUrl)
31
36
  ]);
32
37
 
38
+ if (!statsRes.ok) throw new Error(`Failed to fetch stats: ${statsRes.statusText}`);
39
+ if (!runsRes.ok) throw new Error(`Failed to fetch runs: ${runsRes.statusText}`);
40
+
41
+ const statsData = await statsRes.json();
42
+ const runsData = await runsRes.json();
43
+
33
44
  setStats(statsData);
34
45
  setRecentRuns(runsData.runs || []);
35
46
  } catch (err) {
36
47
  console.error(err);
48
+ setError(err.message);
49
+ toast.error(`Failed to load dashboard: ${err.message}`);
37
50
  } finally {
38
51
  setLoading(false);
39
52
  }
@@ -49,6 +62,24 @@ export function Dashboard() {
49
62
  );
50
63
  }
51
64
 
65
+ if (error) {
66
+ return (
67
+ <div className="flex items-center justify-center h-96">
68
+ <div className="text-center p-8 bg-red-50 dark:bg-red-900/20 rounded-2xl border border-red-100 dark:border-red-800 max-w-md">
69
+ <XCircle className="w-12 h-12 text-red-500 mx-auto mb-4" />
70
+ <h3 className="text-lg font-bold text-red-700 dark:text-red-300 mb-2">Failed to load dashboard</h3>
71
+ <p className="text-red-600 dark:text-red-400 mb-6">{error}</p>
72
+ <button
73
+ onClick={() => window.location.reload()}
74
+ className="px-4 py-2 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg shadow-sm hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors text-slate-700 dark:text-slate-300 font-medium"
75
+ >
76
+ Retry Connection
77
+ </button>
78
+ </div>
79
+ </div>
80
+ );
81
+ }
82
+
52
83
  const container = {
53
84
  hidden: { opacity: 0 },
54
85
  show: {
@@ -1,28 +1,31 @@
1
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';
2
+ import { Link, useParams, useNavigate } from 'react-router-dom';
3
+ import { FlaskConical, ArrowRight, Sparkles, Calendar, Activity, FolderPlus, Layout } from 'lucide-react';
4
4
  import { Card } from '../../components/ui/Card';
5
5
  import { Badge } from '../../components/ui/Badge';
6
6
  import { Button } from '../../components/ui/Button';
7
7
  import { format } from 'date-fns';
8
- import { motion } from 'framer-motion';
8
+ import { motion, AnimatePresence } from 'framer-motion';
9
9
  import { DataView } from '../../components/ui/DataView';
10
10
  import { useProject } from '../../contexts/ProjectContext';
11
11
  import { EmptyState } from '../../components/ui/EmptyState';
12
+ import { NavigationTree } from '../../components/NavigationTree';
13
+ import { ExperimentDetailsPanel } from '../../components/ExperimentDetailsPanel';
12
14
 
13
15
  export function Experiments() {
14
16
  const [experiments, setExperiments] = useState([]);
15
17
  const [loading, setLoading] = useState(true);
16
- const [selectedExperiments, setSelectedExperiments] = useState([]);
18
+ const [selectedExperiment, setSelectedExperiment] = useState(null);
17
19
  const { selectedProject } = useProject();
20
+ const navigate = useNavigate();
18
21
 
19
22
  useEffect(() => {
20
23
  const fetchExperiments = async () => {
21
24
  setLoading(true);
22
25
  try {
23
26
  const url = selectedProject
24
- ? `/api/experiments?project=${encodeURIComponent(selectedProject)}`
25
- : '/api/experiments';
27
+ ? `/api/experiments/?project=${encodeURIComponent(selectedProject)}`
28
+ : '/api/experiments/';
26
29
  const res = await fetch(url);
27
30
  const data = await res.json();
28
31
  setExperiments(data.experiments || []);
@@ -35,326 +38,70 @@ export function Experiments() {
35
38
  fetchExperiments();
36
39
  }, [selectedProject]);
37
40
 
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>
41
+ const handleExperimentSelect = (experiment) => {
42
+ setSelectedExperiment(experiment);
43
+ };
164
44
 
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>
45
+ return (
46
+ <div className="h-screen flex flex-col overflow-hidden bg-slate-50 dark:bg-slate-900">
47
+ {/* Header */}
48
+ <div className="bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 px-6 py-4 shrink-0">
49
+ <div className="flex items-center justify-between max-w-[1800px] mx-auto">
50
+ <div>
51
+ <h1 className="text-xl font-bold text-slate-900 dark:text-white flex items-center gap-2">
52
+ <FlaskConical className="text-purple-500" />
53
+ Experiments
54
+ </h1>
55
+ <p className="text-sm text-slate-600 dark:text-slate-400">
56
+ Manage and track your ML experiments
57
+ </p>
173
58
  </div>
174
59
  </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
60
  </div>
184
- );
185
- }
186
61
 
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>
62
+ {/* Main Content */}
63
+ <div className="flex-1 overflow-hidden">
64
+ <div className="h-full max-w-[1800px] mx-auto px-6 py-6">
65
+ <div className="h-full flex gap-6">
66
+ {/* Left Sidebar - Navigation */}
67
+ <div className="w-[320px] shrink-0 flex flex-col bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden shadow-sm">
68
+ <div className="p-3 border-b border-slate-200 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-800/50">
69
+ <h3 className="text-xs font-semibold text-slate-500 uppercase tracking-wider">Explorer</h3>
70
+ </div>
71
+ <div className="flex-1 min-h-0">
72
+ <NavigationTree
73
+ mode="experiments"
74
+ projectId={selectedProject}
75
+ onSelect={handleExperimentSelect}
76
+ selectedId={selectedExperiment?.experiment_id}
77
+ />
212
78
  </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
79
  </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
80
 
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
- ))
81
+ {/* Right Content - Details Panel or Empty State */}
82
+ <div className="flex-1 min-w-0 bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden shadow-sm">
83
+ {selectedExperiment ? (
84
+ <ExperimentDetailsPanel
85
+ experiment={selectedExperiment}
86
+ onClose={() => setSelectedExperiment(null)}
87
+ />
351
88
  ) : (
352
- <div className="px-3 py-2 text-sm text-slate-400 italic">No projects found</div>
89
+ <div className="h-full flex flex-col items-center justify-center text-center p-8 bg-slate-50/50 dark:bg-slate-900/50">
90
+ <div className="w-20 h-20 bg-purple-100 dark:bg-purple-900/30 rounded-full flex items-center justify-center mb-6 animate-pulse">
91
+ <FlaskConical size={40} className="text-purple-500" />
92
+ </div>
93
+ <h2 className="text-2xl font-bold text-slate-900 dark:text-white mb-2">
94
+ Select an Experiment
95
+ </h2>
96
+ <p className="text-slate-500 max-w-md">
97
+ Choose an experiment from the sidebar to view detailed metrics, runs, and analysis.
98
+ </p>
99
+ </div>
353
100
  )}
354
101
  </div>
355
102
  </div>
356
- </>
357
- )}
103
+ </div>
104
+ </div>
358
105
  </div>
359
106
  );
360
107
  }