flowyml 1.7.2__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.
- flowyml/assets/base.py +15 -0
- flowyml/assets/metrics.py +5 -0
- flowyml/cli/main.py +709 -0
- flowyml/cli/stack_cli.py +138 -25
- flowyml/core/__init__.py +17 -0
- flowyml/core/executor.py +161 -26
- flowyml/core/image_builder.py +129 -0
- flowyml/core/log_streamer.py +227 -0
- flowyml/core/orchestrator.py +22 -2
- flowyml/core/pipeline.py +34 -10
- flowyml/core/routing.py +558 -0
- flowyml/core/step.py +9 -1
- flowyml/core/step_grouping.py +49 -35
- flowyml/core/types.py +407 -0
- flowyml/monitoring/alerts.py +10 -0
- flowyml/monitoring/notifications.py +104 -25
- flowyml/monitoring/slack_blocks.py +323 -0
- flowyml/plugins/__init__.py +251 -0
- flowyml/plugins/alerters/__init__.py +1 -0
- flowyml/plugins/alerters/slack.py +168 -0
- flowyml/plugins/base.py +752 -0
- flowyml/plugins/config.py +478 -0
- flowyml/plugins/deployers/__init__.py +22 -0
- flowyml/plugins/deployers/gcp_cloud_run.py +200 -0
- flowyml/plugins/deployers/sagemaker.py +306 -0
- flowyml/plugins/deployers/vertex.py +290 -0
- flowyml/plugins/integration.py +369 -0
- flowyml/plugins/manager.py +510 -0
- flowyml/plugins/model_registries/__init__.py +22 -0
- flowyml/plugins/model_registries/mlflow.py +159 -0
- flowyml/plugins/model_registries/sagemaker.py +489 -0
- flowyml/plugins/model_registries/vertex.py +386 -0
- flowyml/plugins/orchestrators/__init__.py +13 -0
- flowyml/plugins/orchestrators/sagemaker.py +443 -0
- flowyml/plugins/orchestrators/vertex_ai.py +461 -0
- flowyml/plugins/registries/__init__.py +13 -0
- flowyml/plugins/registries/ecr.py +321 -0
- flowyml/plugins/registries/gcr.py +313 -0
- flowyml/plugins/registry.py +454 -0
- flowyml/plugins/stack.py +494 -0
- flowyml/plugins/stack_config.py +537 -0
- flowyml/plugins/stores/__init__.py +13 -0
- flowyml/plugins/stores/gcs.py +460 -0
- flowyml/plugins/stores/s3.py +453 -0
- flowyml/plugins/trackers/__init__.py +11 -0
- flowyml/plugins/trackers/mlflow.py +316 -0
- flowyml/plugins/validators/__init__.py +3 -0
- flowyml/plugins/validators/deepchecks.py +119 -0
- flowyml/registry/__init__.py +2 -1
- flowyml/registry/model_environment.py +109 -0
- flowyml/registry/model_registry.py +241 -96
- flowyml/serving/__init__.py +17 -0
- flowyml/serving/model_server.py +628 -0
- flowyml/stacks/__init__.py +60 -0
- flowyml/stacks/aws.py +93 -0
- flowyml/stacks/base.py +62 -0
- flowyml/stacks/components.py +12 -0
- flowyml/stacks/gcp.py +44 -9
- flowyml/stacks/plugins.py +115 -0
- flowyml/stacks/registry.py +2 -1
- flowyml/storage/sql.py +401 -12
- flowyml/tracking/experiment.py +8 -5
- flowyml/ui/backend/Dockerfile +87 -16
- flowyml/ui/backend/auth.py +12 -2
- flowyml/ui/backend/main.py +149 -5
- flowyml/ui/backend/routers/ai_context.py +226 -0
- flowyml/ui/backend/routers/assets.py +23 -4
- flowyml/ui/backend/routers/auth.py +96 -0
- flowyml/ui/backend/routers/deployments.py +660 -0
- flowyml/ui/backend/routers/model_explorer.py +597 -0
- flowyml/ui/backend/routers/plugins.py +103 -51
- flowyml/ui/backend/routers/projects.py +91 -8
- flowyml/ui/backend/routers/runs.py +20 -1
- flowyml/ui/backend/routers/schedules.py +22 -17
- flowyml/ui/backend/routers/templates.py +319 -0
- flowyml/ui/backend/routers/websocket.py +2 -2
- flowyml/ui/frontend/Dockerfile +55 -6
- flowyml/ui/frontend/dist/assets/index-B5AsPTSz.css +1 -0
- flowyml/ui/frontend/dist/assets/index-dFbZ8wD8.js +753 -0
- flowyml/ui/frontend/dist/index.html +2 -2
- flowyml/ui/frontend/dist/logo.png +0 -0
- flowyml/ui/frontend/nginx.conf +65 -4
- flowyml/ui/frontend/package-lock.json +1404 -74
- flowyml/ui/frontend/package.json +3 -0
- flowyml/ui/frontend/public/logo.png +0 -0
- flowyml/ui/frontend/src/App.jsx +10 -7
- flowyml/ui/frontend/src/app/auth/Login.jsx +90 -0
- flowyml/ui/frontend/src/app/dashboard/page.jsx +8 -8
- flowyml/ui/frontend/src/app/deployments/page.jsx +786 -0
- flowyml/ui/frontend/src/app/model-explorer/page.jsx +1031 -0
- flowyml/ui/frontend/src/app/pipelines/page.jsx +12 -2
- flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +19 -6
- flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +36 -24
- flowyml/ui/frontend/src/app/runs/page.jsx +8 -2
- flowyml/ui/frontend/src/app/settings/page.jsx +267 -253
- flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +29 -7
- flowyml/ui/frontend/src/components/Layout.jsx +6 -0
- flowyml/ui/frontend/src/components/PipelineGraph.jsx +79 -29
- flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +36 -6
- flowyml/ui/frontend/src/components/RunMetaPanel.jsx +113 -0
- flowyml/ui/frontend/src/components/ai/AIAssistantButton.jsx +71 -0
- flowyml/ui/frontend/src/components/ai/AIAssistantPanel.jsx +420 -0
- flowyml/ui/frontend/src/components/header/Header.jsx +22 -0
- flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +4 -4
- flowyml/ui/frontend/src/components/plugins/{ZenMLIntegration.jsx → StackImport.jsx} +38 -12
- flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +36 -13
- flowyml/ui/frontend/src/contexts/AIAssistantContext.jsx +245 -0
- flowyml/ui/frontend/src/contexts/AuthContext.jsx +108 -0
- flowyml/ui/frontend/src/hooks/useAIContext.js +156 -0
- flowyml/ui/frontend/src/hooks/useWebGPU.js +54 -0
- flowyml/ui/frontend/src/layouts/MainLayout.jsx +6 -0
- flowyml/ui/frontend/src/router/index.jsx +47 -20
- flowyml/ui/frontend/src/services/pluginService.js +3 -1
- flowyml/ui/server_manager.py +5 -5
- flowyml/ui/utils.py +157 -39
- flowyml/utils/config.py +37 -15
- flowyml/utils/model_introspection.py +123 -0
- flowyml/utils/observability.py +30 -0
- flowyml-1.8.0.dist-info/METADATA +174 -0
- {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/RECORD +123 -65
- {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/WHEEL +1 -1
- flowyml/ui/frontend/dist/assets/index-B40RsQDq.css +0 -1
- flowyml/ui/frontend/dist/assets/index-CjI0zKCn.js +0 -685
- flowyml-1.7.2.dist-info/METADATA +0 -477
- {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/entry_points.txt +0 -0
- {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -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,
|
|
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 {
|
|
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: '
|
|
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 === '
|
|
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 {
|
|
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
|
|
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([
|
|
16
|
+
setLogs([`Connecting to ${importType === 'zenml' ? 'ZenML' : 'Source'}...`, 'Fetching stack details...']);
|
|
17
17
|
|
|
18
18
|
try {
|
|
19
|
-
const result = await pluginService.
|
|
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
|
|
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
|
-
<
|
|
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
|
|
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
|
|
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="
|
|
49
|
-
<div className="
|
|
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
|
-
|
|
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}
|