flowyml 1.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. flowyml/__init__.py +207 -0
  2. flowyml/assets/__init__.py +22 -0
  3. flowyml/assets/artifact.py +40 -0
  4. flowyml/assets/base.py +209 -0
  5. flowyml/assets/dataset.py +100 -0
  6. flowyml/assets/featureset.py +301 -0
  7. flowyml/assets/metrics.py +104 -0
  8. flowyml/assets/model.py +82 -0
  9. flowyml/assets/registry.py +157 -0
  10. flowyml/assets/report.py +315 -0
  11. flowyml/cli/__init__.py +5 -0
  12. flowyml/cli/experiment.py +232 -0
  13. flowyml/cli/init.py +256 -0
  14. flowyml/cli/main.py +327 -0
  15. flowyml/cli/run.py +75 -0
  16. flowyml/cli/stack_cli.py +532 -0
  17. flowyml/cli/ui.py +33 -0
  18. flowyml/core/__init__.py +68 -0
  19. flowyml/core/advanced_cache.py +274 -0
  20. flowyml/core/approval.py +64 -0
  21. flowyml/core/cache.py +203 -0
  22. flowyml/core/checkpoint.py +148 -0
  23. flowyml/core/conditional.py +373 -0
  24. flowyml/core/context.py +155 -0
  25. flowyml/core/error_handling.py +419 -0
  26. flowyml/core/executor.py +354 -0
  27. flowyml/core/graph.py +185 -0
  28. flowyml/core/parallel.py +452 -0
  29. flowyml/core/pipeline.py +764 -0
  30. flowyml/core/project.py +253 -0
  31. flowyml/core/resources.py +424 -0
  32. flowyml/core/scheduler.py +630 -0
  33. flowyml/core/scheduler_config.py +32 -0
  34. flowyml/core/step.py +201 -0
  35. flowyml/core/step_grouping.py +292 -0
  36. flowyml/core/templates.py +226 -0
  37. flowyml/core/versioning.py +217 -0
  38. flowyml/integrations/__init__.py +1 -0
  39. flowyml/integrations/keras.py +134 -0
  40. flowyml/monitoring/__init__.py +1 -0
  41. flowyml/monitoring/alerts.py +57 -0
  42. flowyml/monitoring/data.py +102 -0
  43. flowyml/monitoring/llm.py +160 -0
  44. flowyml/monitoring/monitor.py +57 -0
  45. flowyml/monitoring/notifications.py +246 -0
  46. flowyml/registry/__init__.py +5 -0
  47. flowyml/registry/model_registry.py +491 -0
  48. flowyml/registry/pipeline_registry.py +55 -0
  49. flowyml/stacks/__init__.py +27 -0
  50. flowyml/stacks/base.py +77 -0
  51. flowyml/stacks/bridge.py +288 -0
  52. flowyml/stacks/components.py +155 -0
  53. flowyml/stacks/gcp.py +499 -0
  54. flowyml/stacks/local.py +112 -0
  55. flowyml/stacks/migration.py +97 -0
  56. flowyml/stacks/plugin_config.py +78 -0
  57. flowyml/stacks/plugins.py +401 -0
  58. flowyml/stacks/registry.py +226 -0
  59. flowyml/storage/__init__.py +26 -0
  60. flowyml/storage/artifacts.py +246 -0
  61. flowyml/storage/materializers/__init__.py +20 -0
  62. flowyml/storage/materializers/base.py +133 -0
  63. flowyml/storage/materializers/keras.py +185 -0
  64. flowyml/storage/materializers/numpy.py +94 -0
  65. flowyml/storage/materializers/pandas.py +142 -0
  66. flowyml/storage/materializers/pytorch.py +135 -0
  67. flowyml/storage/materializers/sklearn.py +110 -0
  68. flowyml/storage/materializers/tensorflow.py +152 -0
  69. flowyml/storage/metadata.py +931 -0
  70. flowyml/tracking/__init__.py +1 -0
  71. flowyml/tracking/experiment.py +211 -0
  72. flowyml/tracking/leaderboard.py +191 -0
  73. flowyml/tracking/runs.py +145 -0
  74. flowyml/ui/__init__.py +15 -0
  75. flowyml/ui/backend/Dockerfile +31 -0
  76. flowyml/ui/backend/__init__.py +0 -0
  77. flowyml/ui/backend/auth.py +163 -0
  78. flowyml/ui/backend/main.py +187 -0
  79. flowyml/ui/backend/routers/__init__.py +0 -0
  80. flowyml/ui/backend/routers/assets.py +45 -0
  81. flowyml/ui/backend/routers/execution.py +179 -0
  82. flowyml/ui/backend/routers/experiments.py +49 -0
  83. flowyml/ui/backend/routers/leaderboard.py +118 -0
  84. flowyml/ui/backend/routers/notifications.py +72 -0
  85. flowyml/ui/backend/routers/pipelines.py +110 -0
  86. flowyml/ui/backend/routers/plugins.py +192 -0
  87. flowyml/ui/backend/routers/projects.py +85 -0
  88. flowyml/ui/backend/routers/runs.py +66 -0
  89. flowyml/ui/backend/routers/schedules.py +222 -0
  90. flowyml/ui/backend/routers/traces.py +84 -0
  91. flowyml/ui/frontend/Dockerfile +20 -0
  92. flowyml/ui/frontend/README.md +315 -0
  93. flowyml/ui/frontend/dist/assets/index-DFNQnrUj.js +448 -0
  94. flowyml/ui/frontend/dist/assets/index-pWI271rZ.css +1 -0
  95. flowyml/ui/frontend/dist/index.html +16 -0
  96. flowyml/ui/frontend/index.html +15 -0
  97. flowyml/ui/frontend/nginx.conf +26 -0
  98. flowyml/ui/frontend/package-lock.json +3545 -0
  99. flowyml/ui/frontend/package.json +33 -0
  100. flowyml/ui/frontend/postcss.config.js +6 -0
  101. flowyml/ui/frontend/src/App.jsx +21 -0
  102. flowyml/ui/frontend/src/app/assets/page.jsx +397 -0
  103. flowyml/ui/frontend/src/app/dashboard/page.jsx +295 -0
  104. flowyml/ui/frontend/src/app/experiments/[experimentId]/page.jsx +255 -0
  105. flowyml/ui/frontend/src/app/experiments/page.jsx +360 -0
  106. flowyml/ui/frontend/src/app/leaderboard/page.jsx +133 -0
  107. flowyml/ui/frontend/src/app/pipelines/page.jsx +454 -0
  108. flowyml/ui/frontend/src/app/plugins/page.jsx +48 -0
  109. flowyml/ui/frontend/src/app/projects/page.jsx +292 -0
  110. flowyml/ui/frontend/src/app/runs/[runId]/page.jsx +682 -0
  111. flowyml/ui/frontend/src/app/runs/page.jsx +470 -0
  112. flowyml/ui/frontend/src/app/schedules/page.jsx +585 -0
  113. flowyml/ui/frontend/src/app/settings/page.jsx +314 -0
  114. flowyml/ui/frontend/src/app/tokens/page.jsx +456 -0
  115. flowyml/ui/frontend/src/app/traces/page.jsx +246 -0
  116. flowyml/ui/frontend/src/components/Layout.jsx +108 -0
  117. flowyml/ui/frontend/src/components/PipelineGraph.jsx +295 -0
  118. flowyml/ui/frontend/src/components/header/Header.jsx +72 -0
  119. flowyml/ui/frontend/src/components/plugins/AddPluginDialog.jsx +121 -0
  120. flowyml/ui/frontend/src/components/plugins/InstalledPlugins.jsx +124 -0
  121. flowyml/ui/frontend/src/components/plugins/PluginBrowser.jsx +167 -0
  122. flowyml/ui/frontend/src/components/plugins/PluginManager.jsx +60 -0
  123. flowyml/ui/frontend/src/components/sidebar/Sidebar.jsx +145 -0
  124. flowyml/ui/frontend/src/components/ui/Badge.jsx +26 -0
  125. flowyml/ui/frontend/src/components/ui/Button.jsx +34 -0
  126. flowyml/ui/frontend/src/components/ui/Card.jsx +44 -0
  127. flowyml/ui/frontend/src/components/ui/CodeSnippet.jsx +38 -0
  128. flowyml/ui/frontend/src/components/ui/CollapsibleCard.jsx +53 -0
  129. flowyml/ui/frontend/src/components/ui/DataView.jsx +175 -0
  130. flowyml/ui/frontend/src/components/ui/EmptyState.jsx +49 -0
  131. flowyml/ui/frontend/src/components/ui/ExecutionStatus.jsx +122 -0
  132. flowyml/ui/frontend/src/components/ui/KeyValue.jsx +25 -0
  133. flowyml/ui/frontend/src/components/ui/ProjectSelector.jsx +134 -0
  134. flowyml/ui/frontend/src/contexts/ProjectContext.jsx +79 -0
  135. flowyml/ui/frontend/src/contexts/ThemeContext.jsx +54 -0
  136. flowyml/ui/frontend/src/index.css +11 -0
  137. flowyml/ui/frontend/src/layouts/MainLayout.jsx +23 -0
  138. flowyml/ui/frontend/src/main.jsx +10 -0
  139. flowyml/ui/frontend/src/router/index.jsx +39 -0
  140. flowyml/ui/frontend/src/services/pluginService.js +90 -0
  141. flowyml/ui/frontend/src/utils/api.js +47 -0
  142. flowyml/ui/frontend/src/utils/cn.js +6 -0
  143. flowyml/ui/frontend/tailwind.config.js +31 -0
  144. flowyml/ui/frontend/vite.config.js +21 -0
  145. flowyml/ui/utils.py +77 -0
  146. flowyml/utils/__init__.py +67 -0
  147. flowyml/utils/config.py +308 -0
  148. flowyml/utils/debug.py +240 -0
  149. flowyml/utils/environment.py +346 -0
  150. flowyml/utils/git.py +319 -0
  151. flowyml/utils/logging.py +61 -0
  152. flowyml/utils/performance.py +314 -0
  153. flowyml/utils/stack_config.py +296 -0
  154. flowyml/utils/validation.py +270 -0
  155. flowyml-1.1.0.dist-info/METADATA +372 -0
  156. flowyml-1.1.0.dist-info/RECORD +159 -0
  157. flowyml-1.1.0.dist-info/WHEEL +4 -0
  158. flowyml-1.1.0.dist-info/entry_points.txt +3 -0
  159. flowyml-1.1.0.dist-info/licenses/LICENSE +17 -0
