Skip to content

Commit ec44f62

Browse files
feat: overview mode, expanded sitemap, and UI improvements (#3168)
## Summary - **Overview/Detail Mode Navigation**: New URL structure for spec pages - `/{spec_id}` → Overview grid showing all implementations - `/{spec_id}/{library}` → Detail view with carousel - **Action Buttons on Overview**: Copy code, download PNG, and open interactive buttons appear on hover - **Library Tooltips**: Shows library description and documentation URL (like home page) - **Interactive Page Fix**: postMessage origin now works correctly on localhost - **Expanded Sitemap**: Includes all implementation URLs (~1500 pages) for better SEO - **Spacing Fixes**: Consistent footer spacing across all pages - **ClickAwayListener**: Tooltips close when clicking outside ## Test plan - [ ] Visit `/scatter-basic` - should show overview grid with all libraries - [ ] Click on an implementation - should navigate to `/scatter-basic/matplotlib` - [ ] Hover over grid cards - action buttons should appear - [ ] Click library name in overview - tooltip should open - [ ] Click elsewhere - tooltip should close - [ ] Check `/sitemap.xml` - should include implementation URLs - [ ] Test interactive page on localhost - iframe should load correctly - [ ] Verify footer spacing is consistent across `/`, `/catalog`, and spec pages 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 9ab294a commit ec44f62

12 files changed

Lines changed: 987 additions & 453 deletions

File tree

api/routers/libraries.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from api.exceptions import raise_not_found
99
from core.constants import LIBRARIES_METADATA, SUPPORTED_LIBRARIES
1010
from core.database import LibraryRepository, SpecRepository
11+
from core.utils import strip_noqa_comments
1112

1213

1314
router = APIRouter(tags=["libraries"])
@@ -82,7 +83,7 @@ async def get_library_images(library_id: str, db: AsyncSession = Depends(require
8283
"url": impl.preview_url,
8384
"thumb": impl.preview_thumb,
8485
"html": impl.preview_html,
85-
"code": impl.code,
86+
"code": strip_noqa_comments(impl.code),
8687
}
8788
)
8889

api/routers/proxy.py

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,17 @@
99

1010
router = APIRouter(tags=["proxy"])
1111

12-
# Script injected to report content size to parent window
13-
# Uses specific origin (pyplots.ai) for postMessage security
14-
SIZE_REPORTER_SCRIPT = """
12+
# Allowed origins for postMessage
13+
ALLOWED_ORIGINS = ["https://pyplots.ai", "http://localhost:3000"]
14+
15+
16+
def get_size_reporter_script(target_origin: str) -> str:
17+
"""Generate size reporter script with specified target origin."""
18+
return f"""
1519
<script>
16-
(function() {
17-
function reportSize() {
18-
try {
20+
(function() {{
21+
function reportSize() {{
22+
try {{
1923
// Find the main content element (try common patterns for different libraries)
2024
var content = document.querySelector(
2125
'.bk-root, .vega-embed, .plotly, .chart-container, #container, .lp-plot, svg, canvas'
@@ -32,34 +36,38 @@
3236
height += padding;
3337
3438
// Send to parent with specific origin for security
35-
if (width > 0 && height > 0 && window.parent !== window) {
36-
window.parent.postMessage({
39+
if (width > 0 && height > 0 && window.parent !== window) {{
40+
window.parent.postMessage({{
3741
type: 'pyplots-size',
3842
width: Math.ceil(width),
3943
height: Math.ceil(height)
40-
}, 'https://pyplots.ai');
41-
}
42-
} catch (e) {
44+
}}, '{target_origin}');
45+
}}
46+
}} catch (e) {{
4347
// Silently fail if postMessage is blocked
44-
}
45-
}
48+
}}
49+
}}
4650
4751
// Report after load and after delays (for async rendering libraries)
48-
if (document.readyState === 'complete') {
52+
if (document.readyState === 'complete') {{
4953
setTimeout(reportSize, 100);
5054
setTimeout(reportSize, 500);
5155
setTimeout(reportSize, 1000);
52-
} else {
53-
window.addEventListener('load', function() {
56+
}} else {{
57+
window.addEventListener('load', function() {{
5458
setTimeout(reportSize, 100);
5559
setTimeout(reportSize, 500);
5660
setTimeout(reportSize, 1000);
57-
});
58-
}
59-
})();
61+
}});
62+
}}
63+
}})();
6064
</script>
6165
"""
6266

67+
68+
# Legacy constant for backwards compatibility with tests
69+
SIZE_REPORTER_SCRIPT = get_size_reporter_script("https://pyplots.ai")
70+
6371
# Allowed GCS bucket for security
6472
ALLOWED_HOST = "storage.googleapis.com"
6573
ALLOWED_BUCKET = "pyplots-images"
@@ -107,7 +115,7 @@ def build_safe_gcs_url(url: str) -> str | None:
107115

108116

109117
@router.get("/proxy/html", response_class=HTMLResponse)
110-
async def proxy_html(url: str):
118+
async def proxy_html(url: str, origin: str | None = None):
111119
"""
112120
Proxy an HTML file and inject size reporting script.
113121
@@ -118,6 +126,7 @@ async def proxy_html(url: str):
118126
119127
Args:
120128
url: The GCS URL to fetch (must be from allowed bucket)
129+
origin: Target origin for postMessage (must be in ALLOWED_ORIGINS)
121130
122131
Returns:
123132
Modified HTML with size reporting script injected
@@ -127,6 +136,11 @@ async def proxy_html(url: str):
127136
if safe_url is None:
128137
raise HTTPException(status_code=400, detail=f"Only URLs from {ALLOWED_HOST}/{ALLOWED_BUCKET} are allowed")
129138

139+
# Validate origin parameter - default to production if not specified or invalid
140+
target_origin = "https://pyplots.ai"
141+
if origin and origin in ALLOWED_ORIGINS:
142+
target_origin = origin
143+
130144
# Fetch the HTML with shorter timeout
131145
async with httpx.AsyncClient(timeout=10.0) as client:
132146
try:
@@ -139,13 +153,16 @@ async def proxy_html(url: str):
139153

140154
html_content = response.text
141155

156+
# Generate script with correct target origin
157+
size_script = get_size_reporter_script(target_origin)
158+
142159
# Inject the size reporter script before </body>
143160
if "</body>" in html_content:
144-
html_content = html_content.replace("</body>", f"{SIZE_REPORTER_SCRIPT}</body>")
161+
html_content = html_content.replace("</body>", f"{size_script}</body>")
145162
elif "</html>" in html_content:
146-
html_content = html_content.replace("</html>", f"{SIZE_REPORTER_SCRIPT}</html>")
163+
html_content = html_content.replace("</html>", f"{size_script}</html>")
147164
else:
148165
# Fallback: append to end
149-
html_content += SIZE_REPORTER_SCRIPT
166+
html_content += size_script
150167

151168
return HTMLResponse(content=html_content)

api/routers/seo.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,19 @@ async def get_sitemap(db: AsyncSession | None = Depends(optional_db)):
3434
" <url><loc>https://pyplots.ai/catalog</loc></url>",
3535
]
3636

37-
# Add spec URLs (only specs with implementations)
37+
# Add spec URLs (overview + all implementations)
3838
if db is not None:
3939
repo = SpecRepository(db)
4040
specs = await repo.get_all()
4141
for spec in specs:
4242
if spec.impls: # Only include specs with implementations
4343
spec_id = html.escape(spec.id)
44+
# Overview page
4445
xml_lines.append(f" <url><loc>https://pyplots.ai/{spec_id}</loc></url>")
46+
# Individual implementation pages
47+
for impl in spec.impls:
48+
library_id = html.escape(impl.library_id)
49+
xml_lines.append(f" <url><loc>https://pyplots.ai/{spec_id}/{library_id}</loc></url>")
4550

4651
xml_lines.append("</urlset>")
4752
xml = "\n".join(xml_lines)

api/routers/specs.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from api.exceptions import raise_not_found
99
from api.schemas import ImplementationResponse, SpecDetailResponse, SpecListItem
1010
from core.database import SpecRepository
11+
from core.utils import strip_noqa_comments
1112

1213

1314
router = APIRouter(tags=["specs"])
@@ -76,7 +77,7 @@ async def get_spec(spec_id: str, db: AsyncSession = Depends(require_db)):
7677
preview_thumb=impl.preview_thumb,
7778
preview_html=impl.preview_html,
7879
quality_score=impl.quality_score,
79-
code=impl.code,
80+
code=strip_noqa_comments(impl.code),
8081
generated_at=impl.generated_at.isoformat() if impl.generated_at else None,
8182
generated_by=impl.generated_by,
8283
python_version=impl.python_version,

app/src/components/Footer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ interface FooterProps {
1010

1111
export function Footer({ onTrackEvent, selectedSpec, selectedLibrary }: FooterProps) {
1212
return (
13-
<Box sx={{ textAlign: 'center', mt: 8, pt: 5, borderTop: '1px solid #f3f4f6' }}>
13+
<Box sx={{ textAlign: 'center', mt: 4, pt: 4, borderTop: '1px solid #f3f4f6' }}>
1414
<Box
1515
sx={{
1616
display: 'flex',

0 commit comments

Comments
 (0)