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.
Files changed (126) hide show
  1. flowyml/assets/base.py +15 -0
  2. flowyml/assets/metrics.py +5 -0
  3. flowyml/cli/main.py +709 -0
  4. flowyml/cli/stack_cli.py +138 -25
  5. flowyml/core/__init__.py +17 -0
  6. flowyml/core/executor.py +161 -26
  7. flowyml/core/image_builder.py +129 -0
  8. flowyml/core/log_streamer.py +227 -0
  9. flowyml/core/orchestrator.py +22 -2
  10. flowyml/core/pipeline.py +34 -10
  11. flowyml/core/routing.py +558 -0
  12. flowyml/core/step.py +9 -1
  13. flowyml/core/step_grouping.py +49 -35
  14. flowyml/core/types.py +407 -0
  15. flowyml/monitoring/alerts.py +10 -0
  16. flowyml/monitoring/notifications.py +104 -25
  17. flowyml/monitoring/slack_blocks.py +323 -0
  18. flowyml/plugins/__init__.py +251 -0
  19. flowyml/plugins/alerters/__init__.py +1 -0
  20. flowyml/plugins/alerters/slack.py +168 -0
  21. flowyml/plugins/base.py +752 -0
  22. flowyml/plugins/config.py +478 -0
  23. flowyml/plugins/deployers/__init__.py +22 -0
  24. flowyml/plugins/deployers/gcp_cloud_run.py +200 -0
  25. flowyml/plugins/deployers/sagemaker.py +306 -0
  26. flowyml/plugins/deployers/vertex.py +290 -0
  27. flowyml/plugins/integration.py +369 -0
  28. flowyml/plugins/manager.py +510 -0
  29. flowyml/plugins/model_registries/__init__.py +22 -0
  30. flowyml/plugins/model_registries/mlflow.py +159 -0
  31. flowyml/plugins/model_registries/sagemaker.py +489 -0
  32. flowyml/plugins/model_registries/vertex.py +386 -0
  33. flowyml/plugins/orchestrators/__init__.py +13 -0
  34. flowyml/plugins/orchestrators/sagemaker.py +443 -0
  35. flowyml/plugins/orchestrators/vertex_ai.py +461 -0
  36. flowyml/plugins/registries/__init__.py +13 -0
  37. flowyml/plugins/registries/ecr.py +321 -0
  38. flowyml/plugins/registries/gcr.py +313 -0
  39. flowyml/plugins/registry.py +454 -0
  40. flowyml/plugins/stack.py +494 -0
  41. flowyml/plugins/stack_config.py +537 -0
  42. flowyml/plugins/stores/__init__.py +13 -0
  43. flowyml/plugins/stores/gcs.py +460 -0
  44. flowyml/plugins/stores/s3.py +453 -0
  45. flowyml/plugins/trackers/__init__.py +11 -0
  46. flowyml/plugins/trackers/mlflow.py +316 -0
  47. flowyml/plugins/validators/__init__.py +3 -0
  48. flowyml/plugins/validators/deepchecks.py +119 -0
  49. flowyml/registry/__init__.py +2 -1
  50. flowyml/registry/model_environment.py +109 -0
  51. flowyml/registry/model_registry.py +241 -96
  52. flowyml/serving/__init__.py +17 -0
  53. flowyml/serving/model_server.py +628 -0
  54. flowyml/stacks/__init__.py +60 -0
  55. flowyml/stacks/aws.py +93 -0
  56. flowyml/stacks/base.py +62 -0
  57. flowyml/stacks/components.py +12 -0
  58. flowyml/stacks/gcp.py +44 -9
  59. flowyml/stacks/plugins.py +115 -0
  60. flowyml/stacks/registry.py +2 -1
  61. flowyml/storage/sql.py +401 -12
  62. flowyml/tracking/experiment.py +8 -5
  63. flowyml/ui/backend/Dockerfile +87 -16
  64. flowyml/ui/backend/auth.py +12 -2
  65. flowyml/ui/backend/main.py +149 -5
  66. flowyml/ui/backend/routers/ai_context.py +226 -0
  67. flowyml/ui/backend/routers/assets.py +23 -4
  68. flowyml/ui/backend/routers/auth.py +96 -0
  69. flowyml/ui/backend/routers/deployments.py +660 -0
  70. flowyml/ui/backend/routers/model_explorer.py +597 -0
  71. flowyml/ui/backend/routers/plugins.py +103 -51
  72. flowyml/ui/backend/routers/projects.py +91 -8
  73. flowyml/ui/backend/routers/runs.py +20 -1
  74. flowyml/ui/backend/routers/schedules.py +22 -17
  75. flowyml/ui/backend/routers/templates.py +319 -0
  76. flowyml/ui/backend/routers/websocket.py +2 -2
  77. flowyml/ui/frontend/Dockerfile +55 -6
  78. flowyml/ui/frontend/dist/assets/index-B5AsPTSz.css +1 -0
  79. flowyml/ui/frontend/dist/assets/index-dFbZ8wD8.js +753 -0
  80. flowyml/ui/frontend/dist/index.html +2 -2
  81. flowyml/ui/frontend/dist/logo.png +0 -0
  82. flowyml/ui/frontend/nginx.conf +65 -4
  83. flowyml/ui/frontend/package-lock.json +1404 -74
  84. flowyml/ui/frontend/package.json +3 -0
  85. flowyml/ui/frontend/public/logo.png +0 -0
  86. flowyml/ui/frontend/src/App.jsx +10 -7
  87. flowyml/ui/frontend/src/app/auth/Login.jsx +90 -0
  88. flowyml/ui/frontend/src/app/dashboard/page.jsx +8 -8
  89. flowyml/ui/frontend/src/app/deployments/page.jsx +786 -0
  90. flowyml/ui/frontend/src/app/model-explorer/page.jsx +1031 -0
  91. flowyml/ui/frontend/src/app/pipelines/page.jsx +12 -2
  92. flowyml/ui/frontend/src/app/projects/[projectId]/_components/ProjectExperimentsList.jsx +19 -6
  93. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +36 -24
  94. flowyml/ui/frontend/src/app/runs/page.jsx +8 -2
  95. flowyml/ui/frontend/src/app/settings/page.jsx +267 -253
  96. flowyml/ui/frontend/src/components/AssetDetailsPanel.jsx +29 -7
  97. flowyml/ui/frontend/src/components/Layout.jsx +6 -0
  98. flowyml/ui/frontend/src/components/PipelineGraph.jsx +79 -29
  99. flowyml/ui/frontend/src/components/RunDetailsPanel.jsx +36 -6
  100. flowyml/ui/frontend/src/components/RunMetaPanel.jsx +113 -0
  101. flowyml/ui/frontend/src/components/ai/AIAssistantButton.jsx +71 -0
  102. flowyml/ui/frontend/src/components/ai/AIAssistantPanel.jsx +420 -0
  103. flowyml/ui/frontend/src/components/header/Header.jsx +22 -0
  104. flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +4 -4
  105. flowyml/ui/frontend/src/components/plugins/{ZenMLIntegration.jsx → StackImport.jsx} +38 -12
  106. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +36 -13
  107. flowyml/ui/frontend/src/contexts/AIAssistantContext.jsx +245 -0
  108. flowyml/ui/frontend/src/contexts/AuthContext.jsx +108 -0
  109. flowyml/ui/frontend/src/hooks/useAIContext.js +156 -0
  110. flowyml/ui/frontend/src/hooks/useWebGPU.js +54 -0
  111. flowyml/ui/frontend/src/layouts/MainLayout.jsx +6 -0
  112. flowyml/ui/frontend/src/router/index.jsx +47 -20
  113. flowyml/ui/frontend/src/services/pluginService.js +3 -1
  114. flowyml/ui/server_manager.py +5 -5
  115. flowyml/ui/utils.py +157 -39
  116. flowyml/utils/config.py +37 -15
  117. flowyml/utils/model_introspection.py +123 -0
  118. flowyml/utils/observability.py +30 -0
  119. flowyml-1.8.0.dist-info/METADATA +174 -0
  120. {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/RECORD +123 -65
  121. {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/WHEEL +1 -1
  122. flowyml/ui/frontend/dist/assets/index-B40RsQDq.css +0 -1
  123. flowyml/ui/frontend/dist/assets/index-CjI0zKCn.js +0 -685
  124. flowyml-1.7.2.dist-info/METADATA +0 -477
  125. {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/entry_points.txt +0 -0
  126. {flowyml-1.7.2.dist-info → flowyml-1.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -24,14 +24,15 @@ import {
24
24
  Hash,
25
25
  Gauge,
26
26
  LineChart,
27
- Eye
27
+ Eye,
28
+ Rocket
28
29
  } from 'lucide-react';
29
30
  import { Card } from './ui/Card';
30
31
  import { Badge } from './ui/Badge';
31
32
  import { Button } from './ui/Button';
32
33
  import { format } from 'date-fns';
33
34
  import { downloadArtifactById } from '../utils/downloads';
34
- import { Link } from 'react-router-dom';
35
+ import { Link, useNavigate } from 'react-router-dom';
35
36
  import { motion } from 'framer-motion';
36
37
  import { ProjectSelector } from './ProjectSelector';
37
38
  import { fetchApi } from '../utils/api';
@@ -41,9 +42,22 @@ import { TrainingHistoryChart } from './TrainingHistoryChart';
41
42
  export function AssetDetailsPanel({ asset, onClose, hideHeader = false }) {
42
43
  const [activeTab, setActiveTab] = useState('overview');
43
44
  const [currentProject, setCurrentProject] = useState(asset?.project);
45
+ const navigate = useNavigate();
44
46
 
45
47
  if (!asset) return null;
46
48
 
49
+ // Check if this asset can be deployed (model-like)
50
+ const isDeployable = asset.type?.toLowerCase().includes('model') ||
51
+ asset.name?.toLowerCase().includes('model') ||
52
+ ['keras', 'pytorch', 'sklearn', 'tensorflow', 'xgboost', 'lightgbm', 'onnx']
53
+ .some(kw => (asset.type || '').toLowerCase().includes(kw) ||
54
+ (asset.name || '').toLowerCase().includes(kw));
55
+
56
+ const handleDeploy = async () => {
57
+ // Navigate to deployments page with pre-selected model
58
+ navigate(`/deployments?deploy=${encodeURIComponent(asset.artifact_id)}&name=${encodeURIComponent(asset.name)}`);
59
+ };
60
+
47
61
  const handleProjectUpdate = async (newProject) => {
48
62
  try {
49
63
  await fetchApi(`/api/assets/${asset.artifact_id}/project`, {
@@ -334,6 +348,15 @@ export function AssetDetailsPanel({ asset, onClose, hideHeader = false }) {
334
348
  <Download size={16} className="mr-2" />
335
349
  Download Asset
336
350
  </Button>
351
+ {isDeployable && (
352
+ <Button
353
+ onClick={handleDeploy}
354
+ className="bg-gradient-to-r from-orange-500 to-red-500 hover:from-orange-600 hover:to-red-600 text-white"
355
+ >
356
+ <Rocket size={16} className="mr-2" />
357
+ Deploy
358
+ </Button>
359
+ )}
337
360
  {asset.run_id && (
338
361
  <Link to={`/runs/${asset.run_id}`}>
339
362
  <Button variant="outline">
@@ -624,11 +647,10 @@ function DatasetOverview({ properties }) {
624
647
  {(Array.isArray(columns) ? columns : []).slice(0, 10).map((col, i) => (
625
648
  <span
626
649
  key={i}
627
- className={`text-[10px] px-2 py-0.5 rounded ${
628
- col === labelColumn
629
- ? 'bg-emerald-100 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400 font-medium'
630
- : 'bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-slate-300'
631
- }`}
650
+ className={`text-[10px] px-2 py-0.5 rounded ${col === labelColumn
651
+ ? 'bg-emerald-100 dark:bg-emerald-900/30 text-emerald-600 dark:text-emerald-400 font-medium'
652
+ : 'bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-slate-300'
653
+ }`}
632
654
  >
633
655
  {col}{col === labelColumn && ' (target)'}
634
656
  </span>
@@ -3,6 +3,8 @@ import { Link, useLocation } from 'react-router-dom';
3
3
  import { LayoutDashboard, PlayCircle, FolderKanban, FlaskConical, Database, Settings, Trophy, Calendar, MessageSquare, Moon, Sun, Key, Package } from 'lucide-react';
4
4
  import { useTheme } from '../contexts/ThemeContext';
5
5
  import { ProjectSelector } from './ui/ProjectSelector';
6
+ import { AIAssistantButton } from './ai/AIAssistantButton';
7
+ import { AIAssistantPanel } from './ai/AIAssistantPanel';
6
8
 
7
9
  function NavLink({ to, icon, label }) {
8
10
  const location = useLocation();
@@ -103,6 +105,10 @@ export function Layout({ children }) {
103
105
  {children}
104
106
  </div>
105
107
  </main>
108
+
109
+ {/* AI Assistant */}
110
+ <AIAssistantButton />
111
+ <AIAssistantPanel />
106
112
  </div>
107
113
  );
108
114
  }
@@ -10,7 +10,7 @@ import ReactFlow, {
10
10
  Position
11
11
  } from 'reactflow';
12
12
  import 'reactflow/dist/style.css';
13
- import { CheckCircle, XCircle, Clock, Loader, Database, Box, BarChart2, FileText, Layers } from 'lucide-react';
13
+ import { CheckCircle, XCircle, Clock, Loader, Database, Box, BarChart2, FileText, Layers, GitFork, User } from 'lucide-react';
14
14
  import dagre from 'dagre';
15
15
 
16
16
  const stepNodeWidth = 240;
@@ -115,6 +115,11 @@ export function PipelineGraph({ dag, steps, selectedStep, onStepSelect, onArtifa
115
115
  const executionGroup = stepData.execution_group;
116
116
  const groupColor = executionGroup ? groupColors[executionGroup] : null;
117
117
 
118
+ // Detect special step types
119
+ const isConditional = node.name.toLowerCase().startsWith('if') || node.name.includes('condition');
120
+ const isHumanInLoop = node.name.toLowerCase().includes('approve') || node.name.includes('review') || node.name.includes('human');
121
+
122
+
118
123
  nodes.push({
119
124
  id: node.id,
120
125
  type: 'step',
@@ -125,7 +130,11 @@ export function PipelineGraph({ dag, steps, selectedStep, onStepSelect, onArtifa
125
130
  cached: stepData.cached,
126
131
  selected: selectedStep === node.id,
127
132
  execution_group: executionGroup,
128
- groupColor: groupColor
133
+ groupColor: groupColor,
134
+ isConditional: isConditional,
135
+ isHumanInLoop: isHumanInLoop,
136
+ inputs: node.inputs || [],
137
+ outputs: node.outputs || []
129
138
  }
130
139
  });
131
140
 
@@ -250,7 +259,7 @@ function CustomStepNode({ data }) {
250
259
  bg: 'bg-white dark:bg-slate-800',
251
260
  border: 'border-amber-500 dark:border-amber-500',
252
261
  ring: 'ring-amber-200 dark:ring-amber-900',
253
- shadow: 'shadow-amber-100 dark:shadow-none'
262
+ shadow: 'shadow-amber-100 dark:shadow-none animate-pulse ring-2 ring-amber-400/50'
254
263
  },
255
264
  pending: {
256
265
  icon: <Clock size={18} />,
@@ -266,6 +275,39 @@ function CustomStepNode({ data }) {
266
275
  const groupColor = data.groupColor;
267
276
  const hasGroup = data.execution_group && groupColor;
268
277
 
278
+ // Special styling for conditional nodes
279
+ if (data.isConditional) {
280
+ return (
281
+ <div
282
+ className={`
283
+ relative px-4 py-3 rounded-xl border-2 transition-all duration-200 flex flex-col justify-center items-center text-center
284
+ ${config.bg} border-violet-400 dark:border-violet-500
285
+ ${data.selected ? 'ring-4 ring-violet-200 dark:ring-violet-900 shadow-lg scale-105' : 'hover:shadow-md'}
286
+ transform rotate-0
287
+ `}
288
+ style={{ width: 180, height: 100 }} // Slightly different size for conditional
289
+ >
290
+ <Handle type="target" position={Position.Top} className="!bg-violet-400 !w-3 !h-3" />
291
+
292
+ <div className="bg-violet-100 dark:bg-violet-900/50 p-2 rounded-full mb-2">
293
+ <GitFork size={20} className="text-violet-600 dark:text-violet-300" />
294
+ </div>
295
+ <div className="font-bold text-xs text-violet-900 dark:text-violet-100 uppercase tracking-wider mb-1">Decision</div>
296
+ <div className="text-xs font-semibold text-slate-700 dark:text-slate-300 line-clamp-2 leading-tight">
297
+ {data.label}
298
+ </div>
299
+
300
+ {data.status !== 'pending' && (
301
+ <div className={`absolute -right-2 -top-2 rounded-full p-1 border-2 border-white dark:border-slate-900 ${config.bg}`}>
302
+ <div className={config.color}>{config.icon}</div>
303
+ </div>
304
+ )}
305
+
306
+ <Handle type="source" position={Position.Bottom} className="!bg-violet-400 !w-3 !h-3" />
307
+ </div>
308
+ );
309
+ }
310
+
269
311
  return (
270
312
  <div
271
313
  className={`
@@ -273,6 +315,7 @@ function CustomStepNode({ data }) {
273
315
  ${hasGroup ? groupColor.bg : config.bg}
274
316
  ${hasGroup ? groupColor.border : config.border}
275
317
  ${data.selected ? `ring-4 ${config.ring} shadow-lg` : `hover:shadow-md ${config.shadow}`}
318
+ ${data.isHumanInLoop ? 'border-dashed border-amber-400 dark:border-amber-500' : ''}
276
319
  `}
277
320
  style={{ width: stepNodeWidth, height: stepNodeHeight }}
278
321
  >
@@ -280,8 +323,13 @@ function CustomStepNode({ data }) {
280
323
 
281
324
  <div className="flex flex-col h-full justify-between">
282
325
  <div className="flex items-start gap-3">
283
- <div className={`p-1.5 rounded-md bg-slate-50 border border-slate-100 ${config.color}`}>
326
+ <div className={`p-1.5 rounded-md bg-slate-50 border border-slate-100 ${config.color} relative`}>
284
327
  {config.icon}
328
+ {data.isHumanInLoop && (
329
+ <div className="absolute -bottom-1 -right-1 bg-amber-100 text-amber-600 rounded-full p-0.5 border border-white" title="Human in the loop">
330
+ <User size={10} />
331
+ </div>
332
+ )}
285
333
  </div>
286
334
  <div className="min-w-0 flex-1">
287
335
  <h3 className={`font-bold text-sm truncate ${hasGroup ? groupColor.text : 'text-slate-900 dark:text-white'}`} title={data.label}>
@@ -298,18 +346,19 @@ function CustomStepNode({ data }) {
298
346
  </div>
299
347
  </div>
300
348
 
301
- {data.duration !== undefined && (
302
- <div className="flex items-center justify-between pt-2 border-t border-slate-100 mt-1">
303
- <span className="text-xs text-slate-400 font-mono">
304
- {data.duration.toFixed(2)}s
349
+ <div className="flex items-center justify-between pt-2 border-t border-slate-100 mt-1">
350
+ {data.duration !== undefined ? (
351
+ <span className="text-xs text-slate-400 font-mono flex items-center gap-1">
352
+ <Clock size={10} /> {data.duration.toFixed(2)}s
305
353
  </span>
306
- {data.cached && (
307
- <span className="text-[10px] font-bold text-blue-600 bg-blue-50 px-1.5 py-0.5 rounded uppercase tracking-wider">
308
- Cached
309
- </span>
310
- )}
311
- </div>
312
- )}
354
+ ) : <span className="text-xs text-slate-300">-</span>}
355
+
356
+ {data.cached && (
357
+ <span className="flex items-center gap-1 text-[10px] font-bold text-emerald-600 bg-emerald-50 border border-emerald-100 px-1.5 py-0.5 rounded-full uppercase tracking-wider shadow-sm">
358
+ <Database size={8} /> Cached
359
+ </span>
360
+ )}
361
+ </div>
313
362
  </div>
314
363
 
315
364
  <Handle type="source" position={Position.Bottom} className="!bg-slate-400 !w-2 !h-2" />
@@ -318,6 +367,7 @@ function CustomStepNode({ data }) {
318
367
  }
319
368
 
320
369
 
370
+
321
371
  function CustomArtifactNode({ data }) {
322
372
  // Determine icon and styling based on artifact type (inferred from label)
323
373
  const getArtifactStyle = (label) => {
@@ -326,8 +376,8 @@ function CustomArtifactNode({ data }) {
326
376
  if (lowerLabel.includes('model') || lowerLabel.includes('weights')) {
327
377
  return {
328
378
  icon: Box,
329
- bgColor: 'bg-purple-100 dark:bg-purple-900/30',
330
- borderColor: 'border-purple-400 dark:border-purple-600',
379
+ bgColor: 'bg-purple-100 dark:bg-purple-900/40',
380
+ borderColor: 'border-purple-300 dark:border-purple-600',
331
381
  iconColor: 'text-purple-600 dark:text-purple-400',
332
382
  textColor: 'text-purple-900 dark:text-purple-100'
333
383
  };
@@ -335,8 +385,8 @@ function CustomArtifactNode({ data }) {
335
385
  if (lowerLabel.includes('feature') || lowerLabel.includes('train_set') || lowerLabel.includes('test_set')) {
336
386
  return {
337
387
  icon: Layers,
338
- bgColor: 'bg-emerald-100 dark:bg-emerald-900/30',
339
- borderColor: 'border-emerald-400 dark:border-emerald-600',
388
+ bgColor: 'bg-emerald-100 dark:bg-emerald-900/40',
389
+ borderColor: 'border-emerald-300 dark:border-emerald-600',
340
390
  iconColor: 'text-emerald-600 dark:text-emerald-400',
341
391
  textColor: 'text-emerald-900 dark:text-emerald-100'
342
392
  };
@@ -344,8 +394,8 @@ function CustomArtifactNode({ data }) {
344
394
  if (lowerLabel.includes('data') || lowerLabel.includes('batch') || lowerLabel.includes('set')) {
345
395
  return {
346
396
  icon: Database,
347
- bgColor: 'bg-blue-100 dark:bg-blue-900/30',
348
- borderColor: 'border-blue-400 dark:border-blue-600',
397
+ bgColor: 'bg-blue-100 dark:bg-blue-900/40',
398
+ borderColor: 'border-blue-300 dark:border-blue-600',
349
399
  iconColor: 'text-blue-600 dark:text-blue-400',
350
400
  textColor: 'text-blue-900 dark:text-blue-100'
351
401
  };
@@ -353,8 +403,8 @@ function CustomArtifactNode({ data }) {
353
403
  if (lowerLabel.includes('metrics') || lowerLabel.includes('report') || lowerLabel.includes('status')) {
354
404
  return {
355
405
  icon: BarChart2,
356
- bgColor: 'bg-orange-100 dark:bg-orange-900/30',
357
- borderColor: 'border-orange-400 dark:border-orange-600',
406
+ bgColor: 'bg-orange-100 dark:bg-orange-900/40',
407
+ borderColor: 'border-orange-300 dark:border-orange-600',
358
408
  iconColor: 'text-orange-600 dark:text-orange-400',
359
409
  textColor: 'text-orange-900 dark:text-orange-100'
360
410
  };
@@ -362,8 +412,8 @@ function CustomArtifactNode({ data }) {
362
412
  if (lowerLabel.includes('image') || lowerLabel.includes('docker')) {
363
413
  return {
364
414
  icon: Box,
365
- bgColor: 'bg-cyan-100 dark:bg-cyan-900/30',
366
- borderColor: 'border-cyan-400 dark:border-cyan-600',
415
+ bgColor: 'bg-cyan-100 dark:bg-cyan-900/40',
416
+ borderColor: 'border-cyan-300 dark:border-cyan-600',
367
417
  iconColor: 'text-cyan-600 dark:text-cyan-400',
368
418
  textColor: 'text-cyan-900 dark:text-cyan-100'
369
419
  };
@@ -384,13 +434,13 @@ function CustomArtifactNode({ data }) {
384
434
 
385
435
  return (
386
436
  <div
387
- className={`px-3 py-2 rounded-lg ${style.bgColor} border-2 ${style.borderColor} flex items-center justify-center gap-2 shadow-md hover:shadow-lg transition-all min-w-[140px] cursor-pointer`}
388
- style={{ height: artifactNodeHeight }}
437
+ className={`px-4 py-1.5 rounded-full ${style.bgColor} border ${style.borderColor} flex items-center justify-center gap-2 shadow-sm hover:shadow-md transition-all min-w-[120px] cursor-pointer`}
438
+ style={{ height: 36 }}
389
439
  >
390
440
  <Handle type="target" position={Position.Top} className="!bg-slate-400 !w-2 !h-2" />
391
441
 
392
- <Icon size={14} className={style.iconColor} />
393
- <span className={`text-xs font-semibold ${style.textColor} truncate max-w-[100px]`} title={data.label}>
442
+ <Icon size={12} className={style.iconColor} />
443
+ <span className={`text-[11px] font-bold ${style.textColor} truncate max-w-[120px] uppercase tracking-wide`} title={data.label}>
394
444
  {data.label}
395
445
  </span>
396
446
 
@@ -232,11 +232,41 @@ export function RunDetailsPanel({ run, onClose }) {
232
232
  {step.duration ? `${step.duration.toFixed(2)}s` : '-'}
233
233
  </span>
234
234
  </div>
235
- {step.error && (
236
- <div className="text-xs text-rose-600 bg-rose-50 dark:bg-rose-900/20 p-2 rounded mt-2 font-mono">
237
- {step.error}
238
- </div>
239
- )}
235
+ {
236
+ step.source_file && (
237
+ <div className="flex justify-end mb-2">
238
+ <a
239
+ href={`vscode://file/${step.source_file}:${step.source_line || 1}`}
240
+ className="text-xs flex items-center gap-1 text-slate-500 hover:text-blue-600 transition-colors"
241
+ title="Open in VS Code"
242
+ >
243
+ <Terminal size={12} />
244
+ <span className="font-mono">{step.source_file.split('/').pop()}:{step.source_line}</span>
245
+ </a>
246
+ </div>
247
+ )
248
+ }
249
+ {
250
+ step.metrics && (
251
+ <div className="flex gap-4 mb-2 justify-end text-xs text-slate-500 font-mono">
252
+ <div className="flex items-center gap-1.5" title="CPU Usage">
253
+ <Cpu size={12} className="text-slate-400" />
254
+ <span>{step.metrics.cpu_percent?.toFixed(1) || 0}%</span>
255
+ </div>
256
+ <div className="flex items-center gap-1.5" title="Memory Usage">
257
+ <Activity size={12} className="text-slate-400" />
258
+ <span>{step.metrics.memory_mb?.toFixed(0) || 0} MB</span>
259
+ </div>
260
+ </div>
261
+ )
262
+ }
263
+ {
264
+ step.error && (
265
+ <div className="text-xs text-rose-600 bg-rose-50 dark:bg-rose-900/20 p-2 rounded mt-2 font-mono">
266
+ {step.error}
267
+ </div>
268
+ )
269
+ }
240
270
  </div>
241
271
  ))}
242
272
  {(!runData.steps || Object.keys(runData.steps).length === 0) && (
@@ -280,7 +310,7 @@ export function RunDetailsPanel({ run, onClose }) {
280
310
  )}
281
311
  </div>
282
312
  </div>
283
- </div>
313
+ </div >
284
314
  );
285
315
  }
286
316
 
@@ -0,0 +1,113 @@
1
+ import React from 'react';
2
+ import { Card } from './ui/Card';
3
+ import { Badge } from './ui/Badge';
4
+ import { Cpu, Box, Calendar, Clock, Container, Database, Tag } from 'lucide-react';
5
+ import { motion } from 'framer-motion';
6
+
7
+ export function RunMetaPanel({ run }) {
8
+ if (!run) return null;
9
+
10
+ // Simulate getting resource info from run metadata if available, else placeholders
11
+ const resources = run.metadata?.resources || {
12
+ cpu: "Standard (2 vCPU)",
13
+ memory: "8 GiB",
14
+ gpu: run.metadata?.resources?.gpu ? `${run.metadata.resources.gpu_count}x ${run.metadata.resources.gpu}` : null
15
+ };
16
+
17
+ const dockerInfo = run.metadata?.docker || {
18
+ image: "flowyml/base:latest",
19
+ registry: "ghcr.io",
20
+ requirements: ["tensorflow", "scikit-learn"]
21
+ };
22
+
23
+ const scheduleInfo = run.trigger ? {
24
+ type: run.trigger.type,
25
+ cron: run.trigger.cron,
26
+ next_run: run.trigger.next_run
27
+ } : null;
28
+
29
+ return (
30
+ <Card className="p-4 bg-white/50 dark:bg-slate-800/50 backdrop-blur-sm border border-slate-200 dark:border-slate-700/50 shadow-sm">
31
+ <h4 className="text-xs font-bold text-slate-400 uppercase tracking-wider mb-4 flex items-center gap-2">
32
+ <Tag size={12} /> Run Environment
33
+ </h4>
34
+
35
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
36
+ {/* Resources */}
37
+ <div className="space-y-3">
38
+ <div className="flex items-center gap-2 text-sm font-semibold text-slate-700 dark:text-slate-300">
39
+ <Cpu size={16} className="text-blue-500" />
40
+ Resources
41
+ </div>
42
+ <div className="space-y-2">
43
+ <div className="flex items-center justify-between text-xs">
44
+ <span className="text-slate-500">Compute</span>
45
+ <span className="font-mono text-slate-700 dark:text-slate-200">{resources.cpu}</span>
46
+ </div>
47
+ <div className="flex items-center justify-between text-xs">
48
+ <span className="text-slate-500">Memory</span>
49
+ <span className="font-mono text-slate-700 dark:text-slate-200">{resources.memory}</span>
50
+ </div>
51
+ {resources.gpu && (
52
+ <div className="flex items-center justify-between text-xs">
53
+ <span className="text-slate-500">GPU</span>
54
+ <Badge variant="secondary" className="text-[10px] bg-purple-50 text-purple-700 border-purple-200">
55
+ {resources.gpu}
56
+ </Badge>
57
+ </div>
58
+ )}
59
+ </div>
60
+ </div>
61
+
62
+ {/* Docker / Environment */}
63
+ <div className="space-y-3">
64
+ <div className="flex items-center gap-2 text-sm font-semibold text-slate-700 dark:text-slate-300">
65
+ <Container size={16} className="text-cyan-500" />
66
+ Environment
67
+ </div>
68
+ <div className="space-y-2">
69
+ <div className="flex flex-col gap-1">
70
+ <span className="text-xs text-slate-500">Base Image</span>
71
+ <code className="text-[10px] bg-slate-100 dark:bg-slate-700 px-1.5 py-1 rounded text-slate-600 dark:text-slate-300 truncate">
72
+ {dockerInfo.image}
73
+ </code>
74
+ </div>
75
+ <div className="flex items-center gap-2">
76
+ <span className="text-xs text-slate-500 w-16">Registry</span>
77
+ <span className="text-xs font-medium text-slate-700 dark:text-slate-300 truncate">{dockerInfo.registry}</span>
78
+ </div>
79
+ </div>
80
+ </div>
81
+
82
+ {/* Schedule / Trigger */}
83
+ <div className="space-y-3">
84
+ <div className="flex items-center gap-2 text-sm font-semibold text-slate-700 dark:text-slate-300">
85
+ <Calendar size={16} className="text-emerald-500" />
86
+ Trigger Info
87
+ </div>
88
+ {scheduleInfo ? (
89
+ <div className="space-y-2">
90
+ <div className="flex items-center gap-2">
91
+ <Badge variant="outline" className="text-xs uppercase">{scheduleInfo.type}</Badge>
92
+ {scheduleInfo.cron && <code className="text-[10px] bg-slate-100 px-1 py-0.5 rounded">{scheduleInfo.cron}</code>}
93
+ </div>
94
+ {scheduleInfo.next_run && (
95
+ <div className="text-xs text-slate-500 flex items-center gap-1">
96
+ <Clock size={10} />
97
+ Next: {new Date(scheduleInfo.next_run).toLocaleString()}
98
+ </div>
99
+ )}
100
+ </div>
101
+ ) : (
102
+ <div className="flex items-center gap-2 h-full py-2">
103
+ <div className="p-1.5 bg-slate-100 rounded-lg">
104
+ <Box size={14} className="text-slate-400" />
105
+ </div>
106
+ <span className="text-xs text-slate-500 italic">Manual Trigger</span>
107
+ </div>
108
+ )}
109
+ </div>
110
+ </div>
111
+ </Card>
112
+ );
113
+ }
@@ -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;