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,246 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { fetchApi } from '../../utils/api';
3
+ import { Activity, Zap, MessageSquare, Clock, DollarSign } from 'lucide-react';
4
+ import { useProject } from '../../contexts/ProjectContext';
5
+
6
+ export function Traces() {
7
+ const [traces, setTraces] = useState([]);
8
+ const [selectedTrace, setSelectedTrace] = useState(null);
9
+ const [loading, setLoading] = useState(true);
10
+ const [filterType, setFilterType] = useState('all');
11
+ const { selectedProject } = useProject();
12
+
13
+ useEffect(() => {
14
+ fetchTraces();
15
+ }, [filterType, selectedProject]);
16
+
17
+ const fetchTraces = async () => {
18
+ setLoading(true);
19
+ try {
20
+ const params = new URLSearchParams();
21
+ if (filterType !== 'all') {
22
+ params.append('event_type', filterType);
23
+ }
24
+ if (selectedProject) {
25
+ params.append('project', selectedProject);
26
+ }
27
+
28
+ const response = await fetchApi(`/api/traces?${params}`);
29
+ const data = await response.json();
30
+ setTraces(data);
31
+ } catch (error) {
32
+ console.error('Failed to fetch traces:', error);
33
+ } finally {
34
+ setLoading(false);
35
+ }
36
+ };
37
+
38
+ const fetchTraceDetails = async (traceId) => {
39
+ try {
40
+ const response = await fetchApi(`/api/traces/${traceId}`);
41
+ const data = await response.json();
42
+ setSelectedTrace(data);
43
+ } catch (error) {
44
+ console.error('Failed to fetch trace details:', error);
45
+ }
46
+ };
47
+
48
+ const formatDuration = (duration) => {
49
+ if (!duration) return 'N/A';
50
+ return `${(duration * 1000).toFixed(0)}ms`;
51
+ };
52
+
53
+ const getStatusColor = (status) => {
54
+ switch (status) {
55
+ case 'success': return 'text-green-400';
56
+ case 'error': return 'text-red-400';
57
+ case 'running': return 'text-yellow-400';
58
+ default: return 'text-gray-400';
59
+ }
60
+ };
61
+
62
+ const getEventTypeIcon = (type) => {
63
+ switch (type) {
64
+ case 'llm': return <MessageSquare className="w-4 h-4" />;
65
+ case 'tool': return <Zap className="w-4 h-4" />;
66
+ default: return <Activity className="w-4 h-4" />;
67
+ }
68
+ };
69
+
70
+ const TraceTree = ({ events, level = 0 }) => {
71
+ if (!events) return null;
72
+
73
+ return (
74
+ <div className={`pl-${level * 4}`}>
75
+ {events.map((event, idx) => (
76
+ <div key={idx} className="mb-2">
77
+ <div className="flex items-center gap-2 p-2 bg-gray-800/50 rounded border border-gray-700/50 hover:border-blue-500/50 transition-colors">
78
+ <div className="flex items-center gap-2 flex-1">
79
+ {getEventTypeIcon(event.event_type)}
80
+ <span className="font-medium">{event.name}</span>
81
+ <span className={`text-sm ${getStatusColor(event.status)}`}>
82
+ {event.status}
83
+ </span>
84
+ </div>
85
+
86
+ <div className="flex items-center gap-4 text-sm text-gray-400">
87
+ {event.duration && (
88
+ <div className="flex items-center gap-1">
89
+ <Clock className="w-3 h-3" />
90
+ {formatDuration(event.duration)}
91
+ </div>
92
+ )}
93
+
94
+ {event.total_tokens > 0 && (
95
+ <div className="flex items-center gap-1">
96
+ <Activity className="w-3 h-3" />
97
+ {event.total_tokens} tokens
98
+ </div>
99
+ )}
100
+
101
+ {event.cost > 0 && (
102
+ <div className="flex items-center gap-1">
103
+ <DollarSign className="w-3 h-3" />
104
+ ${event.cost.toFixed(4)}
105
+ </div>
106
+ )}
107
+ </div>
108
+ </div>
109
+
110
+ {event.children && event.children.length > 0 && (
111
+ <div className="ml-6 mt-2 border-l-2 border-gray-700/50 pl-2">
112
+ <TraceTree events={event.children} level={level + 1} />
113
+ </div>
114
+ )}
115
+ </div>
116
+ ))}
117
+ </div>
118
+ );
119
+ };
120
+
121
+ return (
122
+ <div className="p-6">
123
+ <div className="flex justify-between items-center mb-6">
124
+ <h1 className="text-2xl font-bold">🔍 LLM Traces</h1>
125
+
126
+ <div className="flex gap-2">
127
+ <select
128
+ value={filterType}
129
+ onChange={(e) => setFilterType(e.target.value)}
130
+ className="px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg"
131
+ >
132
+ <option value="all">All Types</option>
133
+ <option value="llm">LLM Calls</option>
134
+ <option value="tool">Tool Calls</option>
135
+ <option value="chain">Chains</option>
136
+ <option value="agent">Agents</option>
137
+ </select>
138
+
139
+ <button
140
+ onClick={fetchTraces}
141
+ className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
142
+ >
143
+ Refresh
144
+ </button>
145
+ </div>
146
+ </div>
147
+
148
+ {loading ? (
149
+ <div className="text-center py-12">
150
+ <div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
151
+ <p className="mt-4 text-gray-400">Loading traces...</p>
152
+ </div>
153
+ ) : traces.length === 0 ? (
154
+ <div className="text-center py-12 bg-gray-800/30 rounded-lg border-2 border-dashed border-gray-700">
155
+ <Activity className="w-12 h-12 mx-auto text-gray-600 mb-4" />
156
+ <p className="text-gray-400">No traces found</p>
157
+ <p className="text-sm text-gray-500 mt-2">
158
+ Use @trace_llm decorator to track LLM calls
159
+ </p>
160
+ </div>
161
+ ) : (
162
+ <div className="grid gap-4">
163
+ {traces.map((trace) => {
164
+ // Group by trace_id
165
+ const traceId = trace.trace_id;
166
+
167
+ return (
168
+ <div
169
+ key={trace.event_id}
170
+ className="bg-gray-800/50 rounded-lg border border-gray-700/50 p-4 hover:border-blue-500/50 transition-all cursor-pointer"
171
+ onClick={() => fetchTraceDetails(traceId)}
172
+ >
173
+ <div className="flex items-start justify-between mb-3">
174
+ <div className="flex items-center gap-3">
175
+ {getEventTypeIcon(trace.event_type)}
176
+ <div>
177
+ <h3 className="font-semibold text-lg">{trace.name}</h3>
178
+ <p className="text-sm text-gray-400">
179
+ Trace ID: {traceId.slice(0, 8)}...
180
+ </p>
181
+ </div>
182
+ </div>
183
+
184
+ <span className={`px-3 py-1 rounded-full text-sm ${getStatusColor(trace.status)}`}>
185
+ {trace.status}
186
+ </span>
187
+ </div>
188
+
189
+ <div className="grid grid-cols-4 gap-4 text-sm">
190
+ <div>
191
+ <span className="text-gray-500">Duration:</span>
192
+ <span className="ml-2 text-gray-300">{formatDuration(trace.duration)}</span>
193
+ </div>
194
+
195
+ {trace.model && (
196
+ <div>
197
+ <span className="text-gray-500">Model:</span>
198
+ <span className="ml-2 text-gray-300">{trace.model}</span>
199
+ </div>
200
+ )}
201
+
202
+ {trace.total_tokens > 0 && (
203
+ <div>
204
+ <span className="text-gray-500">Tokens:</span>
205
+ <span className="ml-2 text-gray-300">
206
+ {trace.total_tokens} ({trace.prompt_tokens}/{trace.completion_tokens})
207
+ </span>
208
+ </div>
209
+ )}
210
+
211
+ {trace.cost > 0 && (
212
+ <div>
213
+ <span className="text-gray-500">Cost:</span>
214
+ <span className="ml-2 text-gray-300">${trace.cost.toFixed(4)}</span>
215
+ </div>
216
+ )}
217
+ </div>
218
+ </div>
219
+ );
220
+ })}
221
+ </div>
222
+ )}
223
+
224
+ {/* Trace Details Modal */}
225
+ {selectedTrace && (
226
+ <div className="fixed inset-0 bg-black/80 flex items-center justify-center p-6 z-50">
227
+ <div className="bg-gray-900 rounded-lg max-w-4xl w-full max-h-[80vh] overflow-auto border border-gray-700">
228
+ <div className="sticky top-0 bg-gray-900 border-b border-gray-700 p-4 flex justify-between items-center">
229
+ <h2 className="text-xl font-bold">Trace Details</h2>
230
+ <button
231
+ onClick={() => setSelectedTrace(null)}
232
+ className="px-4 py-2 bg-gray-800 hover:bg-gray-700 rounded-lg"
233
+ >
234
+ Close
235
+ </button>
236
+ </div>
237
+
238
+ <div className="p-6">
239
+ <TraceTree events={selectedTrace} />
240
+ </div>
241
+ </div>
242
+ </div>
243
+ )}
244
+ </div>
245
+ );
246
+ }
@@ -0,0 +1,108 @@
1
+ import React from 'react';
2
+ import { Link, useLocation } from 'react-router-dom';
3
+ import { LayoutDashboard, PlayCircle, FolderKanban, FlaskConical, Database, Settings, Trophy, Calendar, MessageSquare, Moon, Sun, Key, Package } from 'lucide-react';
4
+ import { useTheme } from '../contexts/ThemeContext';
5
+ import { ProjectSelector } from './ui/ProjectSelector';
6
+
7
+ function NavLink({ to, icon, label }) {
8
+ const location = useLocation();
9
+ const isActive = location.pathname === to;
10
+
11
+ return (
12
+ <Link
13
+ to={to}
14
+ className={`flex items-center gap-3 px-4 py-2.5 rounded-lg transition-all duration-200 group ${isActive
15
+ ? 'bg-primary-50 dark:bg-primary-900/20 text-primary-700 dark:text-primary-400 font-medium shadow-sm'
16
+ : 'text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-700 hover:text-slate-900 dark:hover:text-white'
17
+ }`}
18
+ >
19
+ <span className={`transition-colors ${isActive ? 'text-primary-600 dark:text-primary-400' : 'text-slate-400 group-hover:text-slate-600 dark:group-hover:text-slate-300'
20
+ }`}>
21
+ {icon}
22
+ </span>
23
+ <span className="text-sm">{label}</span>
24
+ </Link>
25
+ );
26
+ }
27
+
28
+ export function Layout({ children }) {
29
+ const { theme, toggleTheme } = useTheme();
30
+
31
+ const navLinks = [
32
+ { icon: LayoutDashboard, label: 'Dashboard', path: '/' },
33
+ { icon: FolderKanban, label: 'Projects', path: '/projects' },
34
+ { icon: PlayCircle, label: 'Pipelines', path: '/pipelines' },
35
+ { icon: Calendar, label: 'Schedules', path: '/schedules' },
36
+ { icon: PlayCircle, label: 'Runs', path: '/runs' },
37
+ { icon: Trophy, label: 'Leaderboard', path: '/leaderboard' },
38
+ { icon: Database, label: 'Assets', path: '/assets' },
39
+ { icon: FlaskConical, label: 'Experiments', path: '/experiments' },
40
+ { icon: MessageSquare, label: 'Traces', path: '/traces' },
41
+ ];
42
+
43
+ return (
44
+ <div className="flex h-screen bg-slate-50 dark:bg-slate-900">
45
+ {/* Sidebar */}
46
+ <aside className="w-64 bg-white dark:bg-slate-800 border-r border-slate-200 dark:border-slate-700 flex flex-col shadow-sm z-10">
47
+ <div className="p-6 border-b border-slate-100 dark:border-slate-700 flex items-center gap-3">
48
+ <div className="w-8 h-8 bg-primary-600 rounded-lg flex items-center justify-center shadow-lg shadow-primary-500/30">
49
+ <PlayCircle className="text-white w-5 h-5" />
50
+ </div>
51
+ <h1 className="text-xl font-bold text-slate-900 dark:text-white tracking-tight">flowyml</h1>
52
+ </div>
53
+
54
+ <nav className="flex-1 p-4 space-y-1 overflow-y-auto">
55
+ <div className="px-4 py-2 text-xs font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-wider">
56
+ Platform
57
+ </div>
58
+ {navLinks.map((link) => (
59
+ <NavLink
60
+ key={link.path}
61
+ to={link.path}
62
+ icon={<link.icon size={18} />}
63
+ label={link.label}
64
+ />
65
+ ))}
66
+ <div className="px-4 py-2 text-xs font-semibold text-slate-400 dark:text-slate-500 uppercase tracking-wider mt-4">
67
+ Settings
68
+ </div>
69
+ <NavLink to="/plugins" icon={<Package size={18} />} label="Plugins" />
70
+ <NavLink to="/tokens" icon={<Key size={18} />} label="API Tokens" />
71
+ <NavLink to="/settings" icon={<Settings size={18} />} label="Settings" />
72
+ </nav>
73
+
74
+ <div className="p-4 border-t border-slate-100 dark:border-slate-700">
75
+ <div className="bg-slate-50 dark:bg-slate-900 rounded-lg p-4 border border-slate-100 dark:border-slate-700">
76
+ <p className="text-xs font-medium text-slate-500 dark:text-slate-400">flowyml v0.1.0</p>
77
+ <p className="text-xs text-slate-400 dark:text-slate-500 mt-1">Local Environment</p>
78
+ </div>
79
+ </div>
80
+ </aside>
81
+
82
+ {/* Main Content */}
83
+ <main className="flex-1 flex flex-col overflow-hidden">
84
+ {/* Header with Project Selector */}
85
+ <header className="bg-white dark:bg-slate-800 border-b border-slate-200 dark:border-slate-700 px-6 py-4 flex items-center justify-between shadow-sm">
86
+ <ProjectSelector />
87
+
88
+ <button
89
+ onClick={toggleTheme}
90
+ className="p-2 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
91
+ aria-label="Toggle theme"
92
+ >
93
+ {theme === 'dark' ? (
94
+ <Sun size={20} className="text-slate-400" />
95
+ ) : (
96
+ <Moon size={20} className="text-slate-400" />
97
+ )}
98
+ </button>
99
+ </header>
100
+
101
+ {/* Page Content */}
102
+ <div className="flex-1 overflow-y-auto p-6">
103
+ {children}
104
+ </div>
105
+ </main>
106
+ </div>
107
+ );
108
+ }
@@ -0,0 +1,295 @@
1
+ import React, { useCallback, useMemo, useEffect } from 'react';
2
+ import ReactFlow, {
3
+ Background,
4
+ Controls,
5
+ MiniMap,
6
+ useNodesState,
7
+ useEdgesState,
8
+ MarkerType,
9
+ Handle,
10
+ Position
11
+ } from 'reactflow';
12
+ import 'reactflow/dist/style.css';
13
+ import { CheckCircle, XCircle, Clock, Loader, Database, Box, BarChart2, FileText, Layers } from 'lucide-react';
14
+ import dagre from 'dagre';
15
+
16
+ const stepNodeWidth = 240;
17
+ const stepNodeHeight = 100;
18
+ const artifactNodeWidth = 180;
19
+ const artifactNodeHeight = 50;
20
+
21
+ const getLayoutedElements = (nodes, edges, direction = 'TB') => {
22
+ const dagreGraph = new dagre.graphlib.Graph();
23
+ dagreGraph.setDefaultEdgeLabel(() => ({}));
24
+
25
+ const isHorizontal = direction === 'LR';
26
+ dagreGraph.setGraph({ rankdir: direction, nodesep: 80, ranksep: 100 });
27
+
28
+ nodes.forEach((node) => {
29
+ const width = node.type === 'artifact' ? artifactNodeWidth : stepNodeWidth;
30
+ const height = node.type === 'artifact' ? artifactNodeHeight : stepNodeHeight;
31
+ dagreGraph.setNode(node.id, { width, height });
32
+ });
33
+
34
+ edges.forEach((edge) => {
35
+ dagreGraph.setEdge(edge.source, edge.target);
36
+ });
37
+
38
+ dagre.layout(dagreGraph);
39
+
40
+ nodes.forEach((node) => {
41
+ const nodeWithPosition = dagreGraph.node(node.id);
42
+ node.targetPosition = isHorizontal ? 'left' : 'top';
43
+ node.sourcePosition = isHorizontal ? 'right' : 'bottom';
44
+
45
+ // Shift to center the node
46
+ const width = node.type === 'artifact' ? artifactNodeWidth : stepNodeWidth;
47
+ const height = node.type === 'artifact' ? artifactNodeHeight : stepNodeHeight;
48
+
49
+ node.position = {
50
+ x: nodeWithPosition.x - width / 2,
51
+ y: nodeWithPosition.y - height / 2,
52
+ };
53
+
54
+ return node;
55
+ });
56
+
57
+ return { nodes, edges };
58
+ };
59
+
60
+ export function PipelineGraph({ dag, steps, selectedStep, onStepSelect }) {
61
+ // Transform DAG data to ReactFlow format with Artifact Nodes
62
+ const { nodes: initialNodes, edges: initialEdges } = useMemo(() => {
63
+ if (!dag || !dag.nodes) return { nodes: [], edges: [] };
64
+
65
+ const nodes = [];
66
+ const edges = [];
67
+ const artifactIds = new Set();
68
+ const createdArtifacts = new Map(); // Map name -> id
69
+
70
+ // Helper to get or create artifact node
71
+ const getArtifactId = (name) => {
72
+ if (createdArtifacts.has(name)) return createdArtifacts.get(name);
73
+ const id = `artifact-${name}`;
74
+ if (!artifactIds.has(id)) {
75
+ nodes.push({
76
+ id: id,
77
+ type: 'artifact',
78
+ data: { label: name }
79
+ });
80
+ artifactIds.add(id);
81
+ createdArtifacts.set(name, id);
82
+ }
83
+ return id;
84
+ };
85
+
86
+ // 1. Create Step Nodes and Connections
87
+ dag.nodes.forEach(node => {
88
+ const stepData = steps?.[node.id] || {};
89
+ const status = stepData.success ? 'success' : stepData.error ? 'failed' : stepData.running ? 'running' : 'pending';
90
+
91
+ nodes.push({
92
+ id: node.id,
93
+ type: 'step',
94
+ data: {
95
+ label: node.name,
96
+ status,
97
+ duration: stepData.duration,
98
+ cached: stepData.cached,
99
+ selected: selectedStep === node.id
100
+ }
101
+ });
102
+
103
+ // Input Edges: Artifact -> Step
104
+ node.inputs?.forEach(inputName => {
105
+ const artifactId = getArtifactId(inputName);
106
+ edges.push({
107
+ id: `e-${artifactId}-${node.id}`,
108
+ source: artifactId,
109
+ target: node.id,
110
+ type: 'smoothstep',
111
+ animated: true,
112
+ style: { stroke: '#94a3b8', strokeWidth: 2 },
113
+ markerEnd: { type: MarkerType.ArrowClosed, color: '#94a3b8' }
114
+ });
115
+ });
116
+
117
+ // Output Edges: Step -> Artifact
118
+ node.outputs?.forEach(outputName => {
119
+ const artifactId = getArtifactId(outputName);
120
+ edges.push({
121
+ id: `e-${node.id}-${artifactId}`,
122
+ source: node.id,
123
+ target: artifactId,
124
+ type: 'smoothstep',
125
+ animated: true,
126
+ style: { stroke: '#94a3b8', strokeWidth: 2 },
127
+ markerEnd: { type: MarkerType.ArrowClosed, color: '#94a3b8' }
128
+ });
129
+ });
130
+ });
131
+
132
+ return { nodes, edges };
133
+ }, [dag, steps, selectedStep]);
134
+
135
+ const { nodes: layoutedNodes, edges: layoutedEdges } = useMemo(() => {
136
+ return getLayoutedElements(initialNodes, initialEdges, 'TB');
137
+ }, [initialNodes, initialEdges]);
138
+
139
+ const [nodes, setNodes, onNodesChange] = useNodesState(layoutedNodes);
140
+ const [edges, setEdges, onEdgesChange] = useEdgesState(layoutedEdges);
141
+
142
+ // Update nodes when selection changes or layout changes
143
+ useEffect(() => {
144
+ setNodes(layoutedNodes.map(node => {
145
+ if (node.type === 'step') {
146
+ return {
147
+ ...node,
148
+ data: {
149
+ ...node.data,
150
+ selected: selectedStep === node.id
151
+ }
152
+ };
153
+ }
154
+ return node;
155
+ }));
156
+ setEdges(layoutedEdges);
157
+ }, [layoutedNodes, layoutedEdges, selectedStep, setNodes, setEdges]);
158
+
159
+ const onNodeClick = useCallback((event, node) => {
160
+ if (node.type === 'step' && onStepSelect) {
161
+ onStepSelect(node.id);
162
+ }
163
+ }, [onStepSelect]);
164
+
165
+ const nodeTypes = useMemo(() => ({
166
+ step: CustomStepNode,
167
+ artifact: CustomArtifactNode
168
+ }), []);
169
+
170
+ return (
171
+ <div className="w-full h-full bg-slate-50/50 rounded-xl border border-slate-200 overflow-hidden">
172
+ <ReactFlow
173
+ nodes={nodes}
174
+ edges={edges}
175
+ onNodesChange={onNodesChange}
176
+ onEdgesChange={onEdgesChange}
177
+ onNodeClick={onNodeClick}
178
+ nodeTypes={nodeTypes}
179
+ fitView
180
+ attributionPosition="bottom-left"
181
+ minZoom={0.2}
182
+ maxZoom={1.5}
183
+ defaultViewport={{ x: 0, y: 0, zoom: 0.8 }}
184
+ fitViewOptions={{ padding: 0.2 }}
185
+ >
186
+ <Background color="#e2e8f0" gap={20} size={1} />
187
+ <Controls className="bg-white border border-slate-200 shadow-sm rounded-lg" />
188
+ <MiniMap
189
+ nodeColor={n => n.type === 'step' ? '#3b82f6' : '#cbd5e1'}
190
+ maskColor="rgba(241, 245, 249, 0.7)"
191
+ className="bg-white border border-slate-200 shadow-sm rounded-lg"
192
+ />
193
+ </ReactFlow>
194
+ </div>
195
+ );
196
+ }
197
+
198
+ function CustomStepNode({ data }) {
199
+ const statusConfig = {
200
+ success: {
201
+ icon: <CheckCircle size={18} />,
202
+ color: 'text-emerald-600',
203
+ bg: 'bg-white',
204
+ border: 'border-emerald-500',
205
+ ring: 'ring-emerald-200',
206
+ shadow: 'shadow-emerald-100'
207
+ },
208
+ failed: {
209
+ icon: <XCircle size={18} />,
210
+ color: 'text-rose-600',
211
+ bg: 'bg-white',
212
+ border: 'border-rose-500',
213
+ ring: 'ring-rose-200',
214
+ shadow: 'shadow-rose-100'
215
+ },
216
+ running: {
217
+ icon: <Loader size={18} className="animate-spin" />,
218
+ color: 'text-amber-600',
219
+ bg: 'bg-white',
220
+ border: 'border-amber-500',
221
+ ring: 'ring-amber-200',
222
+ shadow: 'shadow-amber-100'
223
+ },
224
+ pending: {
225
+ icon: <Clock size={18} />,
226
+ color: 'text-slate-400',
227
+ bg: 'bg-slate-50',
228
+ border: 'border-slate-300',
229
+ ring: 'ring-slate-200',
230
+ shadow: 'shadow-slate-100'
231
+ }
232
+ };
233
+
234
+ const config = statusConfig[data.status] || statusConfig.pending;
235
+
236
+ return (
237
+ <div
238
+ className={`
239
+ relative px-4 py-3 rounded-lg border-2 transition-all duration-200
240
+ ${config.bg} ${config.border}
241
+ ${data.selected ? `ring-4 ${config.ring} shadow-lg` : `hover:shadow-md ${config.shadow}`}
242
+ `}
243
+ style={{ width: stepNodeWidth, height: stepNodeHeight }}
244
+ >
245
+ <Handle type="target" position={Position.Top} className="!bg-slate-400 !w-2 !h-2" />
246
+
247
+ <div className="flex flex-col h-full justify-between">
248
+ <div className="flex items-start gap-3">
249
+ <div className={`p-1.5 rounded-md bg-slate-50 border border-slate-100 ${config.color}`}>
250
+ {config.icon}
251
+ </div>
252
+ <div className="min-w-0 flex-1">
253
+ <h3 className="font-bold text-slate-900 text-sm truncate" title={data.label}>
254
+ {data.label}
255
+ </h3>
256
+ <p className="text-xs text-slate-500 capitalize">{data.status}</p>
257
+ </div>
258
+ </div>
259
+
260
+ {data.duration !== undefined && (
261
+ <div className="flex items-center justify-between pt-2 border-t border-slate-100 mt-1">
262
+ <span className="text-xs text-slate-400 font-mono">
263
+ {data.duration.toFixed(2)}s
264
+ </span>
265
+ {data.cached && (
266
+ <span className="text-[10px] font-bold text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded uppercase tracking-wider">
267
+ Cached
268
+ </span>
269
+ )}
270
+ </div>
271
+ )}
272
+ </div>
273
+
274
+ <Handle type="source" position={Position.Bottom} className="!bg-slate-400 !w-2 !h-2" />
275
+ </div>
276
+ );
277
+ }
278
+
279
+ function CustomArtifactNode({ data }) {
280
+ return (
281
+ <div
282
+ className="px-3 py-2 rounded-full bg-slate-100 border border-slate-300 flex items-center justify-center gap-2 shadow-sm min-w-[120px]"
283
+ style={{ height: artifactNodeHeight }}
284
+ >
285
+ <Handle type="target" position={Position.Top} className="!bg-slate-400 !w-2 !h-2" />
286
+
287
+ <Database size={12} className="text-slate-500" />
288
+ <span className="text-xs font-medium text-slate-700 truncate max-w-[140px]" title={data.label}>
289
+ {data.label}
290
+ </span>
291
+
292
+ <Handle type="source" position={Position.Bottom} className="!bg-slate-400 !w-2 !h-2" />
293
+ </div>
294
+ );
295
+ }