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,753 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import {
3
+ BarChart,
4
+ Bar,
5
+ XAxis,
6
+ YAxis,
7
+ CartesianGrid,
8
+ Tooltip,
9
+ ResponsiveContainer,
10
+ PieChart,
11
+ Pie,
12
+ Cell,
13
+ Legend,
14
+ } from 'recharts';
15
+ import {
16
+ Database,
17
+ BarChart3,
18
+ Table,
19
+ Hash,
20
+ Type,
21
+ Calendar,
22
+ AlertTriangle,
23
+ ChevronDown,
24
+ ChevronRight,
25
+ Eye,
26
+ Columns,
27
+ Rows,
28
+ Info,
29
+ Activity,
30
+ PieChart as PieChartIcon,
31
+ } from 'lucide-react';
32
+ import { motion, AnimatePresence } from 'framer-motion';
33
+
34
+ // Color palette for charts
35
+ const COLORS = [
36
+ '#3b82f6', '#8b5cf6', '#10b981', '#f59e0b', '#ef4444',
37
+ '#ec4899', '#06b6d4', '#84cc16', '#f97316', '#6366f1',
38
+ ];
39
+
40
+ // Custom tooltip for histograms
41
+ const HistogramTooltip = ({ active, payload, label }) => {
42
+ if (active && payload && payload.length) {
43
+ return (
44
+ <div className="bg-white/95 backdrop-blur-lg p-3 rounded-lg shadow-xl border border-slate-200/50">
45
+ <p className="font-medium text-slate-900 text-sm">{label}</p>
46
+ <p className="text-primary-600 text-sm font-mono">
47
+ Count: <span className="font-bold">{payload[0].value}</span>
48
+ </p>
49
+ </div>
50
+ );
51
+ }
52
+ return null;
53
+ };
54
+
55
+ // Statistic card component
56
+ function StatCard({ icon: Icon, label, value, subValue, color = 'blue' }) {
57
+ const colorClasses = {
58
+ blue: 'from-blue-500 to-blue-600',
59
+ purple: 'from-purple-500 to-purple-600',
60
+ emerald: 'from-emerald-500 to-emerald-600',
61
+ amber: 'from-amber-500 to-amber-600',
62
+ rose: 'from-rose-500 to-rose-600',
63
+ };
64
+
65
+ return (
66
+ <motion.div
67
+ initial={{ opacity: 0, y: 10 }}
68
+ animate={{ opacity: 1, y: 0 }}
69
+ className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 p-4 hover:shadow-md transition-shadow"
70
+ >
71
+ <div className="flex items-center gap-3">
72
+ <div className={`p-2 rounded-lg bg-gradient-to-br ${colorClasses[color]} shadow-sm`}>
73
+ <Icon size={16} className="text-white" />
74
+ </div>
75
+ <div>
76
+ <p className="text-xs text-slate-500 dark:text-slate-400 uppercase tracking-wide">{label}</p>
77
+ <p className="text-lg font-bold text-slate-900 dark:text-white font-mono">{value}</p>
78
+ {subValue && (
79
+ <p className="text-xs text-slate-400 dark:text-slate-500">{subValue}</p>
80
+ )}
81
+ </div>
82
+ </div>
83
+ </motion.div>
84
+ );
85
+ }
86
+
87
+ // Column card with expandable histogram
88
+ function ColumnCard({ column, index, data }) {
89
+ const [expanded, setExpanded] = useState(false);
90
+ const color = COLORS[index % COLORS.length];
91
+
92
+ // Calculate statistics for numeric columns
93
+ const stats = useMemo(() => {
94
+ if (!data || !column.values) return null;
95
+
96
+ const values = column.values.filter(v => v !== null && v !== undefined && !isNaN(v));
97
+ if (values.length === 0) return null;
98
+
99
+ const numericValues = values.map(Number).filter(v => !isNaN(v));
100
+ if (numericValues.length === 0) return null;
101
+
102
+ const sum = numericValues.reduce((a, b) => a + b, 0);
103
+ const mean = sum / numericValues.length;
104
+ const sorted = [...numericValues].sort((a, b) => a - b);
105
+ const min = sorted[0];
106
+ const max = sorted[sorted.length - 1];
107
+ const median = sorted[Math.floor(sorted.length / 2)];
108
+ const variance = numericValues.reduce((acc, v) => acc + Math.pow(v - mean, 2), 0) / numericValues.length;
109
+ const std = Math.sqrt(variance);
110
+
111
+ return { mean, min, max, median, std, count: numericValues.length };
112
+ }, [column, data]);
113
+
114
+ // Generate histogram data
115
+ const histogramData = useMemo(() => {
116
+ if (!stats || !column.values) return [];
117
+
118
+ const values = column.values.map(Number).filter(v => !isNaN(v));
119
+ const binCount = Math.min(20, Math.max(5, Math.ceil(Math.sqrt(values.length))));
120
+ const range = stats.max - stats.min;
121
+ const binSize = range / binCount || 1;
122
+
123
+ const bins = Array(binCount).fill(0).map((_, i) => ({
124
+ range: `${(stats.min + i * binSize).toFixed(2)}`,
125
+ count: 0,
126
+ }));
127
+
128
+ values.forEach(v => {
129
+ const binIndex = Math.min(Math.floor((v - stats.min) / binSize), binCount - 1);
130
+ if (binIndex >= 0 && binIndex < binCount) {
131
+ bins[binIndex].count++;
132
+ }
133
+ });
134
+
135
+ return bins;
136
+ }, [column, stats]);
137
+
138
+ const isNumeric = stats !== null;
139
+ const uniqueCount = column.values ? new Set(column.values).size : 0;
140
+ const nullCount = column.values ? column.values.filter(v => v === null || v === undefined || v === '').length : 0;
141
+
142
+ return (
143
+ <motion.div
144
+ layout
145
+ initial={{ opacity: 0, y: 10 }}
146
+ animate={{ opacity: 1, y: 0 }}
147
+ transition={{ delay: index * 0.05 }}
148
+ className="bg-white dark:bg-slate-800 rounded-xl border border-slate-200 dark:border-slate-700 overflow-hidden"
149
+ >
150
+ {/* Column Header */}
151
+ <button
152
+ onClick={() => setExpanded(!expanded)}
153
+ className="w-full p-4 flex items-center justify-between hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors"
154
+ >
155
+ <div className="flex items-center gap-3">
156
+ <div
157
+ className="w-3 h-3 rounded-full"
158
+ style={{ backgroundColor: color }}
159
+ />
160
+ <div className="text-left">
161
+ <h4 className="font-semibold text-slate-900 dark:text-white">{column.name}</h4>
162
+ <div className="flex items-center gap-2 text-xs text-slate-500 dark:text-slate-400">
163
+ <span className="flex items-center gap-1">
164
+ {isNumeric ? <Hash size={10} /> : <Type size={10} />}
165
+ {isNumeric ? 'Numeric' : 'Categorical'}
166
+ </span>
167
+ <span>•</span>
168
+ <span>{uniqueCount} unique</span>
169
+ {nullCount > 0 && (
170
+ <>
171
+ <span>•</span>
172
+ <span className="text-amber-600 flex items-center gap-1">
173
+ <AlertTriangle size={10} />
174
+ {nullCount} null
175
+ </span>
176
+ </>
177
+ )}
178
+ </div>
179
+ </div>
180
+ </div>
181
+ {expanded ? <ChevronDown size={18} /> : <ChevronRight size={18} />}
182
+ </button>
183
+
184
+ {/* Expanded Content */}
185
+ <AnimatePresence>
186
+ {expanded && (
187
+ <motion.div
188
+ initial={{ height: 0, opacity: 0 }}
189
+ animate={{ height: 'auto', opacity: 1 }}
190
+ exit={{ height: 0, opacity: 0 }}
191
+ transition={{ duration: 0.2 }}
192
+ className="overflow-hidden"
193
+ >
194
+ <div className="p-4 pt-0 border-t border-slate-100 dark:border-slate-700">
195
+ {isNumeric && stats && (
196
+ <>
197
+ {/* Statistics Grid */}
198
+ <div className="grid grid-cols-3 gap-2 mb-4">
199
+ <div className="bg-slate-50 dark:bg-slate-900 rounded-lg p-2 text-center">
200
+ <p className="text-xs text-slate-500">Mean</p>
201
+ <p className="font-mono font-bold text-slate-900 dark:text-white">
202
+ {stats.mean.toFixed(4)}
203
+ </p>
204
+ </div>
205
+ <div className="bg-slate-50 dark:bg-slate-900 rounded-lg p-2 text-center">
206
+ <p className="text-xs text-slate-500">Std Dev</p>
207
+ <p className="font-mono font-bold text-slate-900 dark:text-white">
208
+ {stats.std.toFixed(4)}
209
+ </p>
210
+ </div>
211
+ <div className="bg-slate-50 dark:bg-slate-900 rounded-lg p-2 text-center">
212
+ <p className="text-xs text-slate-500">Median</p>
213
+ <p className="font-mono font-bold text-slate-900 dark:text-white">
214
+ {stats.median.toFixed(4)}
215
+ </p>
216
+ </div>
217
+ <div className="bg-slate-50 dark:bg-slate-900 rounded-lg p-2 text-center">
218
+ <p className="text-xs text-slate-500">Min</p>
219
+ <p className="font-mono font-bold text-emerald-600">
220
+ {stats.min.toFixed(4)}
221
+ </p>
222
+ </div>
223
+ <div className="bg-slate-50 dark:bg-slate-900 rounded-lg p-2 text-center">
224
+ <p className="text-xs text-slate-500">Max</p>
225
+ <p className="font-mono font-bold text-rose-600">
226
+ {stats.max.toFixed(4)}
227
+ </p>
228
+ </div>
229
+ <div className="bg-slate-50 dark:bg-slate-900 rounded-lg p-2 text-center">
230
+ <p className="text-xs text-slate-500">Count</p>
231
+ <p className="font-mono font-bold text-slate-900 dark:text-white">
232
+ {stats.count}
233
+ </p>
234
+ </div>
235
+ </div>
236
+
237
+ {/* Histogram */}
238
+ <div className="bg-slate-50 dark:bg-slate-900 rounded-lg p-3">
239
+ <h5 className="text-xs font-semibold text-slate-600 dark:text-slate-400 mb-2 flex items-center gap-1">
240
+ <BarChart3 size={12} /> Distribution
241
+ </h5>
242
+ <ResponsiveContainer width="100%" height={120}>
243
+ <BarChart data={histogramData}>
244
+ <CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" strokeOpacity={0.5} />
245
+ <XAxis
246
+ dataKey="range"
247
+ tick={{ fontSize: 9 }}
248
+ tickLine={false}
249
+ interval="preserveStartEnd"
250
+ />
251
+ <YAxis tick={{ fontSize: 9 }} tickLine={false} width={30} />
252
+ <Tooltip content={<HistogramTooltip />} />
253
+ <Bar dataKey="count" fill={color} radius={[2, 2, 0, 0]} />
254
+ </BarChart>
255
+ </ResponsiveContainer>
256
+ </div>
257
+ </>
258
+ )}
259
+
260
+ {!isNumeric && column.values && (
261
+ <CategoricalDistribution values={column.values} color={color} />
262
+ )}
263
+ </div>
264
+ </motion.div>
265
+ )}
266
+ </AnimatePresence>
267
+ </motion.div>
268
+ );
269
+ }
270
+
271
+ // Categorical distribution component
272
+ function CategoricalDistribution({ values, color }) {
273
+ const distribution = useMemo(() => {
274
+ const counts = {};
275
+ values.forEach(v => {
276
+ const key = v === null || v === undefined ? '(null)' : String(v);
277
+ counts[key] = (counts[key] || 0) + 1;
278
+ });
279
+
280
+ return Object.entries(counts)
281
+ .sort((a, b) => b[1] - a[1])
282
+ .slice(0, 10)
283
+ .map(([name, count], idx) => ({
284
+ name: name.length > 15 ? name.substring(0, 15) + '...' : name,
285
+ count,
286
+ fill: COLORS[idx % COLORS.length],
287
+ }));
288
+ }, [values]);
289
+
290
+ return (
291
+ <div className="bg-slate-50 dark:bg-slate-900 rounded-lg p-3">
292
+ <h5 className="text-xs font-semibold text-slate-600 dark:text-slate-400 mb-2 flex items-center gap-1">
293
+ <PieChartIcon size={12} /> Top Values
294
+ </h5>
295
+ <ResponsiveContainer width="100%" height={150}>
296
+ <BarChart data={distribution} layout="vertical">
297
+ <CartesianGrid strokeDasharray="3 3" stroke="#e2e8f0" strokeOpacity={0.5} />
298
+ <XAxis type="number" tick={{ fontSize: 9 }} />
299
+ <YAxis dataKey="name" type="category" tick={{ fontSize: 9 }} width={80} />
300
+ <Tooltip content={<HistogramTooltip />} />
301
+ <Bar dataKey="count" radius={[0, 2, 2, 0]}>
302
+ {distribution.map((entry, index) => (
303
+ <Cell key={`cell-${index}`} fill={entry.fill} />
304
+ ))}
305
+ </Bar>
306
+ </BarChart>
307
+ </ResponsiveContainer>
308
+ </div>
309
+ );
310
+ }
311
+
312
+ // Sample data table
313
+ function SampleDataTable({ data, columns, maxRows = 10 }) {
314
+ if (!data || !columns || columns.length === 0) return null;
315
+
316
+ const sampleData = data.slice(0, maxRows);
317
+
318
+ return (
319
+ <div className="overflow-x-auto rounded-lg border border-slate-200 dark:border-slate-700">
320
+ <table className="w-full text-xs">
321
+ <thead className="bg-slate-100 dark:bg-slate-800">
322
+ <tr>
323
+ <th className="px-3 py-2 text-left text-slate-600 dark:text-slate-400 font-semibold border-b border-slate-200 dark:border-slate-700">
324
+ #
325
+ </th>
326
+ {columns.map((col, idx) => (
327
+ <th
328
+ key={idx}
329
+ className="px-3 py-2 text-left text-slate-600 dark:text-slate-400 font-semibold border-b border-slate-200 dark:border-slate-700"
330
+ >
331
+ {col}
332
+ </th>
333
+ ))}
334
+ </tr>
335
+ </thead>
336
+ <tbody>
337
+ {sampleData.map((row, rowIdx) => (
338
+ <tr
339
+ key={rowIdx}
340
+ className="hover:bg-slate-50 dark:hover:bg-slate-800/50 transition-colors"
341
+ >
342
+ <td className="px-3 py-2 text-slate-400 border-b border-slate-100 dark:border-slate-800 font-mono">
343
+ {rowIdx + 1}
344
+ </td>
345
+ {columns.map((col, colIdx) => (
346
+ <td
347
+ key={colIdx}
348
+ className="px-3 py-2 text-slate-700 dark:text-slate-300 border-b border-slate-100 dark:border-slate-800 font-mono"
349
+ >
350
+ {row[col] === null || row[col] === undefined ? (
351
+ <span className="text-slate-400 italic">null</span>
352
+ ) : typeof row[col] === 'number' ? (
353
+ row[col].toFixed(4)
354
+ ) : (
355
+ String(row[col]).substring(0, 30)
356
+ )}
357
+ </td>
358
+ ))}
359
+ </tr>
360
+ ))}
361
+ </tbody>
362
+ </table>
363
+ {data.length > maxRows && (
364
+ <div className="px-3 py-2 text-center text-xs text-slate-500 bg-slate-50 dark:bg-slate-900">
365
+ Showing {maxRows} of {data.length} rows
366
+ </div>
367
+ )}
368
+ </div>
369
+ );
370
+ }
371
+
372
+ // Helper to safely parse Python-style dict strings
373
+ function parsePythonDict(str) {
374
+ if (!str || typeof str !== 'string') return null;
375
+ try {
376
+ // Replace Python-style quotes and booleans
377
+ let jsonStr = str
378
+ .replace(/'/g, '"') // Replace single quotes with double quotes
379
+ .replace(/True/g, 'true')
380
+ .replace(/False/g, 'false')
381
+ .replace(/None/g, 'null');
382
+ return JSON.parse(jsonStr);
383
+ } catch (e) {
384
+ console.warn('Failed to parse data string:', e);
385
+ return null;
386
+ }
387
+ }
388
+
389
+ // Normalize different dataset formats (pandas, tensorflow, numpy, etc.)
390
+ function normalizeDatasetFormat(data) {
391
+ if (!data) return null;
392
+
393
+ // Already in the expected format {features: {...}, target: [...]}
394
+ if (data.features && typeof data.features === 'object') {
395
+ return data;
396
+ }
397
+
398
+ // TensorFlow dataset metadata format
399
+ // Often stored as: {element_spec: {...}, cardinality: N, ...}
400
+ if (data.element_spec || data.cardinality !== undefined) {
401
+ return {
402
+ _tf_dataset: true,
403
+ element_spec: data.element_spec,
404
+ cardinality: data.cardinality,
405
+ ...data
406
+ };
407
+ }
408
+
409
+ // Pandas DataFrame format: {columns: [...], data: [[...], ...], index: [...]}
410
+ if (data.columns && Array.isArray(data.columns) && data.data && Array.isArray(data.data)) {
411
+ const features = {};
412
+ data.columns.forEach((col, idx) => {
413
+ features[col] = data.data.map(row => row[idx]);
414
+ });
415
+ return { features, target: [] };
416
+ }
417
+
418
+ // Numpy array format: {shape: [...], dtype: '...', data: [...]}
419
+ if (data.shape && data.dtype && Array.isArray(data.data)) {
420
+ // Single feature array
421
+ return {
422
+ features: { values: data.data.flat() },
423
+ target: []
424
+ };
425
+ }
426
+
427
+ // Dict of arrays format: {col1: [...], col2: [...]}
428
+ if (typeof data === 'object' && !Array.isArray(data)) {
429
+ const keys = Object.keys(data);
430
+ const firstVal = data[keys[0]];
431
+ if (Array.isArray(firstVal)) {
432
+ // Treat last column as target if it's named 'target', 'label', or 'y'
433
+ const targetKeys = ['target', 'label', 'y', 'labels', 'targets'];
434
+ const targetKey = keys.find(k => targetKeys.includes(k.toLowerCase()));
435
+ const featureKeys = keys.filter(k => k !== targetKey);
436
+
437
+ const features = {};
438
+ featureKeys.forEach(k => {
439
+ features[k] = data[k];
440
+ });
441
+
442
+ return {
443
+ features,
444
+ target: targetKey ? data[targetKey] : []
445
+ };
446
+ }
447
+ }
448
+
449
+ return null;
450
+ }
451
+
452
+ // Main DatasetViewer component
453
+ export function DatasetViewer({ artifact }) {
454
+ const [activeTab, setActiveTab] = useState('overview');
455
+
456
+ // Parse dataset information from artifact
457
+ const datasetInfo = useMemo(() => {
458
+ if (!artifact) return null;
459
+
460
+ const props = artifact.properties || {};
461
+
462
+ // Data can be in multiple places:
463
+ // 1. artifact.data (if already parsed by backend)
464
+ // 2. props._full_data (full data stored in properties)
465
+ // 3. artifact.value (string that needs parsing - may be truncated!)
466
+ // 4. props.data
467
+ let rawData = artifact.data || props._full_data || props.data;
468
+
469
+ // If data is not found, try parsing the value field
470
+ if (!rawData && artifact.value) {
471
+ rawData = parsePythonDict(artifact.value);
472
+ }
473
+
474
+ // Normalize the data format (handles pandas, tensorflow, numpy, etc.)
475
+ const data = normalizeDatasetFormat(rawData);
476
+
477
+ // Handle TensorFlow dataset metadata (no actual data, just specs)
478
+ if (data && data._tf_dataset) {
479
+ return {
480
+ name: artifact.name || 'Dataset',
481
+ numSamples: data.cardinality || props.num_samples || props.samples || 'Unknown',
482
+ numFeatures: props.num_features || 0,
483
+ featureColumns: props.feature_columns || [],
484
+ columns: [],
485
+ columnNames: [],
486
+ samples: [],
487
+ source: props.source || 'TensorFlow Dataset',
488
+ createdAt: artifact.created_at,
489
+ isTensorFlow: true,
490
+ elementSpec: data.element_spec,
491
+ tfMetadata: data,
492
+ };
493
+ }
494
+
495
+ // If still no data, return basic info
496
+ if (!data || typeof data !== 'object') {
497
+ return {
498
+ name: artifact.name || 'Dataset',
499
+ numSamples: props.num_samples || props.samples || props.cardinality || 0,
500
+ numFeatures: props.num_features || (props.feature_columns?.length || 0),
501
+ featureColumns: props.feature_columns || [],
502
+ columns: [],
503
+ columnNames: [],
504
+ samples: [],
505
+ source: props.source,
506
+ createdAt: artifact.created_at,
507
+ };
508
+ }
509
+
510
+ // Try to extract features and target
511
+ let features = data.features || {};
512
+ let target = data.target || [];
513
+ let samples = [];
514
+ let columnNames = [];
515
+ let columns = [];
516
+
517
+ // If features is an object with column arrays
518
+ if (features && typeof features === 'object' && !Array.isArray(features)) {
519
+ columnNames = Object.keys(features);
520
+ const numRows = features[columnNames[0]]?.length || 0;
521
+
522
+ // Build row-based samples for table view
523
+ for (let i = 0; i < numRows; i++) {
524
+ const row = {};
525
+ columnNames.forEach(col => {
526
+ row[col] = features[col]?.[i];
527
+ });
528
+ if (Array.isArray(target) && target.length > i) {
529
+ row['target'] = target[i];
530
+ }
531
+ samples.push(row);
532
+ }
533
+
534
+ // Build column info for statistics
535
+ columnNames.forEach(col => {
536
+ columns.push({
537
+ name: col,
538
+ values: features[col] || [],
539
+ });
540
+ });
541
+
542
+ // Add target column if exists
543
+ if (Array.isArray(target) && target.length > 0) {
544
+ columnNames.push('target');
545
+ columns.push({
546
+ name: 'target',
547
+ values: target,
548
+ });
549
+ }
550
+ }
551
+
552
+ return {
553
+ name: artifact.name || 'Dataset',
554
+ numSamples: props.num_samples || props.samples || samples.length,
555
+ numFeatures: props.num_features || columnNames.length - (Array.isArray(target) && target.length > 0 ? 1 : 0),
556
+ featureColumns: props.feature_columns || columnNames.filter(c => c !== 'target'),
557
+ columns,
558
+ columnNames,
559
+ samples,
560
+ source: props.source,
561
+ createdAt: artifact.created_at,
562
+ };
563
+ }, [artifact]);
564
+
565
+ if (!datasetInfo) {
566
+ return (
567
+ <div className="flex flex-col items-center justify-center p-8 text-slate-400">
568
+ <Database size={32} className="mb-2 opacity-50" />
569
+ <p className="text-sm">No dataset information available</p>
570
+ </div>
571
+ );
572
+ }
573
+
574
+ // Special view for TensorFlow datasets (show metadata/specs instead of data)
575
+ if (datasetInfo.isTensorFlow) {
576
+ return (
577
+ <div className="space-y-6">
578
+ {/* Header */}
579
+ <div className="flex items-center gap-3 pb-4 border-b border-slate-200 dark:border-slate-700">
580
+ <div className="p-3 bg-gradient-to-br from-orange-500 to-red-600 rounded-xl shadow-lg">
581
+ <Database size={24} className="text-white" />
582
+ </div>
583
+ <div>
584
+ <h3 className="text-xl font-bold text-slate-900 dark:text-white">{datasetInfo.name}</h3>
585
+ <p className="text-sm text-slate-500 dark:text-slate-400">
586
+ TensorFlow Dataset • {datasetInfo.numSamples} samples
587
+ </p>
588
+ </div>
589
+ </div>
590
+
591
+ {/* TF Dataset Info */}
592
+ <div className="bg-gradient-to-br from-orange-50 to-red-50 dark:from-orange-900/20 dark:to-red-900/20 rounded-xl p-6 border border-orange-200 dark:border-orange-800">
593
+ <h4 className="font-bold text-slate-900 dark:text-white mb-4 flex items-center gap-2">
594
+ <Activity size={18} className="text-orange-600" />
595
+ TensorFlow Dataset Metadata
596
+ </h4>
597
+ <div className="grid grid-cols-2 gap-4">
598
+ <div className="bg-white dark:bg-slate-800 p-4 rounded-lg">
599
+ <p className="text-xs text-slate-500 uppercase tracking-wide">Cardinality</p>
600
+ <p className="text-lg font-bold text-slate-900 dark:text-white font-mono">
601
+ {datasetInfo.numSamples}
602
+ </p>
603
+ </div>
604
+ <div className="bg-white dark:bg-slate-800 p-4 rounded-lg">
605
+ <p className="text-xs text-slate-500 uppercase tracking-wide">Source</p>
606
+ <p className="text-lg font-bold text-slate-900 dark:text-white">
607
+ {datasetInfo.source}
608
+ </p>
609
+ </div>
610
+ </div>
611
+ {datasetInfo.elementSpec && (
612
+ <div className="mt-4 bg-white dark:bg-slate-800 p-4 rounded-lg">
613
+ <p className="text-xs text-slate-500 uppercase tracking-wide mb-2">Element Spec</p>
614
+ <pre className="text-xs font-mono text-slate-700 dark:text-slate-300 overflow-x-auto">
615
+ {JSON.stringify(datasetInfo.elementSpec, null, 2)}
616
+ </pre>
617
+ </div>
618
+ )}
619
+ <p className="text-xs text-slate-500 mt-4 italic">
620
+ 💡 TensorFlow datasets are lazy-loaded. Full data visualization requires materializing the dataset.
621
+ </p>
622
+ </div>
623
+ </div>
624
+ );
625
+ }
626
+
627
+ return (
628
+ <div className="space-y-6">
629
+ {/* Header */}
630
+ <div className="flex items-center gap-3 pb-4 border-b border-slate-200 dark:border-slate-700">
631
+ <div className="p-3 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl shadow-lg">
632
+ <Database size={24} className="text-white" />
633
+ </div>
634
+ <div>
635
+ <h3 className="text-xl font-bold text-slate-900 dark:text-white">{datasetInfo.name}</h3>
636
+ <p className="text-sm text-slate-500 dark:text-slate-400">
637
+ Dataset with {datasetInfo.numSamples} samples, {datasetInfo.numFeatures} features
638
+ </p>
639
+ </div>
640
+ </div>
641
+
642
+ {/* Quick Stats */}
643
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
644
+ <StatCard
645
+ icon={Rows}
646
+ label="Samples"
647
+ value={datasetInfo.numSamples?.toLocaleString() || '—'}
648
+ color="blue"
649
+ />
650
+ <StatCard
651
+ icon={Columns}
652
+ label="Features"
653
+ value={datasetInfo.numFeatures || '—'}
654
+ color="purple"
655
+ />
656
+ <StatCard
657
+ icon={Hash}
658
+ label="Columns"
659
+ value={datasetInfo.columnNames.length}
660
+ color="emerald"
661
+ />
662
+ <StatCard
663
+ icon={Info}
664
+ label="Source"
665
+ value={datasetInfo.source || 'Pipeline'}
666
+ color="amber"
667
+ />
668
+ </div>
669
+
670
+ {/* Tabs */}
671
+ <div className="flex gap-2 border-b border-slate-200 dark:border-slate-700">
672
+ <button
673
+ onClick={() => setActiveTab('overview')}
674
+ className={`px-4 py-2 text-sm font-medium transition-colors ${
675
+ activeTab === 'overview'
676
+ ? 'text-primary-600 border-b-2 border-primary-600'
677
+ : 'text-slate-500 hover:text-slate-700'
678
+ }`}
679
+ >
680
+ <BarChart3 size={14} className="inline mr-1" />
681
+ Column Statistics
682
+ </button>
683
+ <button
684
+ onClick={() => setActiveTab('sample')}
685
+ className={`px-4 py-2 text-sm font-medium transition-colors ${
686
+ activeTab === 'sample'
687
+ ? 'text-primary-600 border-b-2 border-primary-600'
688
+ : 'text-slate-500 hover:text-slate-700'
689
+ }`}
690
+ >
691
+ <Table size={14} className="inline mr-1" />
692
+ Sample Data
693
+ </button>
694
+ </div>
695
+
696
+ {/* Tab Content */}
697
+ <AnimatePresence mode="wait">
698
+ {activeTab === 'overview' && (
699
+ <motion.div
700
+ key="overview"
701
+ initial={{ opacity: 0, y: 10 }}
702
+ animate={{ opacity: 1, y: 0 }}
703
+ exit={{ opacity: 0, y: -10 }}
704
+ className="space-y-3"
705
+ >
706
+ {datasetInfo.columns.length > 0 ? (
707
+ datasetInfo.columns.map((column, idx) => (
708
+ <ColumnCard
709
+ key={column.name}
710
+ column={column}
711
+ index={idx}
712
+ data={datasetInfo.samples}
713
+ />
714
+ ))
715
+ ) : (
716
+ <div className="text-center py-8 text-slate-400">
717
+ <BarChart3 size={32} className="mx-auto mb-2 opacity-50" />
718
+ <p className="text-sm">Column statistics not available</p>
719
+ <p className="text-xs text-slate-500 mt-1">
720
+ Data structure may not support detailed analysis
721
+ </p>
722
+ </div>
723
+ )}
724
+ </motion.div>
725
+ )}
726
+
727
+ {activeTab === 'sample' && (
728
+ <motion.div
729
+ key="sample"
730
+ initial={{ opacity: 0, y: 10 }}
731
+ animate={{ opacity: 1, y: 0 }}
732
+ exit={{ opacity: 0, y: -10 }}
733
+ >
734
+ {datasetInfo.samples.length > 0 ? (
735
+ <SampleDataTable
736
+ data={datasetInfo.samples}
737
+ columns={datasetInfo.columnNames}
738
+ maxRows={15}
739
+ />
740
+ ) : (
741
+ <div className="text-center py-8 text-slate-400">
742
+ <Table size={32} className="mx-auto mb-2 opacity-50" />
743
+ <p className="text-sm">Sample data not available</p>
744
+ </div>
745
+ )}
746
+ </motion.div>
747
+ )}
748
+ </AnimatePresence>
749
+ </div>
750
+ );
751
+ }
752
+
753
+ export default DatasetViewer;