flowyml 1.7.1__py3-none-any.whl → 1.8.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 (137) hide show
  1. flowyml/assets/base.py +15 -0
  2. flowyml/assets/dataset.py +570 -17
  3. flowyml/assets/metrics.py +5 -0
  4. flowyml/assets/model.py +1052 -15
  5. flowyml/cli/main.py +709 -0
  6. flowyml/cli/stack_cli.py +138 -25
  7. flowyml/core/__init__.py +17 -0
  8. flowyml/core/executor.py +231 -37
  9. flowyml/core/image_builder.py +129 -0
  10. flowyml/core/log_streamer.py +227 -0
  11. flowyml/core/orchestrator.py +59 -4
  12. flowyml/core/pipeline.py +65 -13
  13. flowyml/core/routing.py +558 -0
  14. flowyml/core/scheduler.py +88 -5
  15. flowyml/core/step.py +9 -1
  16. flowyml/core/step_grouping.py +49 -35
  17. flowyml/core/types.py +407 -0
  18. flowyml/integrations/keras.py +247 -82
  19. flowyml/monitoring/alerts.py +10 -0
  20. flowyml/monitoring/notifications.py +104 -25
  21. flowyml/monitoring/slack_blocks.py +323 -0
  22. flowyml/plugins/__init__.py +251 -0
  23. flowyml/plugins/alerters/__init__.py +1 -0
  24. flowyml/plugins/alerters/slack.py +168 -0
  25. flowyml/plugins/base.py +752 -0
  26. flowyml/plugins/config.py +478 -0
  27. flowyml/plugins/deployers/__init__.py +22 -0
  28. flowyml/plugins/deployers/gcp_cloud_run.py +200 -0
  29. flowyml/plugins/deployers/sagemaker.py +306 -0
  30. flowyml/plugins/deployers/vertex.py +290 -0
  31. flowyml/plugins/integration.py +369 -0
  32. flowyml/plugins/manager.py +510 -0
  33. flowyml/plugins/model_registries/__init__.py +22 -0
  34. flowyml/plugins/model_registries/mlflow.py +159 -0
  35. flowyml/plugins/model_registries/sagemaker.py +489 -0
  36. flowyml/plugins/model_registries/vertex.py +386 -0
  37. flowyml/plugins/orchestrators/__init__.py +13 -0
  38. flowyml/plugins/orchestrators/sagemaker.py +443 -0
  39. flowyml/plugins/orchestrators/vertex_ai.py +461 -0
  40. flowyml/plugins/registries/__init__.py +13 -0
  41. flowyml/plugins/registries/ecr.py +321 -0
  42. flowyml/plugins/registries/gcr.py +313 -0
  43. flowyml/plugins/registry.py +454 -0
  44. flowyml/plugins/stack.py +494 -0
  45. flowyml/plugins/stack_config.py +537 -0
  46. flowyml/plugins/stores/__init__.py +13 -0
  47. flowyml/plugins/stores/gcs.py +460 -0
  48. flowyml/plugins/stores/s3.py +453 -0
  49. flowyml/plugins/trackers/__init__.py +11 -0
  50. flowyml/plugins/trackers/mlflow.py +316 -0
  51. flowyml/plugins/validators/__init__.py +3 -0
  52. flowyml/plugins/validators/deepchecks.py +119 -0
  53. flowyml/registry/__init__.py +2 -1
  54. flowyml/registry/model_environment.py +109 -0
  55. flowyml/registry/model_registry.py +241 -96
  56. flowyml/serving/__init__.py +17 -0
  57. flowyml/serving/model_server.py +628 -0
  58. flowyml/stacks/__init__.py +60 -0
  59. flowyml/stacks/aws.py +93 -0
  60. flowyml/stacks/base.py +62 -0
  61. flowyml/stacks/components.py +12 -0
  62. flowyml/stacks/gcp.py +44 -9
  63. flowyml/stacks/plugins.py +115 -0
  64. flowyml/stacks/registry.py +2 -1
  65. flowyml/storage/sql.py +401 -12
  66. flowyml/tracking/experiment.py +8 -5
  67. flowyml/ui/backend/Dockerfile +87 -16
  68. flowyml/ui/backend/auth.py +12 -2
  69. flowyml/ui/backend/main.py +149 -5
  70. flowyml/ui/backend/routers/ai_context.py +226 -0
  71. flowyml/ui/backend/routers/assets.py +23 -4
  72. flowyml/ui/backend/routers/auth.py +96 -0
  73. flowyml/ui/backend/routers/deployments.py +660 -0
  74. flowyml/ui/backend/routers/model_explorer.py +597 -0
  75. flowyml/ui/backend/routers/plugins.py +103 -51
  76. flowyml/ui/backend/routers/projects.py +91 -8
  77. flowyml/ui/backend/routers/runs.py +132 -1
  78. flowyml/ui/backend/routers/schedules.py +54 -29
  79. flowyml/ui/backend/routers/templates.py +319 -0
  80. flowyml/ui/backend/routers/websocket.py +2 -2
  81. flowyml/ui/frontend/Dockerfile +55 -6
  82. flowyml/ui/frontend/dist/assets/index-B5AsPTSz.css +1 -0
  83. flowyml/ui/frontend/dist/assets/index-dFbZ8wD8.js +753 -0
  84. flowyml/ui/frontend/dist/index.html +2 -2
  85. flowyml/ui/frontend/dist/logo.png +0 -0
  86. flowyml/ui/frontend/nginx.conf +65 -4
  87. flowyml/ui/frontend/package-lock.json +1415 -74
  88. flowyml/ui/frontend/package.json +4 -0
  89. flowyml/ui/frontend/public/logo.png +0 -0
  90. flowyml/ui/frontend/src/App.jsx +10 -7
  91. flowyml/ui/frontend/src/app/assets/page.jsx +890 -321
  92. flowyml/ui/frontend/src/app/auth/Login.jsx +90 -0
  93. flowyml/ui/frontend/src/app/dashboard/page.jsx +8 -8
  94. flowyml/ui/frontend/src/app/deployments/page.jsx +786 -0
  95. flowyml/ui/frontend/src/app/model-explorer/page.jsx +1031 -0
  96. flowyml/ui/frontend/src/app/pipelines/page.jsx +12 -2
  97. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +19 -6
  98. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectMetricsPanel.jsx +1 -1
  99. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +601 -101
  100. flowyml/ui/frontend/src/app/runs/page.jsx +8 -2
  101. flowyml/ui/frontend/src/app/settings/page.jsx +267 -253
  102. flowyml/ui/frontend/src/components/ArtifactViewer.jsx +62 -2
  103. flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +424 -29
  104. flowyml/ui/frontend/src/components/AssetTreeHierarchy.jsx +119 -11
  105. flowyml/ui/frontend/src/components/DatasetViewer.jsx +753 -0
  106. flowyml/ui/frontend/src/components/Layout.jsx +6 -0
  107. flowyml/ui/frontend/src/components/PipelineGraph.jsx +79 -29
  108. flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +36 -6
  109. flowyml/ui/frontend/src/components/RunMetaPanel.jsx +113 -0
  110. flowyml/ui/frontend/src/components/TrainingHistoryChart.jsx +514 -0
  111. flowyml/ui/frontend/src/components/TrainingMetricsPanel.jsx +175 -0
  112. flowyml/ui/frontend/src/components/ai/AIAssistantButton.jsx +71 -0
  113. flowyml/ui/frontend/src/components/ai/AIAssistantPanel.jsx +420 -0
  114. flowyml/ui/frontend/src/components/header/Header.jsx +22 -0
  115. flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +4 -4
  116. flowyml/ui/frontend/src/components/plugins/{ZenMLIntegration.jsx → StackImport.jsx} +38 -12
  117. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +36 -13
  118. flowyml/ui/frontend/src/contexts/AIAssistantContext.jsx +245 -0
  119. flowyml/ui/frontend/src/contexts/AuthContext.jsx +108 -0
  120. flowyml/ui/frontend/src/hooks/useAIContext.js +156 -0
  121. flowyml/ui/frontend/src/hooks/useWebGPU.js +54 -0
  122. flowyml/ui/frontend/src/layouts/MainLayout.jsx +6 -0
  123. flowyml/ui/frontend/src/router/index.jsx +47 -20
  124. flowyml/ui/frontend/src/services/pluginService.js +3 -1
  125. flowyml/ui/server_manager.py +5 -5
  126. flowyml/ui/utils.py +157 -39
  127. flowyml/utils/config.py +37 -15
  128. flowyml/utils/model_introspection.py +123 -0
  129. flowyml/utils/observability.py +30 -0
  130. flowyml-1.8.0.dist-info/METADATA +174 -0
  131. {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/RECORD +134 -73
  132. {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/WHEEL +1 -1
  133. flowyml/ui/frontend/dist/assets/index-BqDQvp63.js +0 -630
  134. flowyml/ui/frontend/dist/assets/index-By4trVyv.css +0 -1
  135. flowyml-1.7.1.dist-info/METADATA +0 -477
  136. {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/entry_points.txt +0 -0
  137. {flowyml-1.7.1.dist-info → flowyml-1.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,71 @@
1
+ import React from 'react';
2
+ import { motion } from 'framer-motion';
3
+ import { Sparkles, Brain } from 'lucide-react';
4
+ import { useAIAssistant } from '../../contexts/AIAssistantContext';
5
+
6
+ export function AIAssistantButton() {
7
+ const { isOpen, setIsOpen, isLoading, isWebGPUSupported, isModelLoading } = useAIAssistant();
8
+
9
+ // Don't render if WebGPU check is still loading
10
+ if (isWebGPUSupported === null) return null;
11
+
12
+ return (
13
+ <motion.button
14
+ initial={{ scale: 0, opacity: 0 }}
15
+ animate={{ scale: 1, opacity: 1 }}
16
+ transition={{ delay: 0.5, type: 'spring', stiffness: 300, damping: 20 }}
17
+ onClick={() => setIsOpen(true)}
18
+ className={`fixed bottom-6 right-6 z-30 group ${isOpen ? 'hidden' : ''}`}
19
+ >
20
+ {/* Animated glow effect */}
21
+ <motion.div
22
+ className="absolute inset-0 rounded-full bg-gradient-to-r from-purple-500 to-indigo-500 blur-xl opacity-50"
23
+ animate={{
24
+ scale: isLoading || isModelLoading ? [1, 1.3, 1] : 1,
25
+ opacity: isLoading || isModelLoading ? [0.5, 0.8, 0.5] : 0.5
26
+ }}
27
+ transition={{
28
+ duration: 1.5,
29
+ repeat: isLoading || isModelLoading ? Infinity : 0,
30
+ ease: 'easeInOut'
31
+ }}
32
+ />
33
+
34
+ {/* Main button */}
35
+ <div className="relative w-14 h-14 rounded-full bg-gradient-to-br from-purple-600 to-indigo-600 shadow-xl shadow-purple-500/40 flex items-center justify-center transition-transform group-hover:scale-110">
36
+ {isLoading || isModelLoading ? (
37
+ <motion.div
38
+ animate={{ rotate: 360 }}
39
+ transition={{ duration: 2, repeat: Infinity, ease: 'linear' }}
40
+ >
41
+ <Brain size={24} className="text-white" />
42
+ </motion.div>
43
+ ) : (
44
+ <Sparkles size={24} className="text-white" />
45
+ )}
46
+ </div>
47
+
48
+ {/* Tooltip */}
49
+ <div className="absolute right-full mr-3 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none">
50
+ <div className="bg-slate-800 text-white text-sm px-3 py-2 rounded-lg whitespace-nowrap shadow-lg border border-slate-700">
51
+ {!isWebGPUSupported
52
+ ? 'AI Assistant (WebGPU required)'
53
+ : isModelLoading
54
+ ? 'Loading AI...'
55
+ : 'Ask FlowyML Assistant'
56
+ }
57
+ <div className="absolute right-0 top-1/2 -translate-y-1/2 translate-x-1 w-2 h-2 bg-slate-800 rotate-45 border-r border-t border-slate-700" />
58
+ </div>
59
+ </div>
60
+
61
+ {/* WebGPU warning indicator */}
62
+ {!isWebGPUSupported && (
63
+ <div className="absolute -top-1 -right-1 w-4 h-4 bg-amber-500 rounded-full flex items-center justify-center text-xs font-bold text-black">
64
+ !
65
+ </div>
66
+ )}
67
+ </motion.button>
68
+ );
69
+ }
70
+
71
+ export default AIAssistantButton;
@@ -0,0 +1,420 @@
1
+ import React, { useState, useRef, useEffect } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { X, Send, Trash2, Loader2, AlertCircle, Cpu, Zap, ChevronDown, Sparkles, Eye, EyeOff, Layers, FileText, Terminal, BarChart2 } from 'lucide-react';
4
+ import ReactMarkdown from 'react-markdown';
5
+ import remarkGfm from 'remark-gfm';
6
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
7
+ import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
8
+ import { useAIAssistant } from '../../contexts/AIAssistantContext';
9
+
10
+ // Custom markdown components for chat messages
11
+ const MarkdownComponents = {
12
+ code({ node, inline, className, children, ...props }) {
13
+ const match = /language-(\w+)/.exec(className || '');
14
+ const language = match ? match[1] : '';
15
+
16
+ if (!inline && language) {
17
+ return (
18
+ <div className="relative group my-3">
19
+ <div className="absolute right-2 top-2 opacity-0 group-hover:opacity-100 transition-opacity">
20
+ <button
21
+ onClick={() => navigator.clipboard.writeText(String(children))}
22
+ className="px-2 py-1 text-xs bg-slate-700 hover:bg-slate-600 rounded text-slate-300"
23
+ >
24
+ Copy
25
+ </button>
26
+ </div>
27
+ <SyntaxHighlighter
28
+ style={oneDark}
29
+ language={language}
30
+ PreTag="div"
31
+ className="rounded-lg text-sm !my-0"
32
+ {...props}
33
+ >
34
+ {String(children).replace(/\n$/, '')}
35
+ </SyntaxHighlighter>
36
+ </div>
37
+ );
38
+ }
39
+
40
+ return (
41
+ <code className="bg-slate-700/50 px-1.5 py-0.5 rounded text-sm text-purple-300" {...props}>
42
+ {children}
43
+ </code>
44
+ );
45
+ },
46
+ p({ children }) {
47
+ return <p className="mb-2 last:mb-0">{children}</p>;
48
+ },
49
+ ul({ children }) {
50
+ return <ul className="list-disc list-inside mb-2 space-y-1">{children}</ul>;
51
+ },
52
+ ol({ children }) {
53
+ return <ol className="list-decimal list-inside mb-2 space-y-1">{children}</ol>;
54
+ },
55
+ strong({ children }) {
56
+ return <strong className="font-semibold text-white">{children}</strong>;
57
+ },
58
+ a({ href, children }) {
59
+ return (
60
+ <a href={href} className="text-purple-400 hover:text-purple-300 underline" target="_blank" rel="noopener noreferrer">
61
+ {children}
62
+ </a>
63
+ );
64
+ }
65
+ };
66
+
67
+ // Chat message component
68
+ function ChatMessage({ message, isStreaming }) {
69
+ const isUser = message.role === 'user';
70
+
71
+ return (
72
+ <motion.div
73
+ initial={{ opacity: 0, y: 10 }}
74
+ animate={{ opacity: 1, y: 0 }}
75
+ className={`flex ${isUser ? 'justify-end' : 'justify-start'} mb-4`}
76
+ >
77
+ <div className={`max-w-[85%] ${isUser ? 'order-2' : 'order-1'}`}>
78
+ {!isUser && (
79
+ <div className="flex items-center gap-2 mb-1">
80
+ <div className="w-6 h-6 rounded-full bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center">
81
+ <Sparkles size={12} className="text-white" />
82
+ </div>
83
+ <span className="text-xs font-medium text-slate-400">FlowyML Assistant</span>
84
+ </div>
85
+ )}
86
+ <div
87
+ className={`rounded-2xl px-4 py-3 ${isUser
88
+ ? 'bg-gradient-to-r from-purple-600 to-indigo-600 text-white'
89
+ : 'bg-slate-800/80 text-slate-200 border border-slate-700/50'
90
+ }`}
91
+ >
92
+ {isUser ? (
93
+ <p className="text-sm">{message.content}</p>
94
+ ) : (
95
+ <div className="text-sm prose prose-invert prose-sm max-w-none">
96
+ <ReactMarkdown
97
+ remarkPlugins={[remarkGfm]}
98
+ components={MarkdownComponents}
99
+ >
100
+ {message.content}
101
+ </ReactMarkdown>
102
+ {isStreaming && (
103
+ <span className="inline-block w-2 h-4 bg-purple-400 animate-pulse ml-1" />
104
+ )}
105
+ </div>
106
+ )}
107
+ </div>
108
+ </div>
109
+ </motion.div>
110
+ );
111
+ }
112
+
113
+ // Model loading progress component
114
+ function LoadingProgress({ progress, status }) {
115
+ return (
116
+ <div className="p-6 text-center">
117
+ <div className="w-16 h-16 mx-auto mb-4 rounded-full bg-gradient-to-br from-purple-500/20 to-indigo-500/20 flex items-center justify-center">
118
+ <Loader2 className="w-8 h-8 text-purple-400 animate-spin" />
119
+ </div>
120
+ <h3 className="text-lg font-semibold text-white mb-2">Loading AI Model</h3>
121
+ <p className="text-sm text-slate-400 mb-4">{status}</p>
122
+ <div className="w-full bg-slate-700 rounded-full h-2 overflow-hidden">
123
+ <motion.div
124
+ className="h-full bg-gradient-to-r from-purple-500 to-indigo-500"
125
+ initial={{ width: 0 }}
126
+ animate={{ width: `${progress}%` }}
127
+ transition={{ duration: 0.3 }}
128
+ />
129
+ </div>
130
+ <p className="text-xs text-slate-500 mt-2">{progress}% complete</p>
131
+ </div>
132
+ );
133
+ }
134
+
135
+ // WebGPU not supported fallback
136
+ function WebGPUNotSupported() {
137
+ return (
138
+ <div className="p-6 text-center">
139
+ <div className="w-16 h-16 mx-auto mb-4 rounded-full bg-amber-500/20 flex items-center justify-center">
140
+ <AlertCircle className="w-8 h-8 text-amber-400" />
141
+ </div>
142
+ <h3 className="text-lg font-semibold text-white mb-2">WebGPU Not Available</h3>
143
+ <p className="text-sm text-slate-400 mb-4">
144
+ Your browser doesn't support WebGPU, which is required for local AI inference.
145
+ </p>
146
+ <div className="bg-slate-800/50 rounded-lg p-4 text-left text-sm">
147
+ <p className="text-slate-300 font-medium mb-2">Try one of these browsers:</p>
148
+ <ul className="text-slate-400 space-y-1">
149
+ <li>• Chrome 113+ (recommended)</li>
150
+ <li>• Edge 113+</li>
151
+ <li>• Safari 18.2+ (macOS/iOS)</li>
152
+ </ul>
153
+ </div>
154
+ </div>
155
+ );
156
+ }
157
+
158
+ // Context indicator component
159
+ function ContextIndicator({ context, enabled, onToggle }) {
160
+ if (!context) return null;
161
+
162
+ const pageType = context.pageType || 'page';
163
+ const icons = {
164
+ run: <Layers size={12} />,
165
+ pipeline: <FileText size={12} />,
166
+ logs: <Terminal size={12} />,
167
+ metrics: <BarChart2 size={12} />
168
+ };
169
+
170
+ return (
171
+ <div className="px-4 py-2 bg-gradient-to-r from-purple-900/40 to-indigo-900/40 border-b border-slate-700/50">
172
+ <div className="flex items-center justify-between">
173
+ <div className="flex items-center gap-2 text-xs">
174
+ <span className="text-slate-400">Context:</span>
175
+ <div className={`flex items-center gap-1.5 px-2 py-1 rounded-full ${enabled ? 'bg-purple-500/20 text-purple-300' : 'bg-slate-700/50 text-slate-500'}`}>
176
+ {icons[pageType] || <Eye size={12} />}
177
+ <span className="font-medium capitalize">{pageType}</span>
178
+ {context.pipelineName && (
179
+ <span className="text-slate-400">• {context.pipelineName}</span>
180
+ )}
181
+ </div>
182
+ </div>
183
+ <button
184
+ onClick={onToggle}
185
+ className={`flex items-center gap-1.5 px-2 py-1 rounded-lg text-xs font-medium transition-all ${enabled
186
+ ? 'bg-purple-500/20 text-purple-300 hover:bg-purple-500/30'
187
+ : 'bg-slate-700/50 text-slate-400 hover:bg-slate-700'
188
+ }`}
189
+ title={enabled ? 'Disable context sharing' : 'Enable context sharing'}
190
+ >
191
+ {enabled ? <Eye size={12} /> : <EyeOff size={12} />}
192
+ {enabled ? 'Sharing' : 'Off'}
193
+ </button>
194
+ </div>
195
+ {enabled && context.totalSteps !== undefined && (
196
+ <div className="flex items-center gap-3 mt-2 text-xs text-slate-400">
197
+ <span>{context.totalSteps} steps</span>
198
+ {context.failedSteps > 0 && (
199
+ <span className="text-red-400">{context.failedSteps} failed</span>
200
+ )}
201
+ {context.metrics?.length > 0 && (
202
+ <span>{context.metrics.length} metrics</span>
203
+ )}
204
+ </div>
205
+ )}
206
+ </div>
207
+ );
208
+ }
209
+
210
+ // Main panel component
211
+ export function AIAssistantPanel() {
212
+ const {
213
+ isOpen,
214
+ setIsOpen,
215
+ messages,
216
+ isLoading,
217
+ isModelLoading,
218
+ loadProgress,
219
+ loadStatus,
220
+ error,
221
+ isWebGPUSupported,
222
+ initEngine,
223
+ sendMessage,
224
+ cancelGeneration,
225
+ clearChat,
226
+ pipelineContext,
227
+ contextEnabled,
228
+ setContextEnabled
229
+ } = useAIAssistant();
230
+
231
+ const [input, setInput] = useState('');
232
+ const messagesEndRef = useRef(null);
233
+ const inputRef = useRef(null);
234
+
235
+ // Initialize engine when panel opens
236
+ useEffect(() => {
237
+ if (isOpen && isWebGPUSupported && !isModelLoading) {
238
+ initEngine();
239
+ }
240
+ }, [isOpen, isWebGPUSupported, isModelLoading, initEngine]);
241
+
242
+ // Auto-scroll to bottom
243
+ useEffect(() => {
244
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
245
+ }, [messages]);
246
+
247
+ // Focus input when panel opens
248
+ useEffect(() => {
249
+ if (isOpen && !isModelLoading && isWebGPUSupported) {
250
+ setTimeout(() => inputRef.current?.focus(), 100);
251
+ }
252
+ }, [isOpen, isModelLoading, isWebGPUSupported]);
253
+
254
+ // Handle send
255
+ const handleSend = () => {
256
+ if (!input.trim() || isLoading) return;
257
+ sendMessage(input.trim());
258
+ setInput('');
259
+ };
260
+
261
+ // Handle keyboard shortcuts
262
+ const handleKeyDown = (e) => {
263
+ if (e.key === 'Enter' && !e.shiftKey) {
264
+ e.preventDefault();
265
+ handleSend();
266
+ }
267
+ };
268
+
269
+ // Escape to close
270
+ useEffect(() => {
271
+ const handleEscape = (e) => {
272
+ if (e.key === 'Escape' && isOpen) {
273
+ setIsOpen(false);
274
+ }
275
+ };
276
+ window.addEventListener('keydown', handleEscape);
277
+ return () => window.removeEventListener('keydown', handleEscape);
278
+ }, [isOpen, setIsOpen]);
279
+
280
+ return (
281
+ <AnimatePresence>
282
+ {isOpen && (
283
+ <>
284
+ {/* Backdrop */}
285
+ <motion.div
286
+ initial={{ opacity: 0 }}
287
+ animate={{ opacity: 1 }}
288
+ exit={{ opacity: 0 }}
289
+ onClick={() => setIsOpen(false)}
290
+ className="fixed inset-0 bg-black/40 backdrop-blur-sm z-40"
291
+ />
292
+
293
+ {/* Panel */}
294
+ <motion.div
295
+ initial={{ opacity: 0, x: 400, scale: 0.95 }}
296
+ animate={{ opacity: 1, x: 0, scale: 1 }}
297
+ exit={{ opacity: 0, x: 400, scale: 0.95 }}
298
+ transition={{ type: 'spring', damping: 25, stiffness: 300 }}
299
+ className="fixed right-4 top-4 bottom-4 w-[420px] max-w-[calc(100vw-32px)] bg-slate-900/95 backdrop-blur-xl rounded-2xl shadow-2xl border border-slate-700/50 z-50 flex flex-col overflow-hidden"
300
+ >
301
+ {/* Header */}
302
+ <div className="flex items-center justify-between px-5 py-4 border-b border-slate-700/50 bg-gradient-to-r from-purple-900/30 to-indigo-900/30">
303
+ <div className="flex items-center gap-3">
304
+ <div className="w-10 h-10 rounded-xl bg-gradient-to-br from-purple-500 to-indigo-600 flex items-center justify-center shadow-lg shadow-purple-500/30">
305
+ <Sparkles size={20} className="text-white" />
306
+ </div>
307
+ <div>
308
+ <h2 className="font-semibold text-white">FlowyML Assistant</h2>
309
+ <div className="flex items-center gap-1.5 text-xs text-slate-400">
310
+ <Cpu size={10} />
311
+ <span>Powered by WebGPU</span>
312
+ <span className="w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse" />
313
+ </div>
314
+ </div>
315
+ </div>
316
+ <div className="flex items-center gap-2">
317
+ <button
318
+ onClick={clearChat}
319
+ className="p-2 hover:bg-slate-700/50 rounded-lg transition-colors"
320
+ title="Clear chat"
321
+ >
322
+ <Trash2 size={18} className="text-slate-400" />
323
+ </button>
324
+ <button
325
+ onClick={() => setIsOpen(false)}
326
+ className="p-2 hover:bg-slate-700/50 rounded-lg transition-colors"
327
+ >
328
+ <X size={18} className="text-slate-400" />
329
+ </button>
330
+ </div>
331
+ </div>
332
+
333
+ {/* Context Indicator */}
334
+ <ContextIndicator
335
+ context={pipelineContext}
336
+ enabled={contextEnabled}
337
+ onToggle={() => setContextEnabled(!contextEnabled)}
338
+ />
339
+
340
+ {/* Content */}
341
+ <div className="flex-1 overflow-y-auto">
342
+ {isWebGPUSupported === false ? (
343
+ <WebGPUNotSupported />
344
+ ) : isModelLoading ? (
345
+ <LoadingProgress progress={loadProgress} status={loadStatus} />
346
+ ) : error ? (
347
+ <div className="p-6 text-center">
348
+ <div className="w-16 h-16 mx-auto mb-4 rounded-full bg-red-500/20 flex items-center justify-center">
349
+ <AlertCircle className="w-8 h-8 text-red-400" />
350
+ </div>
351
+ <h3 className="text-lg font-semibold text-white mb-2">Error Loading Model</h3>
352
+ <p className="text-sm text-slate-400">{error}</p>
353
+ <button
354
+ onClick={initEngine}
355
+ className="mt-4 px-4 py-2 bg-purple-600 hover:bg-purple-500 rounded-lg text-sm font-medium transition-colors"
356
+ >
357
+ Retry
358
+ </button>
359
+ </div>
360
+ ) : (
361
+ <div className="p-4">
362
+ {messages.map((msg, idx) => (
363
+ <ChatMessage
364
+ key={idx}
365
+ message={msg}
366
+ isStreaming={isLoading && idx === messages.length - 1 && msg.role === 'assistant'}
367
+ />
368
+ ))}
369
+ <div ref={messagesEndRef} />
370
+ </div>
371
+ )}
372
+ </div>
373
+
374
+ {/* Input area */}
375
+ {isWebGPUSupported && !isModelLoading && !error && (
376
+ <div className="p-4 border-t border-slate-700/50 bg-slate-800/50">
377
+ <div className="flex items-end gap-2">
378
+ <div className="flex-1 relative">
379
+ <textarea
380
+ ref={inputRef}
381
+ value={input}
382
+ onChange={(e) => setInput(e.target.value)}
383
+ onKeyDown={handleKeyDown}
384
+ placeholder="Ask about FlowyML, pipelines, optimization..."
385
+ rows={1}
386
+ className="w-full bg-slate-700/50 border border-slate-600/50 rounded-xl px-4 py-3 text-sm text-white placeholder-slate-400 resize-none focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent transition-all"
387
+ style={{ minHeight: '44px', maxHeight: '120px' }}
388
+ />
389
+ </div>
390
+ <button
391
+ onClick={isLoading ? cancelGeneration : handleSend}
392
+ disabled={!input.trim() && !isLoading}
393
+ className={`p-3 rounded-xl transition-all ${isLoading
394
+ ? 'bg-red-500 hover:bg-red-400'
395
+ : input.trim()
396
+ ? 'bg-gradient-to-r from-purple-600 to-indigo-600 hover:from-purple-500 hover:to-indigo-500 shadow-lg shadow-purple-500/30'
397
+ : 'bg-slate-700/50 cursor-not-allowed'
398
+ }`}
399
+ >
400
+ {isLoading ? (
401
+ <X size={20} className="text-white" />
402
+ ) : (
403
+ <Send size={20} className="text-white" />
404
+ )}
405
+ </button>
406
+ </div>
407
+ <p className="text-xs text-slate-500 mt-2 text-center">
408
+ <Zap size={10} className="inline mr-1" />
409
+ Running locally • Your data never leaves this device
410
+ </p>
411
+ </div>
412
+ )}
413
+ </motion.div>
414
+ </>
415
+ )}
416
+ </AnimatePresence>
417
+ );
418
+ }
419
+
420
+ export default AIAssistantPanel;
@@ -4,11 +4,14 @@ import { Sun, Moon, ChevronRight, Home, Server, ExternalLink } from 'lucide-reac
4
4
  import { useTheme } from '../../contexts/ThemeContext';
5
5
  import { ProjectSelector } from '../ui/ProjectSelector';
6
6
  import { useConfig } from '../../utils/api';
7
+ import { useAuth } from '../../contexts/AuthContext';
8
+ import { LogOut, User } from 'lucide-react';
7
9
 
8
10
  export function Header() {
9
11
  const { theme, toggleTheme } = useTheme();
10
12
  const location = useLocation();
11
13
  const { config, loading } = useConfig();
14
+ const { user, logout } = useAuth();
12
15
 
13
16
  // Generate breadcrumbs from path
14
17
  const pathnames = location.pathname.split('/').filter((x) => x);
@@ -113,6 +116,25 @@ export function Header() {
113
116
  <Moon size={20} />
114
117
  )}
115
118
  </button>
119
+
120
+ {user && (
121
+ <>
122
+ <div className="h-6 w-px bg-slate-200 dark:bg-slate-700 mx-2" />
123
+ <div className="flex items-center gap-2">
124
+ <div className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-slate-100 dark:bg-slate-700/50 text-slate-700 dark:text-slate-300">
125
+ <User size={16} />
126
+ <span className="text-xs font-medium">{user.username}</span>
127
+ </div>
128
+ <button
129
+ onClick={logout}
130
+ className="p-2 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 text-slate-500 hover:text-red-600 dark:text-slate-400 dark:hover:text-red-400 transition-colors"
131
+ title="Logout"
132
+ >
133
+ <LogOut size={18} />
134
+ </button>
135
+ </div>
136
+ </>
137
+ )}
116
138
  </div>
117
139
  </header>
118
140
  );
@@ -1,9 +1,9 @@
1
1
  import React, { useState } from 'react';
2
- import { Package, Download, RefreshCw, Server } from 'lucide-react';
2
+ import { Package, Download, Import } from 'lucide-react';
3
3
  import { Card, CardHeader, CardTitle, CardContent } from '../ui/Card';
4
4
  import { PluginBrowser } from './PluginBrowser';
5
5
  import { InstalledPlugins } from './InstalledPlugins';
6
- import { ZenMLIntegration } from './ZenMLIntegration';
6
+ import { StackImport } from './StackImport';
7
7
 
8
8
  export function PluginManager() {
9
9
  const [activeTab, setActiveTab] = useState('browser');
@@ -11,7 +11,7 @@ export function PluginManager() {
11
11
  const tabs = [
12
12
  { id: 'browser', label: 'Plugin Browser', icon: Package },
13
13
  { id: 'installed', label: 'Installed', icon: Download },
14
- { id: 'zenml', label: 'ZenML Integration', icon: Server },
14
+ { id: 'import', label: 'Import Stack', icon: Import },
15
15
  ];
16
16
 
17
17
  return (
@@ -53,7 +53,7 @@ export function PluginManager() {
53
53
  <CardContent className="p-6">
54
54
  {activeTab === 'browser' && <PluginBrowser />}
55
55
  {activeTab === 'installed' && <InstalledPlugins />}
56
- {activeTab === 'zenml' && <ZenMLIntegration />}
56
+ {activeTab === 'import' && <StackImport />}
57
57
  </CardContent>
58
58
  </Card>
59
59
  );
@@ -1,11 +1,11 @@
1
1
  import React, { useState } from 'react';
2
- import { Server, ArrowRight, CheckCircle, AlertCircle, Loader2 } from 'lucide-react';
2
+ import { Layers, ArrowRight, CheckCircle, AlertCircle, Loader2, Import } from 'lucide-react';
3
3
  import { Button } from '../ui/Button';
4
- import { Card } from '../ui/Card';
5
4
  import { pluginService } from '../../services/pluginService';
6
5
 
7
- export function ZenMLIntegration() {
6
+ export function StackImport() {
8
7
  const [stackName, setStackName] = useState('');
8
+ const [importType, setImportType] = useState('zenml');
9
9
  const [status, setStatus] = useState('idle'); // idle, importing, success, error
10
10
  const [logs, setLogs] = useState([]);
11
11
 
@@ -13,13 +13,13 @@ export function ZenMLIntegration() {
13
13
  if (!stackName) return;
14
14
 
15
15
  setStatus('importing');
16
- setLogs(['Connecting to ZenML client...', 'Fetching stack details...']);
16
+ setLogs([`Connecting to ${importType === 'zenml' ? 'ZenML' : 'Source'}...`, 'Fetching stack details...']);
17
17
 
18
18
  try {
19
- const result = await pluginService.importZenMLStack(stackName);
19
+ const result = await pluginService.importStack(stackName, importType);
20
20
  setLogs(prev => [...prev, `Found stack '${stackName}' with ${result.components.length} components.`]);
21
21
 
22
- // Artificial delay for UX to show the progress
22
+ // Artificial delay for UX
23
23
  await new Promise(r => setTimeout(r, 800));
24
24
 
25
25
  setLogs(prev => [...prev, 'Generating flowyml configuration...', 'Import successful!']);
@@ -35,20 +35,43 @@ export function ZenMLIntegration() {
35
35
  <div className="space-y-6">
36
36
  <div className="bg-slate-50 dark:bg-slate-800/50 p-4 rounded-xl border border-slate-200 dark:border-slate-700">
37
37
  <div className="flex items-start gap-3">
38
- <Server className="text-primary-500 mt-1" size={20} />
38
+ <Import className="text-primary-500 mt-1" size={20} />
39
39
  <div>
40
- <h3 className="font-medium text-slate-900 dark:text-white">Import ZenML Stack</h3>
40
+ <h3 className="font-medium text-slate-900 dark:text-white">Import External Stack</h3>
41
41
  <p className="text-sm text-slate-500 dark:text-slate-400 mt-1">
42
- Migrate your existing ZenML infrastructure to flowyml. We'll automatically detect your components and generate the necessary configuration.
42
+ Migrate your existing infrastructure to FlowyML. We'll automatically detect your components and generate the necessary configuration.
43
43
  </p>
44
44
  </div>
45
45
  </div>
46
46
  </div>
47
47
 
48
- <div className="flex gap-4 items-end">
49
- <div className="flex-1 space-y-2">
48
+ <div className="grid gap-6 md:grid-cols-2">
49
+ <div className="space-y-2">
50
50
  <label className="text-sm font-medium text-slate-700 dark:text-slate-300">
51
- ZenML Stack Name
51
+ Source Type
52
+ </label>
53
+ <div className="grid grid-cols-2 gap-3">
54
+ <button
55
+ onClick={() => setImportType('zenml')}
56
+ className={`p-3 rounded-lg border text-sm font-medium flex items-center justify-center gap-2 transition-all ${importType === 'zenml'
57
+ ? 'bg-primary-50 dark:bg-primary-900/20 border-primary-500 text-primary-700 dark:text-primary-300'
58
+ : 'bg-white dark:bg-slate-800 border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600'
59
+ }`}
60
+ >
61
+ ZenML
62
+ </button>
63
+ <button
64
+ disabled
65
+ className="p-3 rounded-lg border border-dashed border-slate-200 dark:border-slate-800 text-slate-400 text-sm font-medium flex items-center justify-center gap-2 cursor-not-allowed"
66
+ >
67
+ FlowyML YAML (Coming Soon)
68
+ </button>
69
+ </div>
70
+ </div>
71
+
72
+ <div className="space-y-2">
73
+ <label className="text-sm font-medium text-slate-700 dark:text-slate-300">
74
+ Stack Name
52
75
  </label>
53
76
  <input
54
77
  type="text"
@@ -58,6 +81,9 @@ export function ZenMLIntegration() {
58
81
  className="w-full px-3 py-2 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500"
59
82
  />
60
83
  </div>
84
+ </div>
85
+
86
+ <div className="flex justify-end">
61
87
  <Button
62
88
  onClick={handleImport}
63
89
  disabled={status === 'importing' || !stackName}