@@ -0,0 +1,33 @@
1
+ {
2
+ "dependencies": {
3
+ "clsx": "^2.1.1",
4
+ "dagre": "^0.8.5",
5
+ "date-fns": "^4.1.0",
6
+ "framer-motion": "^12.23.24",
7
+ "lucide-react": "^0.344.0",
8
+ "react": "^18.2.0",
9
+ "react-dom": "^18.2.0",
10
+ "react-router-dom": "^6.22.0",
11
+ "reactflow": "^11.11.4",
12
+ "recharts": "^2.12.0",
13
+ "tailwind-merge": "^2.6.0"
14
+ },
15
+ "devDependencies": {
16
+ "@types/react": "^18.2.64",
17
+ "@types/react-dom": "^18.2.21",
18
+ "@vitejs/plugin-react": "^4.2.1",
19
+ "autoprefixer": "^10.4.18",
20
+ "postcss": "^8.4.35",
21
+ "tailwindcss": "^3.4.1",
22
+ "vite": "^5.1.6"
23
+ },
24
+ "name": "flowy-ui",
25
+ "private": true,
26
+ "scripts": {
27
+ "build": "vite build",
28
+ "dev": "vite",
29
+ "preview": "vite preview"
30
+ },
31
+ "type": "module",
32
+ "version": "0.1.0"
33
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
@@ -0,0 +1,21 @@
1
+
2
+
3
+ import React from 'react';
4
+ import { RouterProvider } from 'react-router-dom';
5
+ import { ThemeProvider } from './contexts/ThemeContext';
6
+ import { ProjectProvider } from './contexts/ProjectContext';
7
+ import { router } from './router';
8
+
9
+ function App() {
10
+ return (
11
+ <ThemeProvider>
12
+ <ProjectProvider>
13
+ <RouterProvider router={router} />
14
+ </ProjectProvider>
15
+ </ThemeProvider>
16
+ );
17
+ }
18
+
19
+
20
+
21
+ export default App
@@ -0,0 +1,397 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { fetchApi } from '../../utils/api';
3
+ import { Link } from 'react-router-dom';
4
+ import { Database, Box, BarChart2, FileText, Search, Filter, Calendar, Package, Download, Eye, X, ArrowRight } from 'lucide-react';
5
+ import { Card } from '../../components/ui/Card';
6
+ import { Badge } from '../../components/ui/Badge';
7
+ import { Button } from '../../components/ui/Button';
8
+ import { format } from 'date-fns';
9
+ import { motion, AnimatePresence } from 'framer-motion';
10
+ import { DataView } from '../../components/ui/DataView';
11
+ import { useProject } from '../../contexts/ProjectContext';
12
+ import { EmptyState } from '../../components/ui/EmptyState';
13
+ import { KeyValue, KeyValueGrid } from '../../components/ui/KeyValue';
14
+
15
+ export function Assets() {
16
+ const [assets, setAssets] = useState([]);
17
+ const [loading, setLoading] = useState(true);
18
+ const [typeFilter, setTypeFilter] = useState('all');
19
+ const [selectedAsset, setSelectedAsset] = useState(null);
20
+ const { selectedProject } = useProject();
21
+
22
+ useEffect(() => {
23
+ const fetchAssets = async () => {
24
+ setLoading(true);
25
+ try {
26
+ const url = selectedProject
27
+ ? `/api/assets?limit=50&project=${encodeURIComponent(selectedProject)}`
28
+ : '/api/assets?limit=50';
29
+ const res = await fetchApi(url);
30
+ const data = await res.json();
31
+ setAssets(data.assets || []);
32
+ } catch (err) {
33
+ console.error(err);
34
+ } finally {
35
+ setLoading(false);
36
+ }
37
+ };
38
+ fetchAssets();
39
+ }, [selectedProject]);
40
+
41
+ // Get unique types
42
+ const types = ['all', ...new Set(assets.map(a => a.type))];
43
+
44
+ // Filter assets
45
+ const filteredAssets = assets.filter(asset => {
46
+ return typeFilter === 'all' || asset.type === typeFilter;
47
+ });
48
+
49
+ const columns = [
50
+ {
51
+ header: 'Type',
52
+ key: 'type',
53
+ sortable: true,
54
+ render: (asset) => {
55
+ const typeConfig = {
56
+ Dataset: { icon: <Database size={16} />, color: 'text-blue-600', bg: 'bg-blue-50' },
57
+ Model: { icon: <Box size={16} />, color: 'text-purple-600', bg: 'bg-purple-50' },
58
+ Metrics: { icon: <BarChart2 size={16} />, color: 'text-emerald-600', bg: 'bg-emerald-50' },
59
+ default: { icon: <FileText size={16} />, color: 'text-slate-600', bg: 'bg-slate-50' }
60
+ };
61
+ const config = typeConfig[asset.type] || typeConfig.default;
62
+ return (
63
+ <div className={`flex items-center gap-2 px-2 py-1 rounded-md w-fit ${config.bg} ${config.color}`}>
64
+ {config.icon}
65
+ <span className="text-xs font-medium">{asset.type}</span>
66
+ </div>
67
+ );
68
+ }
69
+ },
70
+ {
71
+ header: 'Name',
72
+ key: 'name',
73
+ sortable: true,
74
+ render: (asset) => (
75
+ <span className="font-medium text-slate-900 dark:text-white">{asset.name}</span>
76
+ )
77
+ },
78
+ {
79
+ header: 'Step',
80
+ key: 'step',
81
+ sortable: true,
82
+ render: (asset) => (
83
+ <span className="font-mono text-xs text-slate-500">{asset.step}</span>
84
+ )
85
+ },
86
+ {
87
+ header: 'Pipeline',
88
+ key: 'pipeline',
89
+ sortable: true,
90
+ render: (asset) => (
91
+ <span className="text-sm text-slate-600 dark:text-slate-400">
92
+ {asset.pipeline_name || '-'}
93
+ </span>
94
+ )
95
+ },
96
+ {
97
+ header: 'Project',
98
+ key: 'project',
99
+ sortable: true,
100
+ render: (asset) => (
101
+ <span className="text-sm text-slate-600 dark:text-slate-400">
102
+ {asset.project || '-'}
103
+ </span>
104
+ )
105
+ },
106
+ {
107
+ header: 'Run ID',
108
+ key: 'run_id',
109
+ render: (asset) => asset.run_id ? (
110
+ <Link
111
+ to={`/runs/${asset.run_id}`}
112
+ className="font-mono text-xs bg-slate-100 dark:bg-slate-700 px-2 py-1 rounded text-primary-600 dark:text-primary-400 hover:bg-primary-50 dark:hover:bg-primary-900/20 transition-colors"
113
+ >
114
+ {asset.run_id.substring(0, 8)}...
115
+ </Link>
116
+ ) : (
117
+ <span className="font-mono text-xs text-slate-400">-</span>
118
+ )
119
+ },
120
+ {
121
+ header: 'Created',
122
+ key: 'created_at',
123
+ sortable: true,
124
+ render: (asset) => (
125
+ <div className="flex items-center gap-2 text-slate-500">
126
+ <Calendar size={14} />
127
+ {asset.created_at ? format(new Date(asset.created_at), 'MMM d, HH:mm') : '-'}
128
+ </div>
129
+ )
130
+ },
131
+ {
132
+ header: 'Actions',
133
+ key: 'actions',
134
+ render: (asset) => (
135
+ <button
136
+ onClick={() => setSelectedAsset(asset)}
137
+ className="text-primary-600 hover:text-primary-700 font-medium text-sm flex items-center gap-1"
138
+ >
139
+ View <Eye size={14} />
140
+ </button>
141
+ )
142
+ }
143
+ ];
144
+
145
+ const renderGrid = (asset) => {
146
+ const typeConfig = {
147
+ Dataset: { icon: <Database size={20} />, color: 'blue' },
148
+ Model: { icon: <Box size={20} />, color: 'purple' },
149
+ Metrics: { icon: <BarChart2 size={20} />, color: 'emerald' },
150
+ default: { icon: <FileText size={20} />, color: 'slate' }
151
+ };
152
+
153
+ const config = typeConfig[asset.type] || typeConfig.default;
154
+
155
+ const colorClasses = {
156
+ blue: 'from-blue-500 to-cyan-500',
157
+ purple: 'from-purple-500 to-pink-500',
158
+ emerald: 'from-emerald-500 to-teal-500',
159
+ slate: 'from-slate-500 to-slate-600'
160
+ };
161
+
162
+ return (
163
+ <Card
164
+ className="group cursor-pointer hover:shadow-xl hover:border-primary-300 transition-all duration-200 overflow-hidden h-full"
165
+ onClick={() => setSelectedAsset(asset)}
166
+ >
167
+ {/* Icon Header */}
168
+ <div className="flex items-start justify-between mb-3">
169
+ <div className={`p-3 rounded-xl bg-gradient-to-br ${colorClasses[config.color]} text-white shadow-lg group-hover:scale-110 transition-transform`}>
170
+ {config.icon}
171
+ </div>
172
+ <button className="p-2 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors opacity-0 group-hover:opacity-100">
173
+ <Eye size={16} className="text-slate-400" />
174
+ </button>
175
+ </div>
176
+
177
+ {/* Name */}
178
+ <h4 className="font-bold text-slate-900 dark:text-white mb-1 truncate group-hover:text-primary-600 transition-colors">
179
+ {asset.name}
180
+ </h4>
181
+
182
+ {/* Metadata */}
183
+ <div className="space-y-2 text-xs text-slate-500 dark:text-slate-400">
184
+ <div className="flex items-center justify-between">
185
+ <span>Step:</span>
186
+ <span className="font-mono text-slate-700 dark:text-slate-300 truncate ml-2">{asset.step}</span>
187
+ </div>
188
+ {asset.created_at && (
189
+ <div className="flex items-center justify-between">
190
+ <span>Created:</span>
191
+ <span className="text-slate-700 dark:text-slate-300">{format(new Date(asset.created_at), 'MMM d, HH:mm')}</span>
192
+ </div>
193
+ )}
194
+ </div>
195
+
196
+ {/* Properties Count */}
197
+ {asset.properties && Object.keys(asset.properties).length > 0 && (
198
+ <div className="mt-3 pt-3 border-t border-slate-100 dark:border-slate-700">
199
+ <span className="text-xs text-slate-500">
200
+ {Object.keys(asset.properties).length} properties
201
+ </span>
202
+ </div>
203
+ )}
204
+ </Card>
205
+ );
206
+ };
207
+
208
+ if (loading) {
209
+ return (
210
+ <div className="flex items-center justify-center h-96">
211
+ <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
212
+ </div>
213
+ );
214
+ }
215
+
216
+ return (
217
+ <div className="p-6 max-w-7xl mx-auto space-y-6">
218
+ {/* Type Filter */}
219
+ <div className="flex items-center gap-2 overflow-x-auto pb-2">
220
+ <Filter size={16} className="text-slate-400 shrink-0" />
221
+ <div className="flex gap-2">
222
+ {types.map(type => (
223
+ <button
224
+ key={type}
225
+ onClick={() => setTypeFilter(type)}
226
+ className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-all whitespace-nowrap ${typeFilter === type
227
+ ? 'bg-primary-500 text-white shadow-md'
228
+ : 'bg-slate-100 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700'
229
+ }`}
230
+ >
231
+ {type === 'all' ? 'All Types' : type}
232
+ {type !== 'all' && (
233
+ <span className="ml-1.5 text-xs opacity-75">
234
+ ({assets.filter(a => a.type === type).length})
235
+ </span>
236
+ )}
237
+ </button>
238
+ ))}
239
+ </div>
240
+ </div>
241
+
242
+ <DataView
243
+ title="Artifacts"
244
+ subtitle="Browse and manage pipeline artifacts and outputs"
245
+ items={filteredAssets}
246
+ loading={loading}
247
+ columns={columns}
248
+ renderGrid={renderGrid}
249
+ emptyState={
250
+ <EmptyState
251
+ icon={Package}
252
+ title="No artifacts found"
253
+ description={typeFilter !== 'all'
254
+ ? 'Try adjusting your filters'
255
+ : 'Run a pipeline to generate artifacts'
256
+ }
257
+ />
258
+ }
259
+ />
260
+
261
+ {/* Asset Detail Modal */}
262
+ <AssetDetailModal
263
+ asset={selectedAsset}
264
+ onClose={() => setSelectedAsset(null)}
265
+ />
266
+ </div>
267
+ );
268
+ }
269
+
270
+ function AssetDetailModal({ asset, onClose }) {
271
+ if (!asset) return null;
272
+
273
+ const typeConfig = {
274
+ Dataset: { icon: <Database size={24} />, color: 'text-blue-600', bg: 'bg-blue-50' },
275
+ Model: { icon: <Box size={24} />, color: 'text-purple-600', bg: 'bg-purple-50' },
276
+ Metrics: { icon: <BarChart2 size={24} />, color: 'text-emerald-600', bg: 'bg-emerald-50' },
277
+ default: { icon: <FileText size={24} />, color: 'text-slate-600', bg: 'bg-slate-50' }
278
+ };
279
+
280
+ const config = typeConfig[asset.type] || typeConfig.default;
281
+
282
+ return (
283
+ <AnimatePresence>
284
+ <motion.div
285
+ initial={{ opacity: 0 }}
286
+ animate={{ opacity: 1 }}
287
+ exit={{ opacity: 0 }}
288
+ className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
289
+ onClick={onClose}
290
+ >
291
+ <motion.div
292
+ initial={{ scale: 0.9, opacity: 0 }}
293
+ animate={{ scale: 1, opacity: 1 }}
294
+ exit={{ scale: 0.9, opacity: 0 }}
295
+ onClick={(e) => e.stopPropagation()}
296
+ className="bg-white dark:bg-slate-800 rounded-2xl shadow-2xl max-w-4xl w-full max-h-[85vh] overflow-hidden border border-slate-200 dark:border-slate-700"
297
+ >
298
+ {/* Header */}
299
+ <div className={`flex items-center justify-between p-6 border-b border-slate-100 dark:border-slate-700 ${config.bg} dark:bg-slate-800`}>
300
+ <div className="flex items-center gap-4">
301
+ <div className={`p-3 bg-white dark:bg-slate-700 rounded-xl shadow-sm ${config.color}`}>
302
+ {config.icon}
303
+ </div>
304
+ <div>
305
+ <h3 className="text-2xl font-bold text-slate-900 dark:text-white">{asset.name}</h3>
306
+ <p className="text-sm text-slate-500 mt-1">{asset.type} • {asset.step}</p>
307
+ </div>
308
+ </div>
309
+ <button
310
+ onClick={onClose}
311
+ className="p-2 hover:bg-white dark:hover:bg-slate-700 rounded-lg transition-colors"
312
+ >
313
+ <X size={20} className="text-slate-400" />
314
+ </button>
315
+ </div>
316
+
317
+ {/* Content */}
318
+ <div className="p-6 overflow-y-auto max-h-[calc(85vh-200px)]">
319
+ <div className="space-y-6">
320
+ {/* Properties */}
321
+ {asset.properties && Object.keys(asset.properties).length > 0 && (
322
+ <div>
323
+ <h4 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">Properties</h4>
324
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
325
+ {Object.entries(asset.properties).map(([key, value]) => (
326
+ <div key={key} className="p-4 bg-slate-50 dark:bg-slate-700/50 rounded-lg border border-slate-100 dark:border-slate-700">
327
+ <span className="text-sm text-slate-500 dark:text-slate-400 block mb-2 font-medium">{key}</span>
328
+ <span className="text-base font-mono font-semibold text-slate-900 dark:text-white break-all">
329
+ {typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value)}
330
+ </span>
331
+ </div>
332
+ ))}
333
+ </div>
334
+ </div>
335
+ )}
336
+
337
+ {/* Value Preview */}
338
+ {asset.value && (
339
+ <div>
340
+ <h4 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">Value Preview</h4>
341
+ <pre className="p-4 bg-slate-900 text-slate-100 rounded-lg text-sm font-mono overflow-x-auto leading-relaxed">
342
+ {asset.value}
343
+ </pre>
344
+ </div>
345
+ )}
346
+
347
+ {/* Metadata */}
348
+ <div>
349
+ <h4 className="text-lg font-semibold text-slate-900 dark:text-white mb-4">Metadata</h4>
350
+ <KeyValueGrid items={[
351
+ { label: "Artifact ID", value: asset.artifact_id, valueClassName: "font-mono text-xs" },
352
+ { label: "Type", value: asset.type },
353
+ { label: "Step", value: asset.step },
354
+ { label: "Run ID", value: asset.run_id, valueClassName: "font-mono text-xs" },
355
+ ...(asset.created_at ? [{
356
+ label: "Created At",
357
+ value: format(new Date(asset.created_at), 'MMM d, yyyy HH:mm:ss')
358
+ }] : []),
359
+ ...(asset.path ? [{
360
+ label: "Path",
361
+ value: asset.path,
362
+ valueClassName: "font-mono text-xs"
363
+ }] : [])
364
+ ]} columns={2} />
365
+ </div>
366
+ </div>
367
+ </div>
368
+
369
+ {/* Footer */}
370
+ <div className="p-4 border-t border-slate-100 dark:border-slate-700 bg-slate-50 dark:bg-slate-800 flex justify-between items-center">
371
+ <span className="text-sm text-slate-500">
372
+ {asset.created_at && `Created ${format(new Date(asset.created_at), 'MMM d, yyyy')}`}
373
+ </span>
374
+ <div className="flex gap-2">
375
+ <Button variant="ghost" onClick={onClose}>Close</Button>
376
+ <Button variant="primary" className="flex items-center gap-2">
377
+ <Download size={16} />
378
+ Download
379
+ </Button>
380
+ </div>
381
+ </div>
382
+ </motion.div>
383
+ </motion.div>
384
+ </AnimatePresence>
385
+ );
386
+ }
387
+
388
+
389
+
390
+ function getTypeIcon(type) {
391
+ const icons = {
392
+ Dataset: <Database size={18} className="text-blue-600" />,
393
+ Model: <Box size={18} className="text-purple-600" />,
394
+ Metrics: <BarChart2 size={18} className="text-emerald-600" />
395
+ };
396
+ return icons[type] || <FileText size={18} className="text-slate-600" />;
397
+ }