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.
Files changed (159) hide show
  1. flowyml/__init__.py +207 -0
  2. flowyml/assets/__init__.py +22 -0
  3. flowyml/assets/artifact.py +40 -0
  4. flowyml/assets/base.py +209 -0
  5. flowyml/assets/dataset.py +100 -0
  6. flowyml/assets/featureset.py +301 -0
  7. flowyml/assets/metrics.py +104 -0
  8. flowyml/assets/model.py +82 -0
  9. flowyml/assets/registry.py +157 -0
  10. flowyml/assets/report.py +315 -0
  11. flowyml/cli/__init__.py +5 -0
  12. flowyml/cli/experiment.py +232 -0
  13. flowyml/cli/init.py +256 -0
  14. flowyml/cli/main.py +327 -0
  15. flowyml/cli/run.py +75 -0
  16. flowyml/cli/stack_cli.py +532 -0
  17. flowyml/cli/ui.py +33 -0
  18. flowyml/core/__init__.py +68 -0
  19. flowyml/core/advanced_cache.py +274 -0
  20. flowyml/core/approval.py +64 -0
  21. flowyml/core/cache.py +203 -0
  22. flowyml/core/checkpoint.py +148 -0
  23. flowyml/core/conditional.py +373 -0
  24. flowyml/core/context.py +155 -0
  25. flowyml/core/error_handling.py +419 -0
  26. flowyml/core/executor.py +354 -0
  27. flowyml/core/graph.py +185 -0
  28. flowyml/core/parallel.py +452 -0
  29. flowyml/core/pipeline.py +764 -0
  30. flowyml/core/project.py +253 -0
  31. flowyml/core/resources.py +424 -0
  32. flowyml/core/scheduler.py +630 -0
  33. flowyml/core/scheduler_config.py +32 -0
  34. flowyml/core/step.py +201 -0
  35. flowyml/core/step_grouping.py +292 -0
  36. flowyml/core/templates.py +226 -0
  37. flowyml/core/versioning.py +217 -0
  38. flowyml/integrations/__init__.py +1 -0
  39. flowyml/integrations/keras.py +134 -0
  40. flowyml/monitoring/__init__.py +1 -0
  41. flowyml/monitoring/alerts.py +57 -0
  42. flowyml/monitoring/data.py +102 -0
  43. flowyml/monitoring/llm.py +160 -0
  44. flowyml/monitoring/monitor.py +57 -0
  45. flowyml/monitoring/notifications.py +246 -0
  46. flowyml/registry/__init__.py +5 -0
  47. flowyml/registry/model_registry.py +491 -0
  48. flowyml/registry/pipeline_registry.py +55 -0
  49. flowyml/stacks/__init__.py +27 -0
  50. flowyml/stacks/base.py +77 -0
  51. flowyml/stacks/bridge.py +288 -0
  52. flowyml/stacks/components.py +155 -0
  53. flowyml/stacks/gcp.py +499 -0
  54. flowyml/stacks/local.py +112 -0
  55. flowyml/stacks/migration.py +97 -0
  56. flowyml/stacks/plugin_config.py +78 -0
  57. flowyml/stacks/plugins.py +401 -0
  58. flowyml/stacks/registry.py +226 -0
  59. flowyml/storage/__init__.py +26 -0
  60. flowyml/storage/artifacts.py +246 -0
  61. flowyml/storage/materializers/__init__.py +20 -0
  62. flowyml/storage/materializers/base.py +133 -0
  63. flowyml/storage/materializers/keras.py +185 -0
  64. flowyml/storage/materializers/numpy.py +94 -0
  65. flowyml/storage/materializers/pandas.py +142 -0
  66. flowyml/storage/materializers/pytorch.py +135 -0
  67. flowyml/storage/materializers/sklearn.py +110 -0
  68. flowyml/storage/materializers/tensorflow.py +152 -0
  69. flowyml/storage/metadata.py +931 -0
  70. flowyml/tracking/__init__.py +1 -0
  71. flowyml/tracking/experiment.py +211 -0
  72. flowyml/tracking/leaderboard.py +191 -0
  73. flowyml/tracking/runs.py +145 -0
  74. flowyml/ui/__init__.py +15 -0
  75. flowyml/ui/backend/Dockerfile +31 -0
  76. flowyml/ui/backend/__init__.py +0 -0
  77. flowyml/ui/backend/auth.py +163 -0
  78. flowyml/ui/backend/main.py +187 -0
  79. flowyml/ui/backend/routers/__init__.py +0 -0
  80. flowyml/ui/backend/routers/assets.py +45 -0
  81. flowyml/ui/backend/routers/execution.py +179 -0
  82. flowyml/ui/backend/routers/experiments.py +49 -0
  83. flowyml/ui/backend/routers/leaderboard.py +118 -0
  84. flowyml/ui/backend/routers/notifications.py +72 -0
  85. flowyml/ui/backend/routers/pipelines.py +110 -0
  86. flowyml/ui/backend/routers/plugins.py +192 -0
  87. flowyml/ui/backend/routers/projects.py +85 -0
  88. flowyml/ui/backend/routers/runs.py +66 -0
  89. flowyml/ui/backend/routers/schedules.py +222 -0
  90. flowyml/ui/backend/routers/traces.py +84 -0
  91. flowyml/ui/frontend/Dockerfile +20 -0
  92. flowyml/ui/frontend/README.md +315 -0
  93. flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +448 -0
  94. flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +1 -0
  95. flowyml/ui/frontend/dist/index.html +16 -0
  96. flowyml/ui/frontend/index.html +15 -0
  97. flowyml/ui/frontend/nginx.conf +26 -0
  98. flowyml/ui/frontend/package-lock.json +3545 -0
  99. flowyml/ui/frontend/package.json +33 -0
  100. flowyml/ui/frontend/postcss.config.js +6 -0
  101. flowyml/ui/frontend/src/App.jsx +21 -0
  102. flowyml/ui/frontend/src/app/assets/page.jsx +397 -0
  103. flowyml/ui/frontend/src/app/dashboard/page.jsx +295 -0
  104. flowyml/ui/frontend/src/app/experiments/[experimentId]/page.jsx +255 -0
  105. flowyml/ui/frontend/src/app/experiments/page.jsx +360 -0
  106. flowyml/ui/frontend/src/app/leaderboard/page.jsx +133 -0
  107. flowyml/ui/frontend/src/app/pipelines/page.jsx +454 -0
  108. flowyml/ui/frontend/src/app/plugins/page.jsx +48 -0
  109. flowyml/ui/frontend/src/app/projects/page.jsx +292 -0
  110. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +682 -0
  111. flowyml/ui/frontend/src/app/runs/page.jsx +470 -0
  112. flowyml/ui/frontend/src/app/schedules/page.jsx +585 -0
  113. flowyml/ui/frontend/src/app/settings/page.jsx +314 -0
  114. flowyml/ui/frontend/src/app/tokens/page.jsx +456 -0
  115. flowyml/ui/frontend/src/app/traces/page.jsx +246 -0
  116. flowyml/ui/frontend/src/components/Layout.jsx +108 -0
  117. flowyml/ui/frontend/src/components/PipelineGraph.jsx +295 -0
  118. flowyml/ui/frontend/src/components/header/Header.jsx +72 -0
  119. flowyml/ui/frontend/src/components/plugins/AddPluginDialog.jsx +121 -0
  120. flowyml/ui/frontend/src/components/plugins/InstalledPlugins.jsx +124 -0
  121. flowyml/ui/frontend/src/components/plugins/PluginBrowser.jsx +167 -0
  122. flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +60 -0
  123. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +145 -0
  124. flowyml/ui/frontend/src/components/ui/Badge.jsx +26 -0
  125. flowyml/ui/frontend/src/components/ui/Button.jsx +34 -0
  126. flowyml/ui/frontend/src/components/ui/Card.jsx +44 -0
  127. flowyml/ui/frontend/src/components/ui/CodeSnippet.jsx +38 -0
  128. flowyml/ui/frontend/src/components/ui/CollapsibleCard.jsx +53 -0
  129. flowyml/ui/frontend/src/components/ui/DataView.jsx +175 -0
  130. flowyml/ui/frontend/src/components/ui/EmptyState.jsx +49 -0
  131. flowyml/ui/frontend/src/components/ui/ExecutionStatus.jsx +122 -0
  132. flowyml/ui/frontend/src/components/ui/KeyValue.jsx +25 -0
  133. flowyml/ui/frontend/src/components/ui/ProjectSelector.jsx +134 -0
  134. flowyml/ui/frontend/src/contexts/ProjectContext.jsx +79 -0
  135. flowyml/ui/frontend/src/contexts/ThemeContext.jsx +54 -0
  136. flowyml/ui/frontend/src/index.css +11 -0
  137. flowyml/ui/frontend/src/layouts/MainLayout.jsx +23 -0
  138. flowyml/ui/frontend/src/main.jsx +10 -0
  139. flowyml/ui/frontend/src/router/index.jsx +39 -0
  140. flowyml/ui/frontend/src/services/pluginService.js +90 -0
  141. flowyml/ui/frontend/src/utils/api.js +47 -0
  142. flowyml/ui/frontend/src/utils/cn.js +6 -0
  143. flowyml/ui/frontend/tailwind.config.js +31 -0
  144. flowyml/ui/frontend/vite.config.js +21 -0
  145. flowyml/ui/utils.py +77 -0
  146. flowyml/utils/__init__.py +67 -0
  147. flowyml/utils/config.py +308 -0
  148. flowyml/utils/debug.py +240 -0
  149. flowyml/utils/environment.py +346 -0
  150. flowyml/utils/git.py +319 -0
  151. flowyml/utils/logging.py +61 -0
  152. flowyml/utils/performance.py +314 -0
  153. flowyml/utils/stack_config.py +296 -0
  154. flowyml/utils/validation.py +270 -0
  155. flowyml-1.1.0.dist-info/METADATA +372 -0
  156. flowyml-1.1.0.dist-info/RECORD +159 -0
  157. flowyml-1.1.0.dist-info/WHEEL +4 -0
  158. flowyml-1.1.0.dist-info/entry_points.txt +3 -0
  159. flowyml-1.1.0.dist-info/licenses/LICENSE +17 -0
