Skip to content

feat(frontend+api): surface language across the site + cross-language carousel#7142

Merged
MarkusNeusinger merged 8 commits into
mainfrom
feat/language-first-class-ui
May 17, 2026
Merged

feat(frontend+api): surface language across the site + cross-language carousel#7142
MarkusNeusinger merged 8 commits into
mainfrom
feat/language-first-class-ui

Conversation

@MarkusNeusinger
Copy link
Copy Markdown
Owner

Summary

Makes language (Python / R) a first-class concept across the site, now that R/ggplot2 implementations sit next to the Python libraries. The data layer already supports it; the UI didn't. Companion to #7141 (image-title change for issue #6958).

  • Plot cards — normal mode shows spec-id · {Language} · {library}; compact mode encodes language as a file-extension suffix on the abbreviation (mpl.py, ggplot2.r) to stay two tokens wide.
  • Library cards — small uppercase chip ([PYTHON] / [R]) next to the library name. Stale "nine Python plotting libraries" meta-copy also fixed.
  • SpecPage<title> + og:title + breadcrumb JSON-LD surface language. Detail mode also gets a small muted Python · matplotlib row near the report-issue link.
  • lang filter — new spec-level FilterCategory; FilterBar picks it up via the existing data-driven dispatch. Backend /plots/filter accepts ?lang=python (or r) and returns lang in counts/globalCounts.
  • Carousel scope — the impl-detail carousel now walks all impls of the spec by default, flipping the URL's language path segment as it cycles. The user can pin a scope via ?language=python, in which case the carousel stays language-locked and the query param is preserved across prev/next. PlotsPage card-click propagates ?language=… when an active lang filter is in effect, so clicking through from filtered plots preserves the scope. This is also what makes a future python.anyplot.ai-style subdomain expressible as URL state.

Files touched

  • app/src/constants/index.ts — new LANG_DISPLAY, LANG_EXT helpers (mirror the existing LIB_TO_LANG pattern).
  • app/src/types/index.tslang added to FilterCategory, FILTER_LABELS, FILTER_TOOLTIPS, FILTER_CATEGORIES.
  • app/src/components/ImageCard.tsx, LibraryCard.tsx, LibraryPills.tsx — display + props.
  • app/src/pages/LibrariesPage.tsx, PlotsPage.tsx, SpecPage.tsx — title, breadcrumb, carousel scope, filter-preserving navigation.
  • api/routers/plots.py_get_category_values + friends grow a language parameter; threaded through every call site.
  • tests/unit/api/test_plots_helpers.py, test_routers.py — signature updates + new lang cases.

Test plan

  • cd app && yarn type-check clean
  • cd app && yarn build succeeds
  • cd app && yarn test --run — 459 / 459 pass
  • uv run pytest tests/unit/api/ — 511 / 511 pass (including new lang filter cases)
  • Visual walkthrough deferred — no local DB to bring up the full stack. Please eyeball on the deploy preview:
    • /plots cards (normal + compact)
    • /libraries chips
    • /biplot-pca title (unfiltered)
    • /biplot-pca?language=python title + grid + carousel
    • /biplot-pca/r/ggplot2 — carousel cycles cross-language; pressing → flips both URL path segments
    • /biplot-pca/python/altair?language=python — carousel locked to Python; ?language= preserved across prev/next
    • Click a card on /plots?lang=python — lands on /{spec}/python/{lib}?language=python
    • Click a card on /plots (unfiltered) — lands on /{spec}/{lang}/{lib} (no query)

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings May 17, 2026 20:58
… carousel

The data layer already treats language (Python / R) as first-class — DB
table, FK on libraries, file paths under /{language}/{library}/. The UI
didn't: plot cards, library cards, page titles, and the filter bar were
all language-blind, and the impl-detail carousel was locked to the URL
path's language segment so /biplot-pca/python/altair would never let you
press → over to ggplot2. Now that R/ggplot2 implementations live next to
the Python libraries this gap is visible (plus plotnine is literally
"ggplot for Python", so the library name alone is ambiguous).

