midah commited on
Commit
e904fd3
·
1 Parent(s): 5b86fb9

Improve dashboard UX with cohesive design

Browse files

Add emoji icons, purple theme, better section organization

Files changed (2) hide show
  1. frontend/src/App.css +13 -6
  2. frontend/src/App.tsx +511 -127
frontend/src/App.css CHANGED
@@ -165,12 +165,11 @@
165
 
166
  .sidebar h3 {
167
  font-size: 0.95rem;
168
- font-weight: 600;
169
- color: #2a2a2a;
170
- margin: 0 0 0.75rem 0;
171
- text-transform: uppercase;
172
- letter-spacing: 0.05em;
173
- font-size: 0.8rem;
174
  }
175
 
176
  .sidebar label {
@@ -856,6 +855,14 @@
856
  background: var(--bg-secondary, #f5f5f5);
857
  }
858
 
 
 
 
 
 
 
 
 
859
  .cluster-item input[type="checkbox"] {
860
  margin-right: 0.5rem;
861
  cursor: pointer;
 
165
 
166
  .sidebar h3 {
167
  font-size: 0.95rem;
168
+ font-weight: 700;
169
+ color: #5e35b1;
170
+ margin: 0 0 1rem 0;
171
+ letter-spacing: -0.01em;
172
+ text-transform: none;
 
173
  }
174
 
175
  .sidebar label {
 
855
  background: var(--bg-secondary, #f5f5f5);
856
  }
857
 
858
+ .sidebar input[type="checkbox"] {
859
+ width: 18px;
860
+ height: 18px;
861
+ cursor: pointer;
862
+ accent-color: #5e35b1;
863
+ margin-right: 0.5rem;
864
+ }
865
+
866
  .cluster-item input[type="checkbox"] {
867
  margin-right: 0.5rem;
868
  cursor: pointer;
frontend/src/App.tsx CHANGED
@@ -1,28 +1,81 @@
1
- /**
2
- * Main React app component using Visx for visualization.
3
- */
4
  import React, { useState, useEffect, useCallback, useRef, useMemo, lazy, Suspense } from 'react';
5
- import EnhancedScatterPlot from './components/EnhancedScatterPlot';
6
- import NetworkGraph from './components/NetworkGraph';
7
- import Histogram from './components/Histogram';
8
- import UVProjectionSquare from './components/UVProjectionSquare';
9
- import ModelModal from './components/ModelModal';
10
- import PaperPlots from './components/PaperPlots';
11
- import LiveModelCount from './components/LiveModelCount';
12
- import ColorLegend from './components/ColorLegend';
13
- import ModelTooltip from './components/ModelTooltip';
14
- import ErrorBoundary from './components/ErrorBoundary';
 
 
 
 
 
 
 
 
 
 
 
 
15
  import { ModelPoint, Stats, FamilyTree, SearchResult, SimilarModel } from './types';
16
- import cache, { IndexedDBCache } from './utils/indexedDB';
17
  import { debounce } from './utils/debounce';
18
- import requestManager from './utils/requestManager';
 
 
19
  import './App.css';
20
 
21
- const ScatterPlot3D = lazy(() => import('./components/ScatterPlot3D'));
 
 
 
 
 
 
22
 
23
- const API_BASE = process.env.REACT_APP_API_URL || 'http://localhost:8000';
24
 
25
  function App() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  const [data, setData] = useState<ModelPoint[]>([]);
27
  const [filteredCount, setFilteredCount] = useState<number | null>(null);
28
  const [stats, setStats] = useState<Stats | null>(null);
@@ -31,8 +84,6 @@ function App() {
31
  const [selectedModel, setSelectedModel] = useState<ModelPoint | null>(null);
32
  const [isModalOpen, setIsModalOpen] = useState(false);
33
  const [selectedModels, setSelectedModels] = useState<ModelPoint[]>([]);
34
- const [viewMode, setViewMode] = useState<'scatter' | 'network' | 'histogram' | '3d' | 'paper-plots'>('3d');
35
- const [histogramAttribute, setHistogramAttribute] = useState<'downloads' | 'likes' | 'trending_score'>('downloads');
36
  const [baseModelsOnly, setBaseModelsOnly] = useState(false);
37
  const [semanticSimilarityMode, setSemanticSimilarityMode] = useState(false);
38
  const [semanticQueryModel, setSemanticQueryModel] = useState<string | null>(null);
@@ -48,13 +99,6 @@ function App() {
48
  const [comparisonModels, setComparisonModels] = useState<ModelPoint[]>([]);
49
  const [similarModels, setSimilarModels] = useState<SimilarModel[]>([]);
50
  const [showSimilar, setShowSimilar] = useState(false);
51
-
52
- const [minDownloads, setMinDownloads] = useState(0);
53
- const [minLikes, setMinLikes] = useState(0);
54
- const [searchQuery, setSearchQuery] = useState('');
55
- const [colorBy, setColorBy] = useState('library_name');
56
- const [sizeBy, setSizeBy] = useState('downloads');
57
- const [colorScheme, setColorScheme] = useState<'viridis' | 'plasma' | 'inferno' | 'magma' | 'coolwarm'>('viridis');
58
  const [showLegend, setShowLegend] = useState(true);
59
  const [hoveredModel, setHoveredModel] = useState<ModelPoint | null>(null);
60
  const [tooltipPosition, setTooltipPosition] = useState<{ x: number; y: number } | null>(null);
@@ -64,16 +108,22 @@ function App() {
64
  const [showStructuralGroups, setShowStructuralGroups] = useState(false);
65
  const [overviewMode, setOverviewMode] = useState(false);
66
  const [networkEdgeType, setNetworkEdgeType] = useState<'library' | 'pipeline' | 'combined'>('combined');
 
 
 
 
 
 
 
67
 
68
- const activeFilterCount = (minDownloads > 0 ? 1 : 0) +
69
- (minLikes > 0 ? 1 : 0) +
70
- (searchQuery.length > 0 ? 1 : 0);
71
 
72
  const resetFilters = useCallback(() => {
 
73
  setMinDownloads(0);
74
  setMinLikes(0);
75
  setSearchQuery('');
76
- }, []);
77
 
78
  const [width, setWidth] = useState(window.innerWidth * 0.7);
79
  const [height, setHeight] = useState(window.innerHeight * 0.7);
@@ -106,11 +156,14 @@ function App() {
106
  });
107
 
108
  const cachedModels = await cache.getCachedModels(cacheKey);
109
- if (cachedModels) {
110
  setData(cachedModels);
111
- // Set filteredCount to models length when using cache (best approximation)
112
  setFilteredCount(cachedModels.length);
113
  setLoading(false);
 
 
 
 
114
  return;
115
  }
116
  let models: ModelPoint[];
@@ -129,7 +182,6 @@ function App() {
129
  if (!response.ok) throw new Error('Failed to fetch similar models');
130
  const result = await response.json();
131
  models = result.models || [];
132
- // Semantic similarity doesn't return filtered_count, use models length
133
  count = models.length;
134
  } else {
135
  const params = new URLSearchParams({
@@ -139,14 +191,12 @@ function App() {
139
  size_by: sizeBy,
140
  projection_method: projectionMethod,
141
  base_models_only: baseModelsOnly.toString(),
 
142
  });
143
  if (searchQuery) {
144
  params.append('search_query', searchQuery);
145
  }
146
 
147
- // Request a large number of models for better representation
148
- // The backend will use stratified sampling if needed, and frontend will further optimize
149
- // Set to 500K to get good coverage while allowing backend to optimize
150
  params.append('max_points', '500000');
151
 
152
  const url = `${API_BASE}/api/models?${params}`;
@@ -154,13 +204,14 @@ function App() {
154
  if (!response.ok) throw new Error('Failed to fetch models');
155
  const result = await response.json();
156
 
157
- // Handle both old format (array) and new format (object with models, filtered_count, returned_count)
158
  if (Array.isArray(result)) {
159
  models = result;
160
  count = models.length;
 
161
  } else {
162
  models = result.models || [];
163
  count = result.filtered_count ?? models.length;
 
164
  }
165
  }
166
 
@@ -170,13 +221,19 @@ function App() {
170
  setFilteredCount(count);
171
  } catch (err: any) {
172
  if (err.name !== 'AbortError') {
173
- setError(err instanceof Error ? err.message : 'Unknown error');
 
 
 
 
 
 
174
  }
175
  } finally {
176
  setLoading(false);
177
  fetchDataAbortRef.current = null;
178
  }
179
- }, [minDownloads, minLikes, searchQuery, colorBy, sizeBy, projectionMethod, baseModelsOnly, semanticSimilarityMode, semanticQueryModel]);
180
 
181
  const debouncedFetchData = useMemo(
182
  () => debounce(fetchData, 300),
@@ -187,7 +244,6 @@ function App() {
187
  if (searchQuery) {
188
  debouncedFetchData();
189
  } else {
190
- // Immediate fetch if search is cleared
191
  fetchData();
192
  }
193
  return () => {
@@ -200,7 +256,7 @@ function App() {
200
  return () => {
201
  debouncedFetchData.cancel();
202
  };
203
- }, [minDownloads, minLikes, colorBy, sizeBy, baseModelsOnly, projectionMethod, semanticSimilarityMode, semanticQueryModel, debouncedFetchData]);
204
 
205
  useEffect(() => {
206
  const fetchStats = async () => {
@@ -218,13 +274,35 @@ function App() {
218
  await cache.cacheStats(cacheKey, statsData);
219
  setStats(statsData);
220
  } catch (err) {
221
- console.error('Error fetching stats:', err);
 
 
222
  }
223
  };
224
 
225
  fetchStats();
226
  }, []);
227
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  // Search models for family tree lookup
229
  const searchModels = useCallback(async (query: string) => {
230
  if (query.length < 1) {
@@ -239,7 +317,7 @@ function App() {
239
  setSearchResults(data.results || []);
240
  setShowSearchResults(true);
241
  } catch (err) {
242
- console.error('Search error:', err);
243
  setSearchResults([]);
244
  }
245
  }, []);
@@ -262,7 +340,7 @@ function App() {
262
  setShowSearchResults(false);
263
  setSearchInput('');
264
  } catch (err) {
265
- console.error('Family tree error:', err);
266
  setFamilyTree([]);
267
  setFamilyTreeModelId(null);
268
  }
@@ -273,6 +351,21 @@ function App() {
273
  setFamilyTreeModelId(null);
274
  }, []);
275
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  const loadSimilarModels = useCallback(async (modelId: string) => {
277
  try {
278
  const response = await fetch(`${API_BASE}/api/similar/${encodeURIComponent(modelId)}?k=10`);
@@ -298,11 +391,9 @@ function App() {
298
  setShowSimilar(true);
299
  } catch (err) {
300
  const errorMessage = err instanceof Error ? err.message : 'Failed to load similar models';
301
- console.error('Similar models error:', errorMessage, err);
302
- // Only show error if it's not a silent failure (e.g., user cancelled)
303
  if (errorMessage !== 'Failed to load similar models' || !(err instanceof TypeError && err.message.includes('fetch'))) {
304
  setError(`Similar models: ${errorMessage}`);
305
- // Clear error after 5 seconds
306
  setTimeout(() => setError(null), 5000);
307
  }
308
  setSimilarModels([]);
@@ -351,7 +442,7 @@ function App() {
351
  document.body.removeChild(a);
352
  URL.revokeObjectURL(url);
353
  } catch (err) {
354
- console.error('Export error:', err);
355
  alert('Failed to export models');
356
  }
357
  }, []);
@@ -409,19 +500,34 @@ function App() {
409
 
410
  <div className="main-content">
411
  <aside className="sidebar">
412
- {/* Live Model Count - Prominent Display */}
413
- <LiveModelCount compact={false} />
414
-
415
- <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem', marginTop: '1rem' }}>
416
- <h2 style={{ margin: 0 }}>Filters</h2>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
  {activeFilterCount > 0 && (
418
  <div style={{
419
  fontSize: '0.75rem',
420
- background: '#4a4a4a',
421
  color: 'white',
422
- padding: '0.25rem 0.5rem',
423
- borderRadius: '12px',
424
- fontWeight: '500'
 
425
  }}>
426
  {activeFilterCount} active
427
  </div>
@@ -431,21 +537,41 @@ function App() {
431
  {/* Filter Results Count */}
432
  {!loading && data.length > 0 && (
433
  <div className="sidebar-section" style={{
434
- background: '#e8f5e9',
435
- borderColor: '#c8e6c9',
436
- fontSize: '0.9rem'
 
437
  }}>
438
- <strong>{data.length.toLocaleString()}</strong> {data.length === 1 ? 'model' : 'models'} shown
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
439
  {filteredCount !== null && filteredCount !== data.length && (
440
- <div style={{ fontSize: '0.8rem', color: '#666', marginTop: '0.25rem' }}>
441
- of {filteredCount.toLocaleString()} matching filters
442
  </div>
443
  )}
444
- {stats && filteredCount !== null && (
445
- <div style={{ fontSize: '0.8rem', color: '#666', marginTop: '0.25rem' }}>
446
- {filteredCount < stats.total_models && (
447
- <>out of {stats.total_models.toLocaleString()} total in dataset</>
448
- )}
449
  </div>
450
  )}
451
  </div>
@@ -453,9 +579,15 @@ function App() {
453
 
454
  {/* Search Section */}
455
  <div className="sidebar-section">
456
- <label style={{ fontWeight: '600', marginBottom: '0.5rem', display: 'block' }}>
457
- Search Models
458
- </label>
 
 
 
 
 
 
459
  <input
460
  type="text"
461
  value={searchQuery}
@@ -463,14 +595,21 @@ function App() {
463
  placeholder="Search by model ID, tags, or keywords..."
464
  style={{ width: '100%' }}
465
  />
466
- <div style={{ fontSize: '0.75rem', color: '#666', marginTop: '0.25rem' }}>
467
- Searches model names, tags, and metadata
468
  </div>
469
  </div>
470
 
471
  {/* Popularity Filters */}
472
  <div className="sidebar-section">
473
- <h3>Popularity Filters</h3>
 
 
 
 
 
 
 
474
 
475
  <label style={{ marginBottom: '1rem', display: 'block' }}>
476
  <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
@@ -519,37 +658,149 @@ function App() {
519
  </label>
520
  </div>
521
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522
  {/* Visualization Options */}
523
  <div className="sidebar-section">
524
- <h3>Visualization Options</h3>
 
 
 
 
 
 
 
 
 
 
 
525
 
526
  <label style={{ marginBottom: '1rem', display: 'block' }}>
527
  <span style={{ fontWeight: '500', display: 'block', marginBottom: '0.5rem' }}>View Mode</span>
528
  <select
529
  value={viewMode}
530
- onChange={(e) => setViewMode(e.target.value as any)}
531
  style={{ width: '100%', padding: '0.5rem', borderRadius: '4px', border: '1px solid #d0d0d0' }}
532
  >
533
  <option value="3d">3D Latent Space</option>
534
  <option value="scatter">2D Latent Space</option>
535
  <option value="network">Network Graph</option>
536
- <option value="histogram">Distribution Histogram</option>
537
- <option value="paper-plots">Paper Visualizations</option>
 
538
  </select>
539
  <div style={{ fontSize: '0.75rem', color: '#666', marginTop: '0.25rem' }}>
540
  {viewMode === '3d' && 'Interactive 3D exploration of model relationships'}
541
  {viewMode === 'scatter' && '2D projection showing model similarity'}
542
  {viewMode === 'network' && 'Network graph of model connections'}
543
- {viewMode === 'histogram' && 'Distribution analysis of model attributes'}
544
- {viewMode === 'paper-plots' && 'Interactive visualizations from the research paper'}
 
545
  </div>
546
  </label>
547
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
548
  <label style={{ marginBottom: '1rem', display: 'block' }}>
549
  <span style={{ fontWeight: '500', display: 'block', marginBottom: '0.5rem' }}>Color Encoding</span>
550
  <select
551
  value={colorBy}
552
- onChange={(e) => setColorBy(e.target.value)}
553
  style={{ width: '100%', padding: '0.5rem', borderRadius: '4px', border: '1px solid #d0d0d0' }}
554
  >
555
  <option value="library_name">Library (e.g., transformers, diffusers)</option>
@@ -601,7 +852,7 @@ function App() {
601
  <span style={{ fontWeight: '500', display: 'block', marginBottom: '0.5rem' }}>Size Encoding</span>
602
  <select
603
  value={sizeBy}
604
- onChange={(e) => setSizeBy(e.target.value)}
605
  style={{ width: '100%', padding: '0.5rem', borderRadius: '4px', border: '1px solid #d0d0d0' }}
606
  >
607
  <option value="downloads">Downloads (larger = more popular)</option>
@@ -633,8 +884,15 @@ function App() {
633
  </div>
634
 
635
  {/* View Modes */}
636
- <div className="sidebar-section" style={{ background: '#f0f7ff', borderColor: '#b3d9ff' }}>
637
- <h3>View Modes</h3>
 
 
 
 
 
 
 
638
 
639
  <label style={{ marginBottom: '1rem', display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
640
  <input
@@ -671,6 +929,44 @@ function App() {
671
  </div>
672
  </label>
673
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
674
  {semanticSimilarityMode && (
675
  <div style={{ marginTop: '1rem', padding: '0.75rem', background: 'white', borderRadius: '4px', border: '1px solid #d0d0d0' }}>
676
  <label style={{ fontWeight: '500', display: 'block', marginBottom: '0.5rem' }}>
@@ -710,8 +1006,15 @@ function App() {
710
 
711
  {/* Structural Visualization Options */}
712
  {viewMode === '3d' && (
713
- <div className="sidebar-section" style={{ background: '#f0f8f0', borderColor: '#90ee90' }}>
714
- <h3>🔗 Structural Visualization</h3>
 
 
 
 
 
 
 
715
  <div style={{ fontSize: '0.75rem', color: '#666', marginBottom: '1rem', lineHeight: '1.4' }}>
716
  Explore relationships and structure in the model ecosystem
717
  </div>
@@ -782,7 +1085,14 @@ function App() {
782
 
783
  {/* Quick Filters */}
784
  <div className="sidebar-section">
785
- <h3>Quick Filters</h3>
 
 
 
 
 
 
 
786
  <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
787
  <button
788
  onClick={() => {
@@ -827,7 +1137,101 @@ function App() {
827
  </div>
828
 
829
  <div className="sidebar-section">
830
- <h3>Family Tree Explorer</h3>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
831
  <div style={{ position: 'relative' }}>
832
  <input
833
  type="text"
@@ -987,25 +1391,6 @@ function App() {
987
  </div>
988
  )}
989
 
990
- {viewMode === 'histogram' && (
991
- <div style={{ marginBottom: '1.5rem', padding: '1rem', background: '#f9f9f9', borderRadius: '4px', border: '1px solid #e0e0e0' }}>
992
- <label style={{ display: 'block' }}>
993
- <span style={{ fontWeight: '500', display: 'block', marginBottom: '0.5rem' }}>Histogram Attribute</span>
994
- <select
995
- value={histogramAttribute}
996
- onChange={(e) => setHistogramAttribute(e.target.value as any)}
997
- style={{ width: '100%', padding: '0.5rem', borderRadius: '4px', border: '1px solid #d0d0d0' }}
998
- >
999
- <option value="downloads">Downloads</option>
1000
- <option value="likes">Likes</option>
1001
- <option value="trending_score">Trending Score</option>
1002
- </select>
1003
- <div style={{ fontSize: '0.75rem', color: '#666', marginTop: '0.25rem' }}>
1004
- Distribution of {histogramAttribute.replace('_', ' ')} across all models
1005
- </div>
1006
- </label>
1007
- </div>
1008
- )}
1009
 
1010
  {selectedModels.length > 0 && (
1011
  <div style={{ marginTop: '1rem', padding: '0.5rem', background: '#e3f2fd', borderRadius: '4px' }}>
@@ -1033,7 +1418,6 @@ function App() {
1033
  <div
1034
  style={{ flex: 1, position: 'relative' }}
1035
  onMouseMove={(e) => {
1036
- // Update tooltip position when mouse moves
1037
  setTooltipPosition({ x: e.clientX, y: e.clientY });
1038
  }}
1039
  onMouseLeave={() => {
@@ -1051,6 +1435,10 @@ function App() {
1051
  sizeBy={sizeBy}
1052
  colorScheme={colorScheme}
1053
  showLegend={showLegend}
 
 
 
 
1054
  showNetworkEdges={showNetworkEdges}
1055
  showStructuralGroups={showStructuralGroups}
1056
  overviewMode={overviewMode}
@@ -1059,7 +1447,8 @@ function App() {
1059
  setSelectedModel(model);
1060
  setIsModalOpen(true);
1061
  }}
1062
- selectedModelId={familyTreeModelId}
 
1063
  onViewChange={setViewCenter}
1064
  targetViewCenter={viewCenter}
1065
  onHover={(model, pointer) => {
@@ -1070,6 +1459,8 @@ function App() {
1070
  setTooltipPosition(null);
1071
  }
1072
  }}
 
 
1073
  />
1074
  </Suspense>
1075
  <ModelTooltip
@@ -1086,7 +1477,7 @@ function App() {
1086
  borderRadius: '2px',
1087
  border: '1px solid #d0d0d0',
1088
  fontSize: '10px',
1089
- fontFamily: "'Vend Sans', sans-serif"
1090
  }}>
1091
  <h4 style={{ marginTop: 0, marginBottom: '0.5rem', fontSize: '11px', fontWeight: '600' }}>UV Projection</h4>
1092
  <p style={{ margin: 0, lineHeight: '1.3', color: '#666', fontSize: '9px' }}>
@@ -1099,11 +1490,11 @@ function App() {
1099
  data={data}
1100
  familyTree={familyTree.length > 0 ? familyTree : undefined}
1101
  colorBy={colorBy}
1102
- onRegionSelect={(center) => {
1103
  setViewCenter(center);
1104
  // Camera will automatically animate to this position via targetViewCenter prop
1105
  }}
1106
- selectedModelId={familyTreeModelId}
1107
  currentViewCenter={viewCenter}
1108
  />
1109
  {viewCenter && (
@@ -1114,7 +1505,7 @@ function App() {
1114
  borderRadius: '2px',
1115
  border: '1px solid #d0d0d0',
1116
  fontSize: '10px',
1117
- fontFamily: "'Vend Sans', sans-serif"
1118
  }}>
1119
  <strong style={{ fontSize: '10px' }}>View Center:</strong>
1120
  <div style={{ fontSize: '9px', marginTop: '0.25rem', color: '#666' }}>
@@ -1154,21 +1545,14 @@ function App() {
1154
  }}
1155
  />
1156
  )}
1157
- {viewMode === 'histogram' && (
1158
- <Histogram
1159
- width={width}
1160
- height={height}
1161
- data={data}
1162
- attribute={histogramAttribute}
1163
- />
1164
  )}
1165
-
1166
- {viewMode === 'paper-plots' && (
1167
- <PaperPlots
1168
- data={data}
1169
- width={width}
1170
- height={height}
1171
- />
1172
  )}
1173
  </>
1174
  )}
 
 
 
 
1
  import React, { useState, useEffect, useCallback, useRef, useMemo, lazy, Suspense } from 'react';
2
+ // Visualizations
3
+ import EnhancedScatterPlot from './components/visualizations/EnhancedScatterPlot';
4
+ import NetworkGraph from './components/visualizations/NetworkGraph';
5
+ import UVProjectionSquare from './components/visualizations/UVProjectionSquare';
6
+ import DistributionView from './components/visualizations/DistributionView';
7
+ import StackedView from './components/visualizations/StackedView';
8
+ import HeatmapView from './components/visualizations/HeatmapView';
9
+ // Controls
10
+ import RandomModelButton from './components/controls/RandomModelButton';
11
+ import ZoomSlider from './components/controls/ZoomSlider';
12
+ import ThemeToggle from './components/controls/ThemeToggle';
13
+ import RenderingStyleSelector from './components/controls/RenderingStyleSelector';
14
+ import VisualizationModeButtons from './components/controls/VisualizationModeButtons';
15
+ import ClusterFilter, { Cluster } from './components/controls/ClusterFilter';
16
+ import NodeDensitySlider from './components/controls/NodeDensitySlider';
17
+ // Modals
18
+ import ModelModal from './components/modals/ModelModal';
19
+ // UI Components
20
+ import LiveModelCount from './components/ui/LiveModelCount';
21
+ import ModelTooltip from './components/ui/ModelTooltip';
22
+ import ErrorBoundary from './components/ui/ErrorBoundary';
23
+ // Types & Utils
24
  import { ModelPoint, Stats, FamilyTree, SearchResult, SimilarModel } from './types';
25
+ import cache, { IndexedDBCache } from './utils/data/indexedDB';
26
  import { debounce } from './utils/debounce';
27
+ import requestManager from './utils/api/requestManager';
28
+ import { useFilterStore, ViewMode, ColorByOption, SizeByOption } from './stores/filterStore';
29
+ import { API_BASE } from './config/api';
30
  import './App.css';
31
 
32
+ const logger = {
33
+ error: (message: string, error?: unknown) => {
34
+ if (process.env.NODE_ENV === 'development') {
35
+ console.error(message, error);
36
+ }
37
+ },
38
+ };
39
 
40
+ const ScatterPlot3D = lazy(() => import('./components/visualizations/ScatterPlot3D'));
41
 
42
  function App() {
43
+ // Filter store state
44
+ const {
45
+ viewMode,
46
+ colorBy,
47
+ sizeBy,
48
+ colorScheme,
49
+ showLabels,
50
+ zoomLevel,
51
+ nodeDensity,
52
+ renderingStyle,
53
+ theme,
54
+ selectedClusters,
55
+ searchQuery,
56
+ minDownloads,
57
+ minLikes,
58
+ setViewMode,
59
+ setColorBy,
60
+ setSizeBy,
61
+ setColorScheme,
62
+ setShowLabels,
63
+ setZoomLevel,
64
+ setNodeDensity,
65
+ setRenderingStyle,
66
+ setSelectedClusters,
67
+ setSearchQuery,
68
+ setMinDownloads,
69
+ setMinLikes,
70
+ getActiveFilterCount,
71
+ resetFilters: resetFilterStore,
72
+ } = useFilterStore();
73
+
74
+ // Initialize theme on mount
75
+ useEffect(() => {
76
+ document.documentElement.setAttribute('data-theme', theme);
77
+ }, [theme]);
78
+
79
  const [data, setData] = useState<ModelPoint[]>([]);
80
  const [filteredCount, setFilteredCount] = useState<number | null>(null);
81
  const [stats, setStats] = useState<Stats | null>(null);
 
84
  const [selectedModel, setSelectedModel] = useState<ModelPoint | null>(null);
85
  const [isModalOpen, setIsModalOpen] = useState(false);
86
  const [selectedModels, setSelectedModels] = useState<ModelPoint[]>([]);
 
 
87
  const [baseModelsOnly, setBaseModelsOnly] = useState(false);
88
  const [semanticSimilarityMode, setSemanticSimilarityMode] = useState(false);
89
  const [semanticQueryModel, setSemanticQueryModel] = useState<string | null>(null);
 
99
  const [comparisonModels, setComparisonModels] = useState<ModelPoint[]>([]);
100
  const [similarModels, setSimilarModels] = useState<SimilarModel[]>([]);
101
  const [showSimilar, setShowSimilar] = useState(false);
 
 
 
 
 
 
 
102
  const [showLegend, setShowLegend] = useState(true);
103
  const [hoveredModel, setHoveredModel] = useState<ModelPoint | null>(null);
104
  const [tooltipPosition, setTooltipPosition] = useState<{ x: number; y: number } | null>(null);
 
108
  const [showStructuralGroups, setShowStructuralGroups] = useState(false);
109
  const [overviewMode, setOverviewMode] = useState(false);
110
  const [networkEdgeType, setNetworkEdgeType] = useState<'library' | 'pipeline' | 'combined'>('combined');
111
+ const [maxHierarchyDepth, setMaxHierarchyDepth] = useState<number | null>(null);
112
+ const [showDistanceHeatmap, setShowDistanceHeatmap] = useState(false);
113
+ const [highlightedPath, setHighlightedPath] = useState<string[]>([]);
114
+ const [useGraphEmbeddings, setUseGraphEmbeddings] = useState(false);
115
+ const [embeddingType, setEmbeddingType] = useState<string>('text-only');
116
+ const [clusters, setClusters] = useState<Cluster[]>([]);
117
+ const [clustersLoading, setClustersLoading] = useState(false);
118
 
119
+ const activeFilterCount = getActiveFilterCount();
 
 
120
 
121
  const resetFilters = useCallback(() => {
122
+ resetFilterStore();
123
  setMinDownloads(0);
124
  setMinLikes(0);
125
  setSearchQuery('');
126
+ }, [resetFilterStore, setMinDownloads, setMinLikes, setSearchQuery]);
127
 
128
  const [width, setWidth] = useState(window.innerWidth * 0.7);
129
  const [height, setHeight] = useState(window.innerHeight * 0.7);
 
156
  });
157
 
158
  const cachedModels = await cache.getCachedModels(cacheKey);
159
+ if (cachedModels && cachedModels.length > 0) {
160
  setData(cachedModels);
 
161
  setFilteredCount(cachedModels.length);
162
  setLoading(false);
163
+ // Fetch in background to update cache if stale
164
+ setTimeout(() => {
165
+ fetchData();
166
+ }, 100);
167
  return;
168
  }
169
  let models: ModelPoint[];
 
182
  if (!response.ok) throw new Error('Failed to fetch similar models');
183
  const result = await response.json();
184
  models = result.models || [];
 
185
  count = models.length;
186
  } else {
187
  const params = new URLSearchParams({
 
191
  size_by: sizeBy,
192
  projection_method: projectionMethod,
193
  base_models_only: baseModelsOnly.toString(),
194
+ use_graph_embeddings: useGraphEmbeddings.toString(),
195
  });
196
  if (searchQuery) {
197
  params.append('search_query', searchQuery);
198
  }
199
 
 
 
 
200
  params.append('max_points', '500000');
201
 
202
  const url = `${API_BASE}/api/models?${params}`;
 
204
  if (!response.ok) throw new Error('Failed to fetch models');
205
  const result = await response.json();
206
 
 
207
  if (Array.isArray(result)) {
208
  models = result;
209
  count = models.length;
210
+ setEmbeddingType('text-only');
211
  } else {
212
  models = result.models || [];
213
  count = result.filtered_count ?? models.length;
214
+ setEmbeddingType(result.embedding_type || 'text-only');
215
  }
216
  }
217
 
 
221
  setFilteredCount(count);
222
  } catch (err: any) {
223
  if (err.name !== 'AbortError') {
224
+ const errorMessage = err instanceof Error ? err.message : 'Unknown error';
225
+ // Check if it's a connection error (backend not ready)
226
+ if (errorMessage.includes('Failed to fetch') || errorMessage.includes('NetworkError')) {
227
+ setError('Backend is starting up. Please wait... The first load may take a few minutes.');
228
+ } else {
229
+ setError(errorMessage);
230
+ }
231
  }
232
  } finally {
233
  setLoading(false);
234
  fetchDataAbortRef.current = null;
235
  }
236
+ }, [minDownloads, minLikes, searchQuery, colorBy, sizeBy, projectionMethod, baseModelsOnly, semanticSimilarityMode, semanticQueryModel, useGraphEmbeddings, selectedClusters]);
237
 
238
  const debouncedFetchData = useMemo(
239
  () => debounce(fetchData, 300),
 
244
  if (searchQuery) {
245
  debouncedFetchData();
246
  } else {
 
247
  fetchData();
248
  }
249
  return () => {
 
256
  return () => {
257
  debouncedFetchData.cancel();
258
  };
259
+ }, [minDownloads, minLikes, colorBy, sizeBy, baseModelsOnly, projectionMethod, semanticSimilarityMode, semanticQueryModel, useGraphEmbeddings, debouncedFetchData]);
260
 
261
  useEffect(() => {
262
  const fetchStats = async () => {
 
274
  await cache.cacheStats(cacheKey, statsData);
275
  setStats(statsData);
276
  } catch (err) {
277
+ if (err instanceof Error) {
278
+ logger.error('Error fetching stats:', err);
279
+ }
280
  }
281
  };
282
 
283
  fetchStats();
284
  }, []);
285
 
286
+ // Fetch clusters
287
+ useEffect(() => {
288
+ const fetchClusters = async () => {
289
+ setClustersLoading(true);
290
+ try {
291
+ const response = await fetch(`${API_BASE}/api/clusters`);
292
+ if (!response.ok) throw new Error('Failed to fetch clusters');
293
+ const data = await response.json();
294
+ setClusters(data.clusters || []);
295
+ } catch (err) {
296
+ logger.error('Error fetching clusters:', err);
297
+ setClusters([]);
298
+ } finally {
299
+ setClustersLoading(false);
300
+ }
301
+ };
302
+
303
+ fetchClusters();
304
+ }, []);
305
+
306
  // Search models for family tree lookup
307
  const searchModels = useCallback(async (query: string) => {
308
  if (query.length < 1) {
 
317
  setSearchResults(data.results || []);
318
  setShowSearchResults(true);
319
  } catch (err) {
320
+ logger.error('Search error:', err);
321
  setSearchResults([]);
322
  }
323
  }, []);
 
340
  setShowSearchResults(false);
341
  setSearchInput('');
342
  } catch (err) {
343
+ logger.error('Family tree error:', err);
344
  setFamilyTree([]);
345
  setFamilyTreeModelId(null);
346
  }
 
351
  setFamilyTreeModelId(null);
352
  }, []);
353
 
354
+ const loadFamilyPath = useCallback(async (modelId: string, targetId?: string) => {
355
+ try {
356
+ const url = targetId
357
+ ? `${API_BASE}/api/family/path/${encodeURIComponent(modelId)}?target_id=${encodeURIComponent(targetId)}`
358
+ : `${API_BASE}/api/family/path/${encodeURIComponent(modelId)}`;
359
+ const response = await fetch(url);
360
+ if (!response.ok) throw new Error('Failed to load path');
361
+ const data = await response.json();
362
+ setHighlightedPath(data.path || []);
363
+ } catch (err) {
364
+ logger.error('Path loading error:', err);
365
+ setHighlightedPath([]);
366
+ }
367
+ }, []);
368
+
369
  const loadSimilarModels = useCallback(async (modelId: string) => {
370
  try {
371
  const response = await fetch(`${API_BASE}/api/similar/${encodeURIComponent(modelId)}?k=10`);
 
391
  setShowSimilar(true);
392
  } catch (err) {
393
  const errorMessage = err instanceof Error ? err.message : 'Failed to load similar models';
394
+ logger.error('Similar models error:', err);
 
395
  if (errorMessage !== 'Failed to load similar models' || !(err instanceof TypeError && err.message.includes('fetch'))) {
396
  setError(`Similar models: ${errorMessage}`);
 
397
  setTimeout(() => setError(null), 5000);
398
  }
399
  setSimilarModels([]);
 
442
  document.body.removeChild(a);
443
  URL.revokeObjectURL(url);
444
  } catch (err) {
445
+ logger.error('Export error:', err);
446
  alert('Failed to export models');
447
  }
448
  }, []);
 
500
 
501
  <div className="main-content">
502
  <aside className="sidebar">
503
+ <div style={{
504
+ display: 'flex',
505
+ justifyContent: 'space-between',
506
+ alignItems: 'center',
507
+ marginBottom: '1.5rem',
508
+ paddingBottom: '1rem',
509
+ borderBottom: '2px solid #e8e8e8'
510
+ }}>
511
+ <h2 style={{
512
+ margin: 0,
513
+ fontSize: '1.5rem',
514
+ fontWeight: '700',
515
+ background: 'linear-gradient(135deg, #5e35b1 0%, #7b1fa2 100%)',
516
+ WebkitBackgroundClip: 'text',
517
+ WebkitTextFillColor: 'transparent',
518
+ backgroundClip: 'text'
519
+ }}>
520
+ Filters & Controls
521
+ </h2>
522
  {activeFilterCount > 0 && (
523
  <div style={{
524
  fontSize: '0.75rem',
525
+ background: 'linear-gradient(135deg, #5e35b1 0%, #7b1fa2 100%)',
526
  color: 'white',
527
+ padding: '0.4rem 0.75rem',
528
+ borderRadius: '16px',
529
+ fontWeight: '600',
530
+ boxShadow: '0 2px 6px rgba(94, 53, 177, 0.3)'
531
  }}>
532
  {activeFilterCount} active
533
  </div>
 
537
  {/* Filter Results Count */}
538
  {!loading && data.length > 0 && (
539
  <div className="sidebar-section" style={{
540
+ background: 'linear-gradient(135deg, #f3e5f5 0%, #e1bee7 100%)',
541
+ border: '2px solid #ce93d8',
542
+ fontSize: '0.9rem',
543
+ marginBottom: '1.5rem'
544
  }}>
545
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '0.5rem' }}>
546
+ <div>
547
+ <strong style={{ fontSize: '1.1rem', color: '#6a1b9a' }}>
548
+ {data.length.toLocaleString()}
549
+ </strong>
550
+ <span style={{ marginLeft: '0.4rem', color: '#4a148c' }}>
551
+ {data.length === 1 ? 'model' : 'models'}
552
+ </span>
553
+ </div>
554
+ {embeddingType === 'graph-aware' && (
555
+ <span style={{
556
+ fontSize: '0.7rem',
557
+ background: '#7b1fa2',
558
+ color: 'white',
559
+ padding: '0.3rem 0.6rem',
560
+ borderRadius: '12px',
561
+ fontWeight: '600'
562
+ }}>
563
+ 🌐 Graph
564
+ </span>
565
+ )}
566
+ </div>
567
  {filteredCount !== null && filteredCount !== data.length && (
568
+ <div style={{ fontSize: '0.8rem', color: '#6a1b9a', marginTop: '0.25rem' }}>
569
+ of {filteredCount.toLocaleString()} matching
570
  </div>
571
  )}
572
+ {stats && filteredCount !== null && filteredCount < stats.total_models && (
573
+ <div style={{ fontSize: '0.75rem', color: '#8e24aa', marginTop: '0.25rem' }}>
574
+ from {stats.total_models.toLocaleString()} total
 
 
575
  </div>
576
  )}
577
  </div>
 
579
 
580
  {/* Search Section */}
581
  <div className="sidebar-section">
582
+ <h3 style={{
583
+ display: 'flex',
584
+ alignItems: 'center',
585
+ gap: '0.5rem',
586
+ color: '#5e35b1',
587
+ marginBottom: '0.75rem'
588
+ }}>
589
+ 🔍 Search Models
590
+ </h3>
591
  <input
592
  type="text"
593
  value={searchQuery}
 
595
  placeholder="Search by model ID, tags, or keywords..."
596
  style={{ width: '100%' }}
597
  />
598
+ <div style={{ fontSize: '0.75rem', color: '#666', marginTop: '0.5rem', lineHeight: '1.4' }}>
599
+ Search by model name, tags, library, or metadata
600
  </div>
601
  </div>
602
 
603
  {/* Popularity Filters */}
604
  <div className="sidebar-section">
605
+ <h3 style={{
606
+ display: 'flex',
607
+ alignItems: 'center',
608
+ gap: '0.5rem',
609
+ color: '#5e35b1'
610
+ }}>
611
+ 📊 Popularity Filters
612
+ </h3>
613
 
614
  <label style={{ marginBottom: '1rem', display: 'block' }}>
615
  <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
 
658
  </label>
659
  </div>
660
 
661
+ {/* License Filter */}
662
+ {stats && stats.licenses && typeof stats.licenses === 'object' && Object.keys(stats.licenses).length > 0 && (
663
+ <div className="sidebar-section">
664
+ <h3>License Filter</h3>
665
+ <div style={{ maxHeight: '200px', overflowY: 'auto', marginTop: '0.5rem' }}>
666
+ {Object.entries(stats.licenses as Record<string, number>)
667
+ .sort((a, b) => b[1] - a[1]) // Sort by count descending
668
+ .slice(0, 20) // Show top 20 licenses
669
+ .map(([license, count]) => (
670
+ <label
671
+ key={license}
672
+ style={{
673
+ display: 'flex',
674
+ alignItems: 'center',
675
+ gap: '0.5rem',
676
+ marginBottom: '0.5rem',
677
+ cursor: 'pointer',
678
+ fontSize: '0.9rem'
679
+ }}
680
+ >
681
+ <input
682
+ type="checkbox"
683
+ checked={searchQuery.toLowerCase().includes(license.toLowerCase())}
684
+ onChange={(e) => {
685
+ if (e.target.checked) {
686
+ // Add license to search (simple implementation)
687
+ setSearchQuery(searchQuery ? `${searchQuery} ${license}` : license);
688
+ } else {
689
+ // Remove license from search
690
+ setSearchQuery(searchQuery.replace(license, '').trim() || '');
691
+ }
692
+ }}
693
+ />
694
+ <span style={{ flex: 1 }}>{license || 'Unknown'}</span>
695
+ <span style={{ fontSize: '0.75rem', color: '#666' }}>({Number(count).toLocaleString()})</span>
696
+ </label>
697
+ ))}
698
+ </div>
699
+ {Object.keys(stats.licenses).length > 20 && (
700
+ <div style={{ fontSize: '0.75rem', color: '#666', marginTop: '0.5rem' }}>
701
+ Showing top 20 licenses
702
+ </div>
703
+ )}
704
+ </div>
705
+ )}
706
+
707
+ {/* Discovery */}
708
+ <div className="sidebar-section">
709
+ <h3 style={{
710
+ display: 'flex',
711
+ alignItems: 'center',
712
+ gap: '0.5rem',
713
+ color: '#5e35b1'
714
+ }}>
715
+ 🎲 Discovery
716
+ </h3>
717
+ <RandomModelButton
718
+ data={data}
719
+ onSelect={(model: ModelPoint) => {
720
+ setSelectedModel(model);
721
+ setIsModalOpen(true);
722
+ }}
723
+ disabled={loading || data.length === 0}
724
+ />
725
+ </div>
726
+
727
  {/* Visualization Options */}
728
  <div className="sidebar-section">
729
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '1rem' }}>
730
+ <h3 style={{
731
+ margin: 0,
732
+ display: 'flex',
733
+ alignItems: 'center',
734
+ gap: '0.5rem',
735
+ color: '#5e35b1'
736
+ }}>
737
+ 🎨 Visualization
738
+ </h3>
739
+ <ThemeToggle />
740
+ </div>
741
 
742
  <label style={{ marginBottom: '1rem', display: 'block' }}>
743
  <span style={{ fontWeight: '500', display: 'block', marginBottom: '0.5rem' }}>View Mode</span>
744
  <select
745
  value={viewMode}
746
+ onChange={(e) => setViewMode(e.target.value as ViewMode)}
747
  style={{ width: '100%', padding: '0.5rem', borderRadius: '4px', border: '1px solid #d0d0d0' }}
748
  >
749
  <option value="3d">3D Latent Space</option>
750
  <option value="scatter">2D Latent Space</option>
751
  <option value="network">Network Graph</option>
752
+ <option value="distribution">Distribution</option>
753
+ <option value="stacked">Stacked</option>
754
+ <option value="heatmap">Heatmap</option>
755
  </select>
756
  <div style={{ fontSize: '0.75rem', color: '#666', marginTop: '0.25rem' }}>
757
  {viewMode === '3d' && 'Interactive 3D exploration of model relationships'}
758
  {viewMode === 'scatter' && '2D projection showing model similarity'}
759
  {viewMode === 'network' && 'Network graph of model connections'}
760
+ {viewMode === 'distribution' && 'Statistical distributions of model properties'}
761
+ {viewMode === 'stacked' && 'Hierarchical view of model families'}
762
+ {viewMode === 'heatmap' && 'Density heatmap in latent space'}
763
  </div>
764
  </label>
765
 
766
+ {/* Rendering Style Selector for 3D View */}
767
+ {viewMode === '3d' && (
768
+ <div style={{ marginBottom: '1rem' }}>
769
+ <RenderingStyleSelector />
770
+ </div>
771
+ )}
772
+
773
+ {/* Zoom and Label Controls for 3D View */}
774
+ {viewMode === '3d' && (
775
+ <>
776
+ <ZoomSlider
777
+ value={zoomLevel}
778
+ onChange={setZoomLevel}
779
+ min={0.1}
780
+ max={5}
781
+ step={0.1}
782
+ disabled={loading}
783
+ />
784
+ <NodeDensitySlider disabled={loading} />
785
+ <div className="label-toggle">
786
+ <span className="label-toggle-label">Show Labels</span>
787
+ <label className="label-toggle-switch">
788
+ <input
789
+ type="checkbox"
790
+ checked={showLabels}
791
+ onChange={(e) => setShowLabels(e.target.checked)}
792
+ />
793
+ <span className="label-toggle-slider"></span>
794
+ </label>
795
+ </div>
796
+ </>
797
+ )}
798
+
799
  <label style={{ marginBottom: '1rem', display: 'block' }}>
800
  <span style={{ fontWeight: '500', display: 'block', marginBottom: '0.5rem' }}>Color Encoding</span>
801
  <select
802
  value={colorBy}
803
+ onChange={(e) => setColorBy(e.target.value as ColorByOption)}
804
  style={{ width: '100%', padding: '0.5rem', borderRadius: '4px', border: '1px solid #d0d0d0' }}
805
  >
806
  <option value="library_name">Library (e.g., transformers, diffusers)</option>
 
852
  <span style={{ fontWeight: '500', display: 'block', marginBottom: '0.5rem' }}>Size Encoding</span>
853
  <select
854
  value={sizeBy}
855
+ onChange={(e) => setSizeBy(e.target.value as SizeByOption)}
856
  style={{ width: '100%', padding: '0.5rem', borderRadius: '4px', border: '1px solid #d0d0d0' }}
857
  >
858
  <option value="downloads">Downloads (larger = more popular)</option>
 
884
  </div>
885
 
886
  {/* View Modes */}
887
+ <div className="sidebar-section" style={{ background: 'linear-gradient(135deg, #f3e5f5 0%, #fce4ec 100%)', border: '2px solid #f48fb1' }}>
888
+ <h3 style={{
889
+ display: 'flex',
890
+ alignItems: 'center',
891
+ gap: '0.5rem',
892
+ color: '#5e35b1'
893
+ }}>
894
+ ⚡ View Modes
895
+ </h3>
896
 
897
  <label style={{ marginBottom: '1rem', display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
898
  <input
 
929
  </div>
930
  </label>
931
 
932
+ <label style={{ marginBottom: '1rem', display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
933
+ <input
934
+ type="checkbox"
935
+ checked={useGraphEmbeddings}
936
+ onChange={(e) => setUseGraphEmbeddings(e.target.checked)}
937
+ style={{ marginRight: '0.5rem', cursor: 'pointer' }}
938
+ />
939
+ <div>
940
+ <span style={{ fontWeight: '500' }}>🌐 Graph-Aware Embeddings</span>
941
+ <div style={{ fontSize: '0.75rem', color: '#666', marginTop: '0.25rem' }}>
942
+ Use embeddings that respect family tree structure. Models in the same family will be closer together.
943
+ </div>
944
+ </div>
945
+ </label>
946
+
947
+ {embeddingType && (
948
+ <div style={{
949
+ marginTop: '0.5rem',
950
+ padding: '0.75rem',
951
+ background: embeddingType === 'graph-aware' ? '#e8f5e9' : '#f5f5f5',
952
+ border: `1px solid ${embeddingType === 'graph-aware' ? '#4caf50' : '#d0d0d0'}`,
953
+ borderRadius: '4px',
954
+ fontSize: '0.75rem',
955
+ color: '#666'
956
+ }}>
957
+ <div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
958
+ <strong style={{ color: embeddingType === 'graph-aware' ? '#2e7d32' : '#666' }}>
959
+ {embeddingType === 'graph-aware' ? '🌐 Graph-Aware' : '📝 Text-Only'} Embeddings
960
+ </strong>
961
+ </div>
962
+ <div style={{ fontSize: '0.7rem', color: '#888', lineHeight: '1.4' }}>
963
+ {embeddingType === 'graph-aware'
964
+ ? 'Models in the same family tree are positioned closer together, revealing hierarchical relationships.'
965
+ : 'Standard text-based embeddings showing semantic similarity from model descriptions and tags.'}
966
+ </div>
967
+ </div>
968
+ )}
969
+
970
  {semanticSimilarityMode && (
971
  <div style={{ marginTop: '1rem', padding: '0.75rem', background: 'white', borderRadius: '4px', border: '1px solid #d0d0d0' }}>
972
  <label style={{ fontWeight: '500', display: 'block', marginBottom: '0.5rem' }}>
 
1006
 
1007
  {/* Structural Visualization Options */}
1008
  {viewMode === '3d' && (
1009
+ <div className="sidebar-section" style={{ background: 'linear-gradient(135deg, #e8f5e9 0%, #f1f8e9 100%)', border: '2px solid #aed581' }}>
1010
+ <h3 style={{
1011
+ display: 'flex',
1012
+ alignItems: 'center',
1013
+ gap: '0.5rem',
1014
+ color: '#5e35b1'
1015
+ }}>
1016
+ 🔗 Network Structure
1017
+ </h3>
1018
  <div style={{ fontSize: '0.75rem', color: '#666', marginBottom: '1rem', lineHeight: '1.4' }}>
1019
  Explore relationships and structure in the model ecosystem
1020
  </div>
 
1085
 
1086
  {/* Quick Filters */}
1087
  <div className="sidebar-section">
1088
+ <h3 style={{
1089
+ display: 'flex',
1090
+ alignItems: 'center',
1091
+ gap: '0.5rem',
1092
+ color: '#5e35b1'
1093
+ }}>
1094
+ ⚡ Quick Actions
1095
+ </h3>
1096
  <div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
1097
  <button
1098
  onClick={() => {
 
1137
  </div>
1138
 
1139
  <div className="sidebar-section">
1140
+ <h3 style={{
1141
+ display: 'flex',
1142
+ alignItems: 'center',
1143
+ gap: '0.5rem',
1144
+ color: '#5e35b1'
1145
+ }}>
1146
+ 🌳 Hierarchy Navigation
1147
+ </h3>
1148
+ <label style={{ marginBottom: '1rem', display: 'block' }}>
1149
+ <span style={{ fontWeight: '500', display: 'block', marginBottom: '0.5rem' }}>
1150
+ Max Hierarchy Depth
1151
+ </span>
1152
+ <input
1153
+ type="range"
1154
+ min="0"
1155
+ max="10"
1156
+ value={maxHierarchyDepth ?? 10}
1157
+ onChange={(e) => {
1158
+ const val = parseInt(e.target.value);
1159
+ setMaxHierarchyDepth(val === 10 ? null : val);
1160
+ }}
1161
+ style={{ width: '100%', marginTop: '0.5rem' }}
1162
+ />
1163
+ <div style={{ fontSize: '0.75rem', color: '#666', marginTop: '0.25rem', display: 'flex', justifyContent: 'space-between' }}>
1164
+ <span>All levels</span>
1165
+ <span>{maxHierarchyDepth !== null ? `Depth ≤ ${maxHierarchyDepth}` : 'No limit'}</span>
1166
+ </div>
1167
+ </label>
1168
+ <label style={{ marginBottom: '1rem', display: 'flex', alignItems: 'center', gap: '0.5rem' }}>
1169
+ <input
1170
+ type="checkbox"
1171
+ checked={showDistanceHeatmap}
1172
+ onChange={(e) => setShowDistanceHeatmap(e.target.checked)}
1173
+ />
1174
+ <span style={{ fontSize: '0.9rem' }}>Show Distance Heatmap</span>
1175
+ </label>
1176
+ {selectedModel && (
1177
+ <div style={{ marginTop: '0.5rem', padding: '0.5rem', background: '#f5f5f5', borderRadius: '4px', fontSize: '0.85rem' }}>
1178
+ <div style={{ fontWeight: '500', marginBottom: '0.25rem' }}>Selected Model:</div>
1179
+ <div style={{ color: '#666', marginBottom: '0.5rem', wordBreak: 'break-word' }}>{selectedModel.model_id}</div>
1180
+ {selectedModel.family_depth !== null && (
1181
+ <div style={{ color: '#666', marginBottom: '0.5rem' }}>
1182
+ Hierarchy Depth: {selectedModel.family_depth}
1183
+ </div>
1184
+ )}
1185
+ <button
1186
+ onClick={() => {
1187
+ if (selectedModel.parent_model) {
1188
+ loadFamilyPath(selectedModel.model_id, selectedModel.parent_model);
1189
+ } else {
1190
+ loadFamilyPath(selectedModel.model_id);
1191
+ }
1192
+ }}
1193
+ style={{
1194
+ padding: '0.25rem 0.5rem',
1195
+ fontSize: '0.8rem',
1196
+ background: '#4a90e2',
1197
+ color: 'white',
1198
+ border: 'none',
1199
+ borderRadius: '2px',
1200
+ cursor: 'pointer',
1201
+ marginRight: '0.5rem',
1202
+ marginBottom: '0.5rem'
1203
+ }}
1204
+ >
1205
+ Show Path to Root
1206
+ </button>
1207
+ <button
1208
+ onClick={() => setHighlightedPath([])}
1209
+ style={{
1210
+ padding: '0.25rem 0.5rem',
1211
+ fontSize: '0.8rem',
1212
+ background: '#6a6a6a',
1213
+ color: 'white',
1214
+ border: 'none',
1215
+ borderRadius: '2px',
1216
+ cursor: 'pointer',
1217
+ marginBottom: '0.5rem'
1218
+ }}
1219
+ >
1220
+ Clear Path
1221
+ </button>
1222
+ </div>
1223
+ )}
1224
+ </div>
1225
+
1226
+ <div className="sidebar-section">
1227
+ <h3 style={{
1228
+ display: 'flex',
1229
+ alignItems: 'center',
1230
+ gap: '0.5rem',
1231
+ color: '#5e35b1'
1232
+ }}>
1233
+ 👥 Family Tree Explorer
1234
+ </h3>
1235
  <div style={{ position: 'relative' }}>
1236
  <input
1237
  type="text"
 
1391
  </div>
1392
  )}
1393
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1394
 
1395
  {selectedModels.length > 0 && (
1396
  <div style={{ marginTop: '1rem', padding: '0.5rem', background: '#e3f2fd', borderRadius: '4px' }}>
 
1418
  <div
1419
  style={{ flex: 1, position: 'relative' }}
1420
  onMouseMove={(e) => {
 
1421
  setTooltipPosition({ x: e.clientX, y: e.clientY });
1422
  }}
1423
  onMouseLeave={() => {
 
1435
  sizeBy={sizeBy}
1436
  colorScheme={colorScheme}
1437
  showLegend={showLegend}
1438
+ showLabels={showLabels}
1439
+ zoomLevel={zoomLevel}
1440
+ nodeDensity={nodeDensity}
1441
+ renderingStyle={renderingStyle}
1442
  showNetworkEdges={showNetworkEdges}
1443
  showStructuralGroups={showStructuralGroups}
1444
  overviewMode={overviewMode}
 
1447
  setSelectedModel(model);
1448
  setIsModalOpen(true);
1449
  }}
1450
+ selectedModelId={selectedModel?.model_id || familyTreeModelId}
1451
+ selectedModel={selectedModel}
1452
  onViewChange={setViewCenter}
1453
  targetViewCenter={viewCenter}
1454
  onHover={(model, pointer) => {
 
1459
  setTooltipPosition(null);
1460
  }
1461
  }}
1462
+ highlightedPath={highlightedPath}
1463
+ showDistanceHeatmap={showDistanceHeatmap && !!selectedModel}
1464
  />
1465
  </Suspense>
1466
  <ModelTooltip
 
1477
  borderRadius: '2px',
1478
  border: '1px solid #d0d0d0',
1479
  fontSize: '10px',
1480
+ fontFamily: "'Instrument Sans', sans-serif"
1481
  }}>
1482
  <h4 style={{ marginTop: 0, marginBottom: '0.5rem', fontSize: '11px', fontWeight: '600' }}>UV Projection</h4>
1483
  <p style={{ margin: 0, lineHeight: '1.3', color: '#666', fontSize: '9px' }}>
 
1490
  data={data}
1491
  familyTree={familyTree.length > 0 ? familyTree : undefined}
1492
  colorBy={colorBy}
1493
+ onRegionSelect={(center: { x: number; y: number; z: number }) => {
1494
  setViewCenter(center);
1495
  // Camera will automatically animate to this position via targetViewCenter prop
1496
  }}
1497
+ selectedModelId={selectedModel?.model_id || familyTreeModelId}
1498
  currentViewCenter={viewCenter}
1499
  />
1500
  {viewCenter && (
 
1505
  borderRadius: '2px',
1506
  border: '1px solid #d0d0d0',
1507
  fontSize: '10px',
1508
+ fontFamily: "'Instrument Sans', sans-serif"
1509
  }}>
1510
  <strong style={{ fontSize: '10px' }}>View Center:</strong>
1511
  <div style={{ fontSize: '9px', marginTop: '0.25rem', color: '#666' }}>
 
1545
  }}
1546
  />
1547
  )}
1548
+ {viewMode === 'distribution' && (
1549
+ <DistributionView data={data} width={width} height={height} />
 
 
 
 
 
1550
  )}
1551
+ {viewMode === 'stacked' && (
1552
+ <StackedView data={data} width={width} height={height} />
1553
+ )}
1554
+ {viewMode === 'heatmap' && (
1555
+ <HeatmapView data={data} width={width} height={height} />
 
 
1556
  )}
1557
  </>
1558
  )}