Improve dashboard UX with cohesive design
Browse filesAdd emoji icons, purple theme, better section organization
- frontend/src/App.css +13 -6
- 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:
|
| 169 |
-
color: #
|
| 170 |
-
margin: 0 0
|
| 171 |
-
|
| 172 |
-
|
| 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 |
-
|
| 6 |
-
import
|
| 7 |
-
import
|
| 8 |
-
import UVProjectionSquare from './components/UVProjectionSquare';
|
| 9 |
-
import
|
| 10 |
-
import
|
| 11 |
-
import
|
| 12 |
-
|
| 13 |
-
import
|
| 14 |
-
import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
-
const
|
| 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 = (
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 243 |
setSearchResults([]);
|
| 244 |
}
|
| 245 |
}, []);
|
|
@@ -262,7 +340,7 @@ function App() {
|
|
| 262 |
setShowSearchResults(false);
|
| 263 |
setSearchInput('');
|
| 264 |
} catch (err) {
|
| 265 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
{
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 417 |
{activeFilterCount > 0 && (
|
| 418 |
<div style={{
|
| 419 |
fontSize: '0.75rem',
|
| 420 |
-
background: '#
|
| 421 |
color: 'white',
|
| 422 |
-
padding: '0.
|
| 423 |
-
borderRadius: '
|
| 424 |
-
fontWeight: '
|
|
|
|
| 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: '#
|
| 435 |
-
|
| 436 |
-
fontSize: '0.9rem'
|
|
|
|
| 437 |
}}>
|
| 438 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 439 |
{filteredCount !== null && filteredCount !== data.length && (
|
| 440 |
-
<div style={{ fontSize: '0.8rem', color: '#
|
| 441 |
-
of {filteredCount.toLocaleString()} matching
|
| 442 |
</div>
|
| 443 |
)}
|
| 444 |
-
{stats && filteredCount !== null && (
|
| 445 |
-
<div style={{ fontSize: '0.
|
| 446 |
-
{
|
| 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 |
-
<
|
| 457 |
-
|
| 458 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 467 |
-
|
| 468 |
</div>
|
| 469 |
</div>
|
| 470 |
|
| 471 |
{/* Popularity Filters */}
|
| 472 |
<div className="sidebar-section">
|
| 473 |
-
<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 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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="
|
| 537 |
-
<option value="
|
|
|
|
| 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 === '
|
| 544 |
-
{viewMode === '
|
|
|
|
| 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: '#
|
| 637 |
-
<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: '#
|
| 714 |
-
<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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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: "'
|
| 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 |
-
|
| 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: "'
|
| 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 === '
|
| 1158 |
-
<
|
| 1159 |
-
width={width}
|
| 1160 |
-
height={height}
|
| 1161 |
-
data={data}
|
| 1162 |
-
attribute={histogramAttribute}
|
| 1163 |
-
/>
|
| 1164 |
)}
|
| 1165 |
-
|
| 1166 |
-
|
| 1167 |
-
|
| 1168 |
-
|
| 1169 |
-
|
| 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 |
)}
|