@@ -0,0 +1,122 @@
1
+ import React from 'react';
2
+ import { CheckCircle, XCircle, Clock, Loader, AlertCircle, PlayCircle } from 'lucide-react';
3
+
4
+ const statusConfig = {
5
+ completed: {
6
+ icon: CheckCircle,
7
+ label: 'Completed',
8
+ color: 'text-emerald-600 dark:text-emerald-400',
9
+ bg: 'bg-emerald-50 dark:bg-emerald-900/20',
10
+ border: 'border-emerald-200 dark:border-emerald-800'
11
+ },
12
+ success: {
13
+ icon: CheckCircle,
14
+ label: 'Success',
15
+ color: 'text-emerald-600 dark:text-emerald-400',
16
+ bg: 'bg-emerald-50 dark:bg-emerald-900/20',
17
+ border: 'border-emerald-200 dark:border-emerald-800'
18
+ },
19
+ failed: {
20
+ icon: XCircle,
21
+ label: 'Failed',
22
+ color: 'text-rose-600 dark:text-rose-400',
23
+ bg: 'bg-rose-50 dark:bg-rose-900/20',
24
+ border: 'border-rose-200 dark:border-rose-800'
25
+ },
26
+ running: {
27
+ icon: Loader,
28
+ label: 'Running',
29
+ color: 'text-blue-600 dark:text-blue-400',
30
+ bg: 'bg-blue-50 dark:bg-blue-900/20',
31
+ border: 'border-blue-200 dark:border-blue-800',
32
+ animate: true
33
+ },
34
+ pending: {
35
+ icon: Clock,
36
+ label: 'Pending',
37
+ color: 'text-amber-600 dark:text-amber-400',
38
+ bg: 'bg-amber-50 dark:bg-amber-900/20',
39
+ border: 'border-amber-200 dark:border-amber-800'
40
+ },
41
+ queued: {
42
+ icon: Clock,
43
+ label: 'Queued',
44
+ color: 'text-slate-600 dark:text-slate-400',
45
+ bg: 'bg-slate-50 dark:bg-slate-900/20',
46
+ border: 'border-slate-200 dark:border-slate-700'
47
+ },
48
+ initializing: {
49
+ icon: PlayCircle,
50
+ label: 'Initializing',
51
+ color: 'text-blue-600 dark:text-blue-400',
52
+ bg: 'bg-blue-50 dark:bg-blue-900/20',
53
+ border: 'border-blue-200 dark:border-blue-800'
54
+ },
55
+ cached: {
56
+ icon: CheckCircle,
57
+ label: 'Cached',
58
+ color: 'text-cyan-600 dark:text-cyan-400',
59
+ bg: 'bg-cyan-50 dark:bg-cyan-900/20',
60
+ border: 'border-cyan-200 dark:border-cyan-800'
61
+ }
62
+ };
63
+
64
+ export function ExecutionStatus({
65
+ status,
66
+ size = 'md',
67
+ showLabel = true,
68
+ className = '',
69
+ iconOnly = false
70
+ }) {
71
+ const config = statusConfig[status?.toLowerCase()] || statusConfig.pending;
72
+ const Icon = config.icon;
73
+
74
+ const sizeClasses = {
75
+ sm: { icon: 14, text: 'text-xs', padding: 'px-2 py-0.5' },
76
+ md: { icon: 16, text: 'text-sm', padding: 'px-2.5 py-1' },
77
+ lg: { icon: 20, text: 'text-base', padding: 'px-3 py-1.5' }
78
+ };
79
+
80
+ const { icon: iconSize, text: textSize, padding } = sizeClasses[size];
81
+
82
+ if (iconOnly) {
83
+ return (
84
+ <Icon
85
+ size={iconSize}
86
+ className={`${config.color} ${config.animate ? 'animate-spin' : ''} ${className}`}
87
+ />
88
+ );
89
+ }
90
+
91
+ return (
92
+ <div className={`inline-flex items-center gap-1.5 ${padding} rounded-full border ${config.bg} ${config.border} ${className}`}>
93
+ <Icon
94
+ size={iconSize}
95
+ className={`${config.color} ${config.animate ? 'animate-spin' : ''}`}
96
+ />
97
+ {showLabel && (
98
+ <span className={`font-medium ${config.color} ${textSize}`}>
99
+ {config.label}
100
+ </span>
101
+ )}
102
+ </div>
103
+ );
104
+ }
105
+
106
+ // Badge variant for minimal display
107
+ export function StatusBadge({ status, className = '' }) {
108
+ return <ExecutionStatus status={status} size="sm" className={className} />;
109
+ }
110
+
111
+ // Icon-only variant
112
+ export function StatusIcon({ status, size = 16, className = '' }) {
113
+ const config = statusConfig[status?.toLowerCase()] || statusConfig.pending;
114
+ const Icon = config.icon;
115
+
116
+ return (
117
+ <Icon
118
+ size={size}
119
+ className={`${config.color} ${config.animate ? 'animate-spin' : ''} ${className}`}
120
+ />
121
+ );
122
+ }
@@ -0,0 +1,25 @@
1
+ import React from 'react';
2
+
3
+ export function KeyValue({ label, value, icon, className = '', valueClassName = '' }) {
4
+ return (
5
+ <div className={`flex items-start justify-between p-3 bg-slate-50 dark:bg-slate-800/50 rounded-lg border border-slate-200 dark:border-slate-700 ${className}`}>
6
+ <div className="flex items-center gap-2">
7
+ {icon && <span className="text-slate-400">{icon}</span>}
8
+ <span className="text-sm font-medium text-slate-600 dark:text-slate-400">{label}</span>
9
+ </div>
10
+ <span className={`text-sm font-mono font-semibold text-slate-900 dark:text-white text-right ${valueClassName}`}>
11
+ {value}
12
+ </span>
13
+ </div>
14
+ );
15
+ }
16
+
17
+ export function KeyValueGrid({ items, columns = 2, className = '' }) {
18
+ return (
19
+ <div className={`grid grid-cols-1 md:grid-cols-${columns} gap-3 ${className}`}>
20
+ {items.map((item, idx) => (
21
+ <KeyValue key={idx} {...item} />
22
+ ))}
23
+ </div>
24
+ );
25
+ }
@@ -0,0 +1,134 @@
1
+ import React from 'react';
2
+ import { ChevronDown, FolderOpen, Globe } from 'lucide-react';
3
+ import { useProject } from '../../contexts/ProjectContext';
4
+ import { motion, AnimatePresence } from 'framer-motion';
5
+
6
+ export function ProjectSelector() {
7
+ const { selectedProject, projects, loading, selectProject, clearProject } = useProject();
8
+ const [isOpen, setIsOpen] = React.useState(false);
9
+
10
+ const currentProject = projects.find(p => p.name === selectedProject);
11
+
12
+ return (
13
+ <div className="relative">
14
+ <button
15
+ onClick={() => setIsOpen(!isOpen)}
16
+ className="flex items-center gap-2 px-4 py-2 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 min-w-[200px]"
17
+ >
18
+ {selectedProject ? (
19
+ <>
20
+ <FolderOpen size={18} className="text-primary-600 dark:text-primary-400" />
21
+ <div className="flex-1 text-left">
22
+ <div className="text-sm font-medium text-slate-900 dark:text-white">
23
+ {currentProject?.name || selectedProject}
24
+ </div>
25
+ {currentProject?.description && (
26
+ <div className="text-xs text-slate-500 dark:text-slate-400 truncate max-w-[150px]">
27
+ {currentProject.description}
28
+ </div>
29
+ )}
30
+ </div>
31
+ </>
32
+ ) : (
33
+ <>
34
+ <Globe size={18} className="text-slate-400" />
35
+ <span className="flex-1 text-left text-sm font-medium text-slate-700 dark:text-slate-300">
36
+ All Projects
37
+ </span>
38
+ </>
39
+ )}
40
+ <ChevronDown
41
+ size={16}
42
+ className={`text-slate-400 transition-transform ${isOpen ? 'rotate-180' : ''}`}
43
+ />
44
+ </button>
45
+
46
+ <AnimatePresence>
47
+ {isOpen && (
48
+ <>
49
+ {/* Backdrop */}
50
+ <div
51
+ className="fixed inset-0 z-10"
52
+ onClick={() => setIsOpen(false)}
53
+ />
54
+
55
+ {/* Dropdown */}
56
+ <motion.div
57
+ initial={{ opacity: 0, y: -10 }}
58
+ animate={{ opacity: 1, y: 0 }}
59
+ exit={{ opacity: 0, y: -10 }}
60
+ transition={{ duration: 0.15 }}
61
+ className="absolute top-full mt-2 left-0 w-full min-w-[280px] bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg shadow-xl z-20 max-h-[400px] overflow-y-auto"
62
+ >
63
+ {/* All Projects Option */}
64
+ <button
65
+ onClick={() => {
66
+ clearProject();
67
+ setIsOpen(false);
68
+ }}
69
+ className={`w-full flex items-center gap-3 px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors ${!selectedProject ? 'bg-primary-50 dark:bg-primary-900/20' : ''
70
+ }`}
71
+ >
72
+ <Globe size={18} className="text-slate-400" />
73
+ <div className="flex-1 text-left">
74
+ <div className="text-sm font-medium text-slate-900 dark:text-white">
75
+ All Projects
76
+ </div>
77
+ <div className="text-xs text-slate-500 dark:text-slate-400">
78
+ View data from all projects
79
+ </div>
80
+ </div>
81
+ {!selectedProject && (
82
+ <div className="w-2 h-2 rounded-full bg-primary-600" />
83
+ )}
84
+ </button>
85
+
86
+ <div className="border-t border-slate-200 dark:border-slate-700" />
87
+
88
+ {/* Project List */}
89
+ {loading ? (
90
+ <div className="px-4 py-8 text-center text-slate-500 dark:text-slate-400 text-sm">
91
+ Loading projects...
92
+ </div>
93
+ ) : projects.length === 0 ? (
94
+ <div className="px-4 py-8 text-center text-slate-500 dark:text-slate-400 text-sm">
95
+ No projects yet
96
+ </div>
97
+ ) : (
98
+ projects.map(project => (
99
+ <button
100
+ key={project.name}
101
+ onClick={() => {
102
+ selectProject(project.name);
103
+ setIsOpen(false);
104
+ }}
105
+ className={`w-full flex items-center gap-3 px-4 py-3 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors ${selectedProject === project.name ? 'bg-primary-50 dark:bg-primary-900/20' : ''
106
+ }`}
107
+ >
108
+ <FolderOpen
109
+ size={18}
110
+ className={selectedProject === project.name ? 'text-primary-600 dark:text-primary-400' : 'text-slate-400'}
111
+ />
112
+ <div className="flex-1 text-left">
113
+ <div className="text-sm font-medium text-slate-900 dark:text-white">
114
+ {project.name}
115
+ </div>
116
+ {project.description && (
117
+ <div className="text-xs text-slate-500 dark:text-slate-400">
118
+ {project.description}
119
+ </div>
120
+ )}
121
+ </div>
122
+ {selectedProject === project.name && (
123
+ <div className="w-2 h-2 rounded-full bg-primary-600" />
124
+ )}
125
+ </button>
126
+ ))
127
+ )}
128
+ </motion.div>
129
+ </>
130
+ )}
131
+ </AnimatePresence>
132
+ </div>
133
+ );
134
+ }
@@ -0,0 +1,79 @@
1
+ import React, { createContext, useContext, useState, useEffect } from 'react';
2
+
3
+ const ProjectContext = createContext();
4
+
5
+ export function ProjectProvider({ children }) {
6
+ const [selectedProject, setSelectedProject] = useState(() => {
7
+ // Check localStorage first
8
+ const saved = localStorage.getItem('flowyml-selected-project');
9
+ return saved || null; // null means "All Projects"
10
+ });
11
+
12
+ const [projects, setProjects] = useState([]);
13
+ const [loading, setLoading] = useState(true);
14
+
15
+ useEffect(() => {
16
+ // Fetch available projects
17
+ fetch('/api/projects/')
18
+ .then(res => res.json())
19
+ .then(data => {
20
+ setProjects(data || []);
21
+ setLoading(false);
22
+ })
23
+ .catch(err => {
24
+ console.error('Failed to fetch projects:', err);
25
+ setLoading(false);
26
+ });
27
+ }, []);
28
+
29
+ useEffect(() => {
30
+ // Save to localStorage
31
+ if (selectedProject) {
32
+ localStorage.setItem('flowyml-selected-project', selectedProject);
33
+ } else {
34
+ localStorage.removeItem('flowyml-selected-project');
35
+ }
36
+ }, [selectedProject]);
37
+
38
+ const selectProject = (projectName) => {
39
+ setSelectedProject(projectName);
40
+ };
41
+
42
+ const clearProject = () => {
43
+ setSelectedProject(null);
44
+ };
45
+
46
+ const refreshProjects = async () => {
47
+ setLoading(true);
48
+ try {
49
+ const res = await fetch('/api/projects/');
50
+ const data = await res.json();
51
+ setProjects(data || []);
52
+ } catch (err) {
53
+ console.error('Failed to refresh projects:', err);
54
+ } finally {
55
+ setLoading(false);
56
+ }
57
+ };
58
+
59
+ return (
60
+ <ProjectContext.Provider value={{
61
+ selectedProject,
62
+ projects,
63
+ loading,
64
+ selectProject,
65
+ clearProject,
66
+ refreshProjects
67
+ }}>
68
+ {children}
69
+ </ProjectContext.Provider>
70
+ );
71
+ }
72
+
73
+ export function useProject() {
74
+ const context = useContext(ProjectContext);
75
+ if (!context) {
76
+ throw new Error('useProject must be used within ProjectProvider');
77
+ }
78
+ return context;
79
+ }
@@ -0,0 +1,54 @@
1
+ import React, { createContext, useContext, useState, useEffect } from 'react';
2
+
3
+ const ThemeContext = createContext();
4
+
5
+ export function ThemeProvider({ children }) {
6
+ const [theme, setTheme] = useState(() => {
7
+ // Check localStorage first
8
+ const savedTheme = localStorage.getItem('flowyml-theme');
9
+ if (savedTheme) {
10
+ // Apply immediately before React renders
11
+ document.documentElement.classList.remove('light', 'dark');
12
+ document.documentElement.classList.add(savedTheme);
13
+ return savedTheme;
14
+ }
15
+
16
+ // Check system preference
17
+ if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
18
+ document.documentElement.classList.remove('light');
19
+ document.documentElement.classList.add('dark');
20
+ return 'dark';
21
+ }
22
+
23
+ document.documentElement.classList.remove('dark');
24
+ document.documentElement.classList.add('light');
25
+ return 'light';
26
+ });
27
+
28
+ useEffect(() => {
29
+ // Apply theme to document
30
+ document.documentElement.classList.remove('light', 'dark');
31
+ document.documentElement.classList.add(theme);
32
+
33
+ // Save to localStorage
34
+ localStorage.setItem('flowyml-theme', theme);
35
+ }, [theme]);
36
+
37
+ const toggleTheme = () => {
38
+ setTheme(prev => prev === 'light' ? 'dark' : 'light');
39
+ };
40
+
41
+ return (
42
+ <ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
43
+ {children}
44
+ </ThemeContext.Provider>
45
+ );
46
+ }
47
+
48
+ export function useTheme() {
49
+ const context = useContext(ThemeContext);
50
+ if (!context) {
51
+ throw new Error('useTheme must be used within ThemeProvider');
52
+ }
53
+ return context;
54
+ }
@@ -0,0 +1,11 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ :root {
6
+ font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
7
+ }
8
+
9
+ body {
10
+ @apply bg-slate-50 text-slate-900;
11
+ }
@@ -0,0 +1,23 @@
1
+ import React, { useState } from 'react';
2
+ import { Outlet } from 'react-router-dom';
3
+ import { Sidebar } from '../components/sidebar/Sidebar';
4
+ import { Header } from '../components/header/Header';
5
+
6
+ export function MainLayout() {
7
+ const [collapsed, setCollapsed] = useState(false);
8
+
9
+ return (
10
+ <div className="flex h-screen bg-slate-50 dark:bg-slate-900 overflow-hidden">
11
+ <Sidebar collapsed={collapsed} setCollapsed={setCollapsed} />
12
+
13
+ <div className="flex-1 flex flex-col min-w-0">
14
+ <Header />
15
+ <main className="flex-1 overflow-y-auto p-6 scrollbar-thin scrollbar-thumb-slate-200 dark:scrollbar-thumb-slate-700">
16
+ <div className="max-w-7xl mx-auto w-full">
17
+ <Outlet />
18
+ </div>
19
+ </main>
20
+ </div>
21
+ </div>
22
+ );
23
+ }
@@ -0,0 +1,10 @@
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App.jsx'
4
+ import './index.css'
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>,
10
+ )
@@ -0,0 +1,39 @@
1
+ import { createBrowserRouter } from 'react-router-dom';
2
+ import { MainLayout } from '../layouts/MainLayout';
3
+ import { Dashboard } from '../app/dashboard/page';
4
+ import { Pipelines } from '../app/pipelines/page';
5
+ import { Runs } from '../app/runs/page';
6
+ import { RunDetails } from '../app/runs/[runId]/page';
7
+ import { Assets } from '../app/assets/page';
8
+ import { Experiments } from '../app/experiments/page';
9
+ import { ExperimentDetails } from '../app/experiments/[experimentId]/page';
10
+ import { Traces } from '../app/traces/page';
11
+ import { Projects } from '../app/projects/page';
12
+ import { Schedules } from '../app/schedules/page';
13
+ import { Leaderboard } from '../app/leaderboard/page';
14
+ import { Plugins } from '../app/plugins/page';
15
+ import { Settings } from '../app/settings/page';
16
+ import { TokenManagement } from '../app/tokens/page';
17
+
18
+ export const router = createBrowserRouter([
19
+ {
20
+ path: '/',
21
+ element: <MainLayout />,
22
+ children: [
23
+ { index: true, element: <Dashboard /> },
24
+ { path: 'pipelines', element: <Pipelines /> },
25
+ { path: 'runs', element: <Runs /> },
26
+ { path: 'runs/:runId', element: <RunDetails /> },
27
+ { path: 'assets', element: <Assets /> },
28
+ { path: 'experiments', element: <Experiments /> },
29
+ { path: 'experiments/:experimentId', element: <ExperimentDetails /> },
30
+ { path: 'traces', element: <Traces /> },
31
+ { path: 'projects', element: <Projects /> },
32
+ { path: 'schedules', element: <Schedules /> },
33
+ { path: 'leaderboard', element: <Leaderboard /> },
34
+ { path: 'plugins', element: <Plugins /> },
35
+ { path: 'settings', element: <Settings /> },
36
+ { path: 'tokens', element: <TokenManagement /> },
37
+ ],
38
+ },
39
+ ]);
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Service for managing plugins and integrations.
3
+ * Communicates with the backend API for plugin operations.
4
+ */
5
+
6
+ const API_BASE_URL = '/api';
7
+
8
+ class PluginService {
9
+ async getAvailablePlugins() {
10
+ try {
11
+ const response = await fetch(`${API_BASE_URL}/plugins/available`);
12
+ if (!response.ok) throw new Error('Failed to fetch available plugins');
13
+ return await response.json();
14
+ } catch (error) {
15
+ console.error('Error fetching available plugins:', error);
16
+ return [];
17
+ }
18
+ }
19
+
20
+ async getInstalledPlugins() {
21
+ try {
22
+ const response = await fetch(`${API_BASE_URL}/plugins/installed`);
23
+ if (!response.ok) throw new Error('Failed to fetch installed plugins');
24
+ return await response.json();
25
+ } catch (error) {
26
+ console.error('Error fetching installed plugins:', error);
27
+ return [];
28
+ }
29
+ }
30
+
31
+ async installPlugin(pluginId) {
32
+ try {
33
+ const response = await fetch(`${API_BASE_URL}/plugins/install`, {
34
+ method: 'POST',
35
+ headers: { 'Content-Type': 'application/json' },
36
+ body: JSON.stringify({ plugin_id: pluginId })
37
+ });
38
+
39
+ if (!response.ok) {
40
+ const error = await response.json();
41
+ throw new Error(error.detail || 'Installation failed');
42
+ }
43
+
44
+ return await response.json();
45
+ } catch (error) {
46
+ console.error('Error installing plugin:', error);
47
+ throw error;
48
+ }
49
+ }
50
+
51
+ async uninstallPlugin(pluginId) {
52
+ try {
53
+ const response = await fetch(`${API_BASE_URL}/plugins/uninstall/${pluginId}`, {
54
+ method: 'POST'
55
+ });
56
+
57
+ if (!response.ok) {
58
+ const error = await response.json();
59
+ throw new Error(error.detail || 'Uninstall failed');
60
+ }
61
+
62
+ return await response.json();
63
+ } catch (error) {
64
+ console.error('Error uninstalling plugin:', error);
65
+ throw error;
66
+ }
67
+ }
68
+
69
+ async importZenMLStack(stackName) {
70
+ try {
71
+ const response = await fetch(`${API_BASE_URL}/plugins/import-stack`, {
72
+ method: 'POST',
73
+ headers: { 'Content-Type': 'application/json' },
74
+ body: JSON.stringify({ stack_name: stackName })
75
+ });
76
+
77
+ if (!response.ok) {
78
+ const error = await response.json();
79
+ throw new Error(error.detail || 'Import failed');
80
+ }
81
+
82
+ return await response.json();
83
+ } catch (error) {
84
+ console.error('Error importing stack:', error);
85
+ throw error;
86
+ }
87
+ }
88
+ }
89
+
90
+ export const pluginService = new PluginService();
@@ -0,0 +1,47 @@
1
+ import { useState, useEffect } from 'react';
2
+
3
+ // Cache for config
4
+ let configCache = null;
5
+
6
+ export const getConfig = async () => {
7
+ if (configCache) return configCache;
8
+ try {
9
+ const res = await fetch('/api/config');
10
+ configCache = await res.json();
11
+ return configCache;
12
+ } catch (err) {
13
+ console.error('Failed to fetch config:', err);
14
+ return { execution_mode: 'local' };
15
+ }
16
+ };
17
+
18
+ export const getBaseUrl = async () => {
19
+ const config = await getConfig();
20
+ if (config.execution_mode === 'remote' && config.remote_server_url) {
21
+ return config.remote_server_url;
22
+ }
23
+ return '';
24
+ };
25
+
26
+ export const fetchApi = async (endpoint, options = {}) => {
27
+ const baseUrl = await getBaseUrl();
28
+ // Ensure endpoint starts with /
29
+ const path = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
30
+ const url = `${baseUrl}${path}`;
31
+
32
+ return fetch(url, options);
33
+ };
34
+
35
+ export const useConfig = () => {
36
+ const [config, setConfig] = useState(null);
37
+ const [loading, setLoading] = useState(true);
38
+
39
+ useEffect(() => {
40
+ getConfig().then(cfg => {
41
+ setConfig(cfg);
42
+ setLoading(false);
43
+ });
44
+ }, []);
45
+
46
+ return { config, loading };
47
+ };
@@ -0,0 +1,6 @@
1
+ import { clsx } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs) {
5
+ return twMerge(clsx(inputs))
6
+ }