Display
- Plot cards: normal mode shows `spec-id · {Language} · {library}`;
  compact mode keeps the row two-token but encodes the language as a
  file-extension suffix on the library abbreviation (`mpl.py`, `ggplot2.r`).
- Library cards: small uppercase chip ([PYTHON] / [R]) left of the name.
  Stale "nine Python plotting libraries" meta-copy also updated since R
  has joined.
- SpecPage: `<title>` and og:title surface language —
  `{spec-title} · {Language} · {library} | anyplot.ai` in detail mode,
  `{spec-title} · {Language} | anyplot.ai` in hub-with-?language=. The
  breadcrumb JSON-LD also renders `Python`/`R` instead of `python`/`r`,
  and detail mode gets a small muted `Python · matplotlib` line near the
  report-issue row to anchor where you are.
- New `LANG_DISPLAY` and `LANG_EXT` helpers in `constants/index.ts`
  mirror the existing `LIB_TO_LANG` pattern.

Filter
- New `lang` FilterCategory (frontend types + FilterBar renders it via
  the existing data-driven dispatch — no FilterBar code changes needed).
- Backend `/plots/filter` accepts `?lang=python` (or `r`) and includes
  `lang` in counts/globalCounts. `_get_category_values` and friends grow
  a `language` parameter; the helpers are threaded with the impl's
  `impl.library.language` (or `img["language"]` for collected images).

Carousel scope
- The impl-detail carousel now walks ALL impls of the spec by default,
  flipping the URL's language path segment as it cycles
  (`/biplot-pca/r/ggplot2` → → → `/biplot-pca/python/altair`). The user
  can pin a scope via `?language=python`, in which case the carousel
  stays language-locked AND the query param is preserved across
  prev/next. This is also what makes a future python.anyplot.ai-style
  subdomain expressible as URL state.
- PlotsPage card-click propagates `?language=…` to the destination impl
  page when an active `lang` filter is in effect, so clicking through
  from filtered plots preserves the user's scope.

Verification
- `yarn type-check` clean; `yarn build` succeeds; 459 frontend tests
  pass; 511 backend unit tests pass (including new `lang` cases on
  `_get_category_values`, `_category_matches_filter`,
  `_image_matches_groups`, `_calculate_global_counts`).
- Visual browser walkthrough deferred — no local DB to spin up the full
  stack; please eyeball the cards/title/carousel on the deploy preview.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented May 17, 2026

Auto-fix from `uv run ruff format` after the language-threading change.
Function bodies and behavior unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR makes implementation language visible and filterable across the frontend and /plots/filter API, supporting R/ggplot2 alongside existing Python libraries.

Changes:

  • Adds language display/extension metadata and a new lang filter category.
  • Surfaces language in plot cards, library cards, SpecPage titles/breadcrumbs/detail metadata, and carousel navigation.
  • Threads language through backend plot filtering/count helpers and updates API unit tests.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
app/src/constants/index.ts Adds language display and extension helper maps.
app/src/types/index.ts Adds lang to filter categories, labels, tooltips, and ordering.
app/src/components/ImageCard.tsx Shows language in normal cards and language suffixes in compact cards.
app/src/components/LibraryCard.tsx Adds optional language chip and ggplot2 description.
app/src/components/LibraryPills.tsx Extends implementation type with language for cross-language carousel data.
app/src/pages/LibrariesPage.tsx Updates meta copy and passes language into library cards.
app/src/pages/PlotsPage.tsx Preserves language scope when navigating from language-filtered plot cards.
app/src/pages/SpecPage.tsx Adds language-aware title/breadcrumb/detail UI and carousel scoping/navigation.
api/routers/plots.py Adds language category handling to filtering, counts, parsing, and image collection.
tests/unit/api/test_plots_helpers.py Updates helper signatures and adds language filter/count coverage.
tests/unit/api/test_routers.py Updates router/helper tests for language-aware helper signatures and mocks.
Comments suppressed due to low confidence (3)

