feat: overview mode, expanded sitemap, and UI improvements#3168
feat: overview mode, expanded sitemap, and UI improvements#3168MarkusNeusinger merged 3 commits intomainfrom
Conversation
- Implemented strip_noqa_comments utility to clean code display - Updated SpecTabs component to support collapsible tabs - Enhanced rendering of specifications with improved styling - Added comprehensive tests for strip_noqa_comments utility
- Add overview/detail mode navigation for spec pages - /scatter-basic → overview grid showing all implementations - /scatter-basic/matplotlib → detail view with carousel - Add action buttons (copy code, download, interactive) to overview grid - Add library tooltip with description and doc URL in overview - Fix interactive page postMessage origin for localhost development - Update catalog links to go to overview page - Expand sitemap to include all implementation URLs (~1500 pages) - Fix spacing consistency between pages - Add ClickAwayListener to close tooltips on outside click 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
This PR implements a dual-mode navigation system for spec pages (overview grid vs. detail carousel), adds interactive action buttons on hover, improves library tooltips, fixes the interactive page postMessage origin for localhost, expands the sitemap to include all implementation URLs, and introduces a utility to strip noqa comments from displayed code.
Key Changes:
- New URL structure:
/{spec_id}shows grid of all implementations,/{spec_id}/{library}shows detail view with carousel - Action buttons (copy, download, interactive) appear on hover in overview mode
- Sitemap now includes ~1500 pages (overview + all implementation URLs)
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
core/utils.py |
New utility function to strip noqa comments from code using regex |
api/routers/specs.py |
Apply noqa stripping to code responses |
api/routers/libraries.py |
Apply noqa stripping to library image code |
api/routers/seo.py |
Expand sitemap to include implementation URLs alongside overview pages |
api/routers/proxy.py |
Add origin parameter support for postMessage with localhost |
app/src/pages/SpecPage.tsx |
Implement overview/detail mode with grid layout and library tooltips |
app/src/pages/InteractivePage.tsx |
Add localhost:8000 to allowed origins |
app/src/pages/CatalogPage.tsx |
Update navigation to link to overview pages |
app/src/components/SpecTabs.tsx |
Add tab collapse/expand behavior and overview mode support |
app/src/components/Footer.tsx |
Reduce footer spacing for consistency |
tests/unit/core/test_utils.py |
Comprehensive tests for strip_noqa_comments function |
tests/unit/api/test_routers.py |
Update sitemap test to verify implementation URLs |
| import re | ||
|
|
||
|
|
||
| def strip_noqa_comments(code: str | None) -> str | None: | ||
| """Remove # noqa: comments from code for cleaner user display.""" | ||
| if not code: | ||
| return code | ||
| return re.sub(r"\s*# noqa:[^\n]*", "", code) |
There was a problem hiding this comment.
The regex pattern r"\s*# noqa:[^\n]*" will remove noqa comments even when they appear inside string literals. While the test acknowledges this is "acceptable", it can lead to unintended modifications of code strings. Consider using a more robust approach such as:
- Using Python's AST module to parse and modify only actual comments
- Or at minimum, document this limitation clearly in the function's docstring
For example, code like message = "Error # noqa: F401" would become message = "Error", which changes the string's meaning.
| import re | |
| def strip_noqa_comments(code: str | None) -> str | None: | |
| """Remove # noqa: comments from code for cleaner user display.""" | |
| if not code: | |
| return code | |
| return re.sub(r"\s*# noqa:[^\n]*", "", code) | |
| import io | |
| import re | |
| import tokenize | |
| def strip_noqa_comments(code: str | None) -> str | None: | |
| """Remove ``# noqa:`` comments from code for cleaner user display. | |
| This function removes only actual ``# noqa:...`` comment tokens, as identified | |
| by Python's tokenizer. Substrings like ``"# noqa: F401"`` inside string | |
| literals are preserved and will not be modified. | |
| """ | |
| if not code: | |
| return code | |
| # Use Python's tokenizer so we only remove real comments, not substrings | |
| # inside string literals or other tokens. | |
| lines = code.splitlines(keepends=True) | |
| try: | |
| tokens = tokenize.generate_tokens(io.StringIO(code).readline) | |
| except (SyntaxError, tokenize.TokenError): | |
| # If tokenization fails, fall back to returning the original code | |
| # unchanged rather than risk corrupting it. | |
| return code | |
| for token in tokens: | |
| if token.type != tokenize.COMMENT: | |
| continue | |
| comment_text = token.string.lstrip() | |
| if not comment_text.startswith("# noqa:"): | |
| continue | |
| # Remove the noqa comment and any preceding whitespace on that line. | |
| line_index = token.start[0] - 1 # token rows are 1-based | |
| col = token.start[1] | |
| original_line = lines[line_index] | |
| newline = "" | |
| if original_line.endswith(("\r\n", "\n", "\r")): | |
| newline = original_line[len(original_line.rstrip("\r\n")) :] | |
| # Keep content before the comment column, stripping trailing spaces. | |
| kept = original_line[:col].rstrip() | |
| lines[line_index] = kept + newline | |
| return "".join(lines) |
|
|
||
|
|
||
| def strip_noqa_comments(code: str | None) -> str | None: | ||
| """Remove # noqa: comments from code for cleaner user display.""" |
There was a problem hiding this comment.
The function's docstring should document the known limitation that the regex-based approach will also remove noqa patterns inside string literals. This will help future maintainers understand the trade-off that was made. For example:
"Remove # noqa: comments from code for cleaner user display.
Note: Uses regex-based matching and will remove noqa patterns even within
string literals. This is acceptable for the current use case as noqa comments
in string literals are extremely rare in plot implementation code."
| """Remove # noqa: comments from code for cleaner user display.""" | |
| """Remove ``# noqa:`` comments from code for cleaner user display. | |
| Note: | |
| This uses regex-based matching and will also remove ``noqa`` patterns | |
| that appear inside string literals. This trade-off is acceptable for | |
| the current use case, as ``noqa`` comments inside plot implementation | |
| string literals are expected to be extremely rare. | |
| """ |
| <Typography | ||
| onClick={(e) => { | ||
| e.stopPropagation(); | ||
| setOpenTooltip(isTooltipOpen ? null : tooltipId); | ||
| }} | ||
| sx={{ | ||
| fontSize: '0.8rem', | ||
| fontWeight: 600, | ||
| fontFamily: '"MonoLisa", monospace', | ||
| color: isTooltipOpen ? '#3776AB' : '#9ca3af', | ||
| textTransform: 'lowercase', | ||
| cursor: 'pointer', | ||
| '&:hover': { color: '#3776AB' }, | ||
| }} | ||
| > | ||
| {impl.library_id} | ||
| </Typography> | ||
| </Tooltip> |
There was a problem hiding this comment.
The clickable library name tooltip lacks keyboard accessibility. Users who navigate via keyboard cannot activate the tooltip since it's disabled for focus events (disableFocusListener). Consider adding keyboard support by:
- Removing
disableFocusListeneror conditionally enabling it - Adding
onKeyDownhandler to the Typography component to support Enter/Space key activation - Adding appropriate ARIA attributes (role="button", tabIndex="0") to make it clear the element is interactive
| {!overviewMode && ( | ||
| <Tab | ||
| icon={<CodeIcon sx={{ fontSize: '1.1rem' }} />} | ||
| iconPosition="start" | ||
| label="Code" | ||
| onClick={() => tabIndex === 0 && setTabIndex(null)} | ||
| /> | ||
| )} | ||
| <Tab | ||
| icon={<StarIcon sx={{ fontSize: '1.1rem', color: tabIndex === 3 ? '#3776AB' : '#f59e0b' }} />} | ||
| icon={<DescriptionIcon sx={{ fontSize: '1.1rem' }} />} | ||
| iconPosition="start" | ||
| label={qualityScore ? `${Math.round(qualityScore)}` : 'Quality'} | ||
| label="Spec" | ||
| onClick={() => tabIndex === specTabIndex && setTabIndex(null)} | ||
| /> | ||
| {!overviewMode && ( | ||
| <Tab | ||
| icon={<ImageIcon sx={{ fontSize: '1.1rem' }} />} | ||
| iconPosition="start" | ||
| label="Impl" | ||
| onClick={() => tabIndex === 2 && setTabIndex(null)} | ||
| /> | ||
| )} | ||
| {!overviewMode && ( | ||
| <Tab | ||
| icon={<StarIcon sx={{ fontSize: '1.1rem', color: tabIndex === 3 ? '#3776AB' : '#f59e0b' }} />} | ||
| iconPosition="start" | ||
| label={qualityScore ? `${Math.round(qualityScore)}` : 'Quality'} | ||
| onClick={() => tabIndex === 3 && setTabIndex(null)} | ||
| /> | ||
| )} |
There was a problem hiding this comment.
The Tab components have both onChange (on the parent Tabs) and individual onClick handlers. This creates redundant event handling and potential confusion. The onClick handlers on individual tabs (lines 249, 256, 263, 271) will fire before the onChange handler on the Tabs component (line 227), but both will execute.
The collapse logic in onClick is being duplicated in the handleTabChange function. Consider removing the individual onClick handlers and keeping all the logic in handleTabChange, which already handles the toggle behavior correctly.
| onTrackEvent?.('tab_collapse', { library: libraryId }); | ||
| } else { | ||
| setTabIndex(newValue); | ||
| const tabNames = ['code', 'specification', 'implementation', 'quality']; |
There was a problem hiding this comment.
In overview mode, the tab index mapping is incorrect. When overviewMode is true, the Spec tab is at index 0, but the tabNames array at line 181 still includes 'code' as the first element. This means clicking the Spec tab in overview mode will track it as 'code' instead of 'specification'.
The array should be adjusted based on overview mode, or use a mapping approach that accounts for the actual tab configuration. For example:
const tabNames = overviewMode
? ['specification']
: ['code', 'specification', 'implementation', 'quality'];
| const tabNames = ['code', 'specification', 'implementation', 'quality']; | |
| const tabNames = overviewMode | |
| ? ['specification'] | |
| : ['code', 'specification', 'implementation', 'quality']; |
MarkusNeusinger
left a comment
There was a problem hiding this comment.
Zusammenfassung der wichtigsten Änderungen
1. Overview/Detail Mode (SpecPage.tsx)
Die URL-Struktur wurde geändert:
/{spec_id}→ Overview mit Grid aller Implementierungen/{spec_id}/{library}→ Detail-Ansicht mit Carousel
Das ermöglicht bessere SEO (jede Impl hat eigene URL) und bessere UX (Übersicht vor Detail).
2. PostMessage Origin Security (proxy.py)
Der target_origin Parameter für postMessage ist jetzt dynamisch statt hardcoded. Das ist wichtig weil:
- Localhost-Entwicklung sonst nicht funktioniert (Origin mismatch)
- Die Origin wird gegen eine Whitelist (
ALLOWED_ORIGINS) validiert - Default ist Production (
https://pyplots.ai)
3. Sitemap Expansion (seo.py)
Die Sitemap enthält jetzt alle Impl-URLs zusätzlich zu den Overview-URLs:
- ~170 Overview-Seiten:
/{spec_id} - ~1500 Impl-Seiten:
/{spec_id}/{library}
Das verbessert die SEO für library-spezifische Suchen (z.B. "bokeh heatmap python").
4. Footer Spacing (Footer.tsx, SpecTabs.tsx)
- Footer margin reduziert:
mt: 8, pt: 5→mt: 4, pt: 4 - TabPanel padding:
py: 2→pt: 2(kein bottom padding)
Das sorgt für konsistenten Abstand zwischen Content und Footer auf allen Seiten.
- Fix tabNames mapping in overview mode (was logging 'code' instead of 'specification') - Remove redundant onClick handlers from Tab components (handleTabChange handles toggle) - Document regex limitation in strip_noqa_comments docstring 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Summary
/{spec_id}→ Overview grid showing all implementations/{spec_id}/{library}→ Detail view with carouselTest plan
/scatter-basic- should show overview grid with all libraries/scatter-basic/matplotlib/sitemap.xml- should include implementation URLs/,/catalog, and spec pages🤖 Generated with Claude Code