Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
cfb73c4
feat(api): add GET /specs/map endpoint
MarkusNeusinger Apr 30, 2026
47cf1bc
feat(app): add MapPage with force-directed similarity graph
MarkusNeusinger Apr 30, 2026
ac8019a
feat(app): wire /map route + nav link
MarkusNeusinger Apr 30, 2026
004b447
test(app): cover MapPage canvas/click/hover callbacks
MarkusNeusinger Apr 30, 2026
e88ad99
fix(app): drop crossOrigin on map thumbnails to avoid GCS CORS preflight
MarkusNeusinger Apr 30, 2026
c2d72d1
perf(app): use _400.webp variants for map thumbnails
MarkusNeusinger Apr 30, 2026
c68f153
perf(app): switch map thumbnails to _800.webp variant
MarkusNeusinger Apr 30, 2026
ca8bad5
perf(app): progressive thumbnail loading (400→800→1200) on zoom
MarkusNeusinger Apr 30, 2026
5405c88
fix(app): respect plot aspect ratio (16:9) when drawing map nodes
MarkusNeusinger Apr 30, 2026
7be2269
feat(app): cluster gravity + hover preview for the spec map
MarkusNeusinger Apr 30, 2026
010707d
feat(app): bigger map nodes + cluster colors + subdued links
MarkusNeusinger Apr 30, 2026
66e92e3
feat(app): plot_type-weighted similarity + drop ring layout
MarkusNeusinger Apr 30, 2026
93750a6
feat(app): legend with cluster filter + restore link visibility
MarkusNeusinger Apr 30, 2026
3c46a64
feat(app): per-category weight sliders for live similarity tuning
MarkusNeusinger Apr 30, 2026
3ffdd86
feat(app): plot_type-only default + slider max bumped to 5
MarkusNeusinger Apr 30, 2026
db24af7
fix(app): zero out IDF for tags in >67% of corpus + raise KNN_MIN_SIM
MarkusNeusinger Apr 30, 2026
17659e8
feat(app): legend + colors follow the highest-weighted category
MarkusNeusinger Apr 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ def clear_spec_cache(spec_id: str) -> int:
count += clear_cache_by_pattern(f"spec:{spec_id}")
count += clear_cache_by_pattern(f"spec_images:{spec_id}")
count += clear_cache_by_pattern("specs_list") # List might have changed
count += clear_cache_by_pattern("specs_map") # Map page payload might have changed
count += clear_cache_by_pattern("filter:") # Filters might be affected
count += clear_cache_by_pattern("stats") # Stats might have changed
count += clear_cache_by_pattern("sitemap") # Sitemap includes spec URLs
Expand Down
48 changes: 47 additions & 1 deletion api/routers/specs.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from api.cache import cache_key, get_or_set_cache
from api.dependencies import require_db
from api.exceptions import raise_not_found
from api.schemas import ImplementationResponse, SpecDetailResponse, SpecListItem
from api.schemas import ImplementationResponse, SpecDetailResponse, SpecListItem, SpecMapItem
from core.config import settings
from core.database import ImplRepository, SpecRepository
from core.database.connection import get_db_context
Expand All @@ -28,6 +28,33 @@ async def _build_specs_list(db: AsyncSession) -> list[SpecListItem]:
]


async def _build_specs_map(db: AsyncSession) -> list[SpecMapItem]:
"""One row per spec with its best-rated impl image + spec/impl tag bag for the /map page.

Best-impl tiebreak: highest quality_score, then lexicographic library_id for determinism.
Specs without any implementations are skipped (mirrors _build_specs_list).
"""
repo = SpecRepository(db)
specs = await repo.get_all()
items: list[SpecMapItem] = []
for spec in specs:
if not spec.impls:
continue
best = max(spec.impls, key=lambda i: ((i.quality_score or 0.0), i.library_id))
items.append(
SpecMapItem(
id=spec.id,
title=spec.title,
preview_url_light=best.preview_url_light,
preview_url_dark=best.preview_url_dark,
quality_score=best.quality_score,
tags=spec.tags,
impl_tags=best.impl_tags,
)
)
return items


async def _build_spec_detail(db: AsyncSession, spec_id: str) -> SpecDetailResponse:
repo = SpecRepository(db)
spec = await repo.get_by_id(spec_id)
Expand Down Expand Up @@ -125,6 +152,25 @@ async def _refresh() -> list[SpecListItem]:
)


@router.get("/specs/map", response_model=list[SpecMapItem])
async def get_specs_map(db: AsyncSession = Depends(require_db)):
"""Get one row per spec (best-impl image + tag bag) for the /map clustering page.

NOTE: must stay declared before /specs/{spec_id} so the path-parameter route doesn't capture "map".
"""

async def _fetch() -> list[SpecMapItem]:
return await _build_specs_map(db)

async def _refresh() -> list[SpecMapItem]:
async with get_db_context() as fresh_db:
return await _build_specs_map(fresh_db)

return await get_or_set_cache(
cache_key("specs_map"), _fetch, refresh_after=settings.cache_refresh_after, refresh_factory=_refresh
)


@router.get("/specs/{spec_id}", response_model=SpecDetailResponse)
async def get_spec(spec_id: str, db: AsyncSession = Depends(require_db)):
"""Get detailed spec information including all implementations."""
Expand Down
12 changes: 12 additions & 0 deletions api/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,18 @@ class SpecListItem(BaseModel):
library_count: int = 0


class SpecMapItem(BaseModel):
"""One row per spec for the /map page: best-impl preview + full tag bag for client-side similarity clustering."""

id: str
title: str
preview_url_light: str | None = None
preview_url_dark: str | None = None
quality_score: float | None = None
tags: dict[str, Any] | None = None
impl_tags: dict[str, Any] | None = None
Comment on lines +72 to +81
Comment on lines +72 to +81


class ImageResponse(BaseModel):
"""Image/plot response for grid display."""

Expand Down
2 changes: 2 additions & 0 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^9.0.0",
"@mui/material": "^9.0.0",
"force-graph": "^1.51.4",
"fuse.js": "^7.3.0",
"react": "^19.2.5",
"react-dom": "^19.2.5",
"react-force-graph-2d": "^1.29.1",
"react-helmet-async": "^3.0.0",
"react-router-dom": "^7.14.2",
"react-syntax-highlighter": "^16.1.1",
Expand Down
1 change: 1 addition & 0 deletions app/src/components/NavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const DEBUG_CLICK_WINDOW_MS = 800;
const NAV_LINKS: { label: string; to: string; short?: string }[] = [
{ label: 'specs', to: '/specs' },
{ label: 'plots', to: '/plots' },
{ label: 'map', to: '/map' },
{ label: 'libraries', to: '/libraries', short: 'libs' },
{ label: 'stats', to: '/stats' },
{ label: 'palette', to: '/palette', short: 'pal' },
Expand Down
Loading
Loading