app/src/pages/SpecPage.tsx:370

  • This breadcrumb item points to /{specId}/{language}, but the router and SEO proxy intentionally redirect that tier to /{specId}?language=.... Emitting a redirected/non-canonical URL in BreadcrumbList structured data can give crawlers an inconsistent breadcrumb; use the canonical filtered hub URL (or omit the language item) instead.
    breadcrumbItems.push({
      name: LANG_DISPLAY[urlLanguage] || urlLanguage,
      item: `https://anyplot.ai/${specId}/${urlLanguage}`,

app/src/pages/SpecPage.tsx:159

  • The pinned ?language= scope is only applied to implementation navigation; the .compare() handler below still navigates to specPath(specId!) without this query. From a language-scoped detail page, returning to comparison therefore drops the user's scope and shows all languages instead of the filtered hub.
  // Build a `?language=…` query string when the carousel scope is pinned.
  // Empty otherwise — kept stable to avoid clutter in shareable URLs.
  const carouselQuery = carouselLanguage ? `?language=${encodeURIComponent(carouselLanguage)}` : '';

api/routers/plots.py:276

  • The new lang query parameter is only covered at helper level; there is no router-level test like the existing lib filter test that calls /plots/filter?lang=... and asserts the filtered images/counts. A parsing or response-wiring regression for this public API parameter could pass the current tests.
    for key, value in query_params:
        if key in valid_categories and value:
            values = [v.strip() for v in value.split(",") if v.strip()]

Comment on lines +78 to +82
const langExt = LANG_EXT[image.language];
const libraryDisplay = imageSize === 'compact'
? (LIBRARY_ABBR[image.library] || image.library)
? `${LIBRARY_ABBR[image.library] || image.library}${langExt ? '.' + langExt : ''}`
: image.library;
const languageDisplay = LANG_DISPLAY[image.language] || image.language;
Comment on lines 18 to +20
interface LibraryCardProps {
name: string;
language?: string;
Comment on lines +119 to +123
const carouselImpls = useMemo(() => {
if (!specData) return [];
if (!carouselLanguage) return specData.implementations;
return specData.implementations.filter((i) => i.language === carouselLanguage);
}, [specData, carouselLanguage]);
1. `ImageCard.tsx` — the in-card copy action called `fetchCode(spec_id,
   library)` without passing `image.language`, so R/ggplot2 cards would
   hit the Python codepath and 404. Now passes `image.language` through.

2. `LibrariesSection.tsx` — the landing-page library grid rendered
   `<LibraryCard name=…>` without `language`, so the new language chip
   silently disappeared on `/`. Now threads `info?.language` from the
   `libraries` prop.

3. `SpecPage.tsx` — detail-mode URLs like
   `/biplot-pca/r/ggplot2?language=python` are internally inconsistent:
   `carouselImpls` filters to Python and excludes `ggplot2`, leaving the
   pills/counter centred on the wrong library. Added an effect that
   drops the `?language=…` query in detail mode when it conflicts with
   the URL path's language. The URL path is the source of truth for
   "what is being viewed"; the query is just user-intent for scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 17, 2026 21:22
MarkusNeusinger and others added 3 commits May 17, 2026 23:23
… on plot cards

User feedback on PR #7142: "the language text on plot cards should be the
same weight as spec-id and library, and have a tooltip with a link to the
official site" — and "fill the languages table URL + description sensibly".

Backend
- `core/constants.py` LANGUAGES_METADATA: python now has a real
  description (was "the default language for anyplot plot
  implementations" — meta-y, not actually about Python); r is mildly
  rephrased to parallel.
- Alembic migration `c5f9a3d72be1`: UPDATEs the seeded `languages` rows
  with the new descriptions AND backfills `runtime_version` /
  `documentation_url` on the python row (those were NULL in the live DB
  because `sync_to_postgres` uses `on_conflict_do_nothing` and the
  initial seed predated those columns being populated).
- New `LanguageRepository` mirroring `LibraryRepository`.
- New `GET /languages` endpoint mirroring `/libraries`, including the
  same SWR cache layout (`Cache-Control: max-age=600,
  stale-while-revalidate=3600`).

Frontend
- New `LanguageInfo` type. `AppDataProvider` (`Layout.tsx`) now fetches
  `/languages` alongside specs/libraries/stats and exposes
  `languagesData` via `useAppData()`.
- `PlotsPage` → `ImagesGrid` → `ImageCard` thread `languagesData`
  through; each card receives `languageDescription` and
  `languageDocUrl` for the impl's language.
- `ImageCard`: the language token in normal mode is now `fontWeight:
  600` and the same `semanticColors.labelText` as spec-id/library
  (previously muted/light). Wrapped in a `<Tooltip>` whose body shows
  the language description and a link to the upstream homepage
  (python.org / r-project.org), using the same click-to-toggle pattern
  as the existing library tooltip. Compact mode still encodes language
  as a `.py`/`.r` suffix — no separate tooltip token there.

Verification
- `curl /languages` returns both rows with `runtime_version`,
  `documentation_url`, and the new descriptions populated.
- `yarn type-check` clean; 459 frontend tests + 186 affected backend
  unit tests pass; ruff format + check clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The small `Python · seaborn` row I added near the top of impl-detail
pages duplicates information that's already on screen — the navbar
breadcrumb shows `~/anyplot.ai · polar-line · python · seaborn` and the
code panel docstring shows `polar-line.seaborn`. The browser tab title
and the breadcrumb JSON-LD still carry the language token, so SEO and
window-chrome context are intact; only the visual repeat is gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the chip sat inline before the library name, which pushed the
name off-axis from the surrounding cards and competed with the name for
emphasis. Moving it to an absolute-positioned corner badge keeps the
library name in its original visual slot, makes the chip a glanceable
metadata indicator (matching the visual language of "tags in the
corner"), and gives the row back to the optional count.

The card name reserves right-padding when the chip is present so long
library names don't slide under the badge. `pointerEvents: 'none'` on
the chip lets clicks fall through to the underlying card button.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 22 changed files in this pull request and generated 7 comments.

Comments suppressed due to low confidence (1)

app/src/pages/SpecPage.tsx:560

  • Passing cross-language implementations into LibraryPills means ggplot2 now appears in the mobile carousel, but LibraryPills has its own abbreviation map that lacks a ggplot2 entry while the shared constants map has one. This will render a full ggplot2 pill beside abbreviated labels on small screens; consider using the shared abbreviation map or adding the R library there.
              onImageLoad={() => setImageLoaded(true)}
              onCopyCode={handleCopyCode}

languages = _languages_table()
bind = op.get_bind()
for lang_id, values in _NEW_ROWS.items():
bind.execute(languages.update().where(languages.c.id == lang_id).values(**values))
Comment on lines +348 to +354
<Typography
data-description-btn
aria-label={`Language: ${languageDisplay}`}
onClick={(e) => {
e.stopPropagation();
onTooltipToggle(isLangTooltipOpen ? null : langTooltipId);
}}
Comment thread app/src/types/index.ts
// Spec-level categories describe WHAT is visualized
// Impl-level categories describe HOW the code implements it (issue #2434)
export type FilterCategory =
| 'lang'
Comment thread api/routers/plots.py
- Different categories: AND (lib=matplotlib&plot=scatter)

Query params (comma-separated for OR, multiple params for AND):
- lang: Language filter (python, r)
Comment thread api/routers/languages.py
Comment on lines +36 to +37
@router.get("/languages")
async def get_languages(db: AsyncSession | None = Depends(optional_db)):
Comment on lines +249 to +253
class LanguageRepository(BaseRepository[Language]):
"""Repository for Language operations."""

model = Language
updatable_fields = LANGUAGE_UPDATABLE_FIELDS
const libList = LIBRARIES.map(name => {
const info = libraries.find(l => l.id === name);
return { name, count: info ? undefined : undefined }; // counts come from API if available
return { name, language: info?.language, count: info ? undefined : undefined }; // counts come from API if available
MarkusNeusinger and others added 2 commits May 17, 2026 23:36
Lint
- `alembic/versions/c5f9a3d72be1_update_language_descriptions.py`:
  ruff I001 wanted the imports sorted (the standalone `from __future__
  import annotations` separated by a blank line from the rest).

Frontend tests
- `ImageCard.test.tsx`: 7 new cases — normal-mode Python/R language
  token rendering, click-to-toggle on the language tooltip with the
  `lang-{spec}-{lib}` id, description + docs link inside the tooltip
  body, language token hidden in compact mode, and the `.py` / `.r`
  file-extension suffix on the abbreviated library name in compact mode.
- `LibraryCard.test.tsx` (new file): chip rendering, uppercased token,
  chip absent when language prop missing, count rendering, and the
  card-button click handler.
- `SpecPage.test.tsx`: 3 new cases — `<title>` includes `· Python ·
  {lib}` in detail mode and `· Python` in hub-with-filter mode, and the
  conflict-drop effect calls `setSearchParams` when the URL path
  language disagrees with `?language=` (and does not fire when they
  agree).

Codecov frontend patch coverage was failing at 60% — these targeted
tests cover the previously-uncovered new branches in ImageCard.tsx
(35%) and SpecPage.tsx (57%), plus LibraryCard.tsx which had no
existing test file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 17, 2026 21:56
@MarkusNeusinger MarkusNeusinger merged commit 665eb03 into main May 17, 2026
9 checks passed
@MarkusNeusinger MarkusNeusinger deleted the feat/language-first-class-ui branch May 17, 2026 21:57
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 5 comments.

Comment on lines +131 to +132
const langActive = activeFilters.some((f) => f.category === 'lang');
const qs = langActive && img.language ? `?language=${encodeURIComponent(img.language)}` : '';
return (
<Box key={name} sx={{ position: 'relative' }}>
<LibraryCard name={name} onClick={() => handleLibraryClick(name)} />
<LibraryCard name={name} language={meta?.language} onClick={() => handleLibraryClick(name)} />
const libList = LIBRARIES.map(name => {
const info = libraries.find(l => l.id === name);
return { name, count: info ? undefined : undefined }; // counts come from API if available
return { name, language: info?.language, count: info ? undefined : undefined }; // counts come from API if available
Comment thread api/routers/languages.py
Comment on lines +36 to +37
@router.get("/languages")
async def get_languages(db: AsyncSession | None = Depends(optional_db)):
Comment on lines +249 to +253
class LanguageRepository(BaseRepository[Language]):
"""Repository for Language operations."""

model = Language
updatable_fields = LANGUAGE_UPDATABLE_FIELDS
MarkusNeusinger added a commit that referenced this pull request May 17, 2026
…ge (#7144)

## Summary

Follow-up to #7142, which merged before this commit landed on the
branch. Addresses Copilot's second-pass review on that PR.

The headline fix is **analytics** — `/plots?lang=python` was falling
through `buildPlausibleUrl` and was never recorded as a Plausible
pageview (only `?language=` on spec hubs was wired up).

## Changes

- **`app/src/hooks/useAnalytics.ts`** — `lang` added to `orderedKeys` so
the new FilterBar `lang` filter on `/plots` is converted into a path
segment (`/lang/python`) the same way every other filter category is.
Two new tests in `useAnalytics.test.ts` pin both `?lang=` and
`?language=` conversions.
- **`app/src/utils/filters.test.ts` + `filters-extended.test.ts`** —
added the now-required `lang` key to the `FilterCounts` mock fixtures
(since `lang` was added to the `FilterCategory` union).
- **`app/src/components/LibrariesSection.tsx`** — dropped the dead
`count: info ? undefined : undefined` ternary and stale comment.
- **`tests/unit/core/database/test_repositories.py`** — new
`TestLanguageRepository` mirroring `TestLibraryRepository` (ordered
`get_all`, upsert-creates, upsert-updates, raises on missing id) and a
`LANGUAGE_UPDATABLE_FIELDS` assertion.
- **`tests/unit/api/test_routers.py`** — new `TestLanguagesRouter`
(no-DB seed fallback, DB-backed serialize, cache hit) and two new
`TestPlotsRouter` cases pinning `/plots/filter?lang=python` and
`/plots/filter?lang=r` behavior through the public HTTP path.

No production behavior change other than the analytics bug fix;
everything else is test-coverage hygiene that Copilot flagged as missing
on #7142.

## Test plan

- [x] `cd app && yarn type-check` clean
- [x] `cd app && yarn test --run` (45 in touched files; full suite
green)
- [x] `uv run pytest tests/unit/api/ tests/unit/core/database/` (170
pass)
- [x] `uv run ruff check . && ruff format --check .` clean

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MarkusNeusinger added a commit that referenced this pull request May 18, 2026
Version bump for the v2.4.0 release. Release notes will be attached to
the tag once this lands.

## Highlights since v2.3.0

- **R / ggplot2 added as the 10th library** + multi-language pipeline
(#6944, #6961, #7052). 30 ggplot2 implementations landed across
foundational plot types.
- **In-app feedback widget** (#7143).
- **Stats page** with Plausible visitors chart + daily-impl timeline
(#6608).
- **Language across the site**: `/plots?lang=` filtering, cross-language
carousel, language in URLs and titles (#7141, #7142, #7144).
- **UI polish**: pseudo-function styling for 404 / footer / empty state
/ library card (#6436); mobile fixes for `/stats`, `/mcp`, breadcrumb +
FAB (#6902, #7283).
- **Pipeline**: review-retry listener + stuck-jobs watchdog (#6084);
daily-regen 2h → hourly (#6943).
- **Dependencies**: mypy 1.20→2.1, urllib3 2.6→2.7, authlib bump,
react/mui/python-minor groups.
- ~1200 implementation regenerations across all 10 libraries.

No SemVer-breaking changes.

**Full Changelog:**
v2.3.0...main

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MarkusNeusinger added a commit that referenced this pull request May 18, 2026
## Problem

`Sync: PostgreSQL` has been failing on every push to `main` since ~17:55
with:

```
ERROR [alembic.util.messaging] Multiple head revisions are present for given argument 'head'; please specify a specific target revision, '<branchname>@Head' to narrow to a specific head, or 'heads' for all heads
FAILED: Multiple head revisions are present...
```

PRs #7142 (language descriptions) and #7143 (feedback widget) both
branched from \`3a7e1b5c0c4f\` and merged independently, leaving two
alembic heads:

- `c5f9a3d72be1` — update_language_descriptions
- `e5b1c9d4a7f2` — feedback_uuid_and_status

No merge revision was added, so `alembic upgrade head` is ambiguous.
Production DB is stuck on whatever was the last successful sync.

## Fix

Add a no-op merge revision \`7efe9fc8bde1\` joining both heads. Verified
locally:

- \`alembic heads\` → single head \`7efe9fc8bde1\`
- \`alembic upgrade head --sql\` produces a clean transactional plan

## Test plan

- [ ] CI green on this PR
- [ ] After merge, `Sync: PostgreSQL` workflow goes green on the next
push to `main`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants