From 917213afb127ec982fc75bb43284ec0a408fc0fd Mon Sep 17 00:00:00 2001 From: Erin Shaben Date: Thu, 2 Apr 2026 22:39:55 -0400 Subject: [PATCH 01/15] extend functionality of the per page llms dropdown - Add ability to anchor the per page widget to any class - Add ability to specify between two different styles of dropdown menus --- docs/ai-docs.md | 38 +++++ docs/ai-page-actions.md | 26 ++++ helper_lib/ai_file_utils/ai_file_utils.py | 103 +++++++++++-- plugins/ai_docs/plugin.py | 66 ++++++-- tests/ai_docs/test_ai_docs.py | 180 ++++++++++++++++++++++ tests/ai_file_utils/test_ai_file_utils.py | 128 +++++++++++++-- 6 files changed, 501 insertions(+), 40 deletions(-) diff --git a/docs/ai-docs.md b/docs/ai-docs.md index f81ce62..6730ae0 100644 --- a/docs/ai-docs.md +++ b/docs/ai-docs.md @@ -33,6 +33,9 @@ plugins: | `llms_config` | `string` | `llms_config.json` | Path to the LLM config file, relative to `mkdocs.yml`. | | `ai_resources_page` | `bool` | `true` | Generate the AI resources page from `ai-resources.md`. Set to `false` to disable. | | `ai_page_actions` | `bool` | `true` | Inject the per-page AI actions widget next to each H1. Set to `false` to disable. | +| `ai_page_actions_anchor` | `string` | `""` | CSS class name of the element(s) to append the widget into instead of wrapping the H1. When set, the default H1-wrapping behavior is replaced — see [Custom anchor](#custom-anchor). | +| `ai_page_actions_style` | `string` | `"split"` | Widget layout style. `"split"` renders a copy button left of the dropdown arrow; `"dropdown"` renders a single labelled button with all actions inside — see [Widget style](#widget-style). | +| `ai_page_actions_dropdown_label` | `string` | `"Markdown for LLMs"` | Trigger button label when `ai_page_actions_style` is `"dropdown"`. | | `enabled` | `bool` | `true` | Disable the entire plugin (all features). Supports `!ENV` for environment-based toggling. | The `llms_config` file controls content filtering, category definitions, and output paths. See [Resolve Markdown](resolve-md.md#-configuration) for a full breakdown of the `llms_config.json` schema. @@ -56,6 +59,41 @@ Injects a split-button dropdown widget next to each page's H1 heading at build t See [AI Page Actions](ai-page-actions.md) for details on exclusion rules, toggle page handling, and styling. +#### Widget style + +The widget supports two layout styles, controlled by `ai_page_actions_style`. + +**`split`** (default) — a copy button sits to the left of a chevron trigger that opens the dropdown: + +```yaml +plugins: + - ai_docs: + ai_page_actions_style: split # default, same as omitting the option +``` + +**`dropdown`** — a single labelled button opens a menu that contains all actions, including copy. No separate copy button is rendered outside the menu: + +```yaml +plugins: + - ai_docs: + ai_page_actions_style: dropdown + ai_page_actions_dropdown_label: Markdown for LLMs # default +``` + +The container gets the additional CSS class `ai-file-actions-container--dropdown` so you can style the two modes independently. Resources table widgets always carry `ai-file-actions-container--table`. See [Styling](ai-page-actions.md#styling) for the full class reference and CSS examples. + +#### Custom anchor + +By default, the widget is placed by wrapping the H1 in a `
` flex container. If your theme or custom layout already has a dedicated slot for page-level actions, you can redirect the widget there instead: + +```yaml +plugins: + - ai_docs: + ai_page_actions_anchor: my-page-actions +``` + +The plugin then finds every element that has the class `my-page-actions` within `.md-content` and appends the widget into it, leaving the H1 untouched. If no matching element is found on a given page, the page is left unchanged and a debug message is logged. Toggle-page variant handling is not applied in anchor mode — the widget always uses the page's own `.md` path. + ### AI resources page (`ai_resources_page`) Automatically generates the content for a page named `ai-resources.md`, replacing it with a table listing all available LLM artifact files (global indexes and per-category bundles) with copy, view, and download actions. diff --git a/docs/ai-page-actions.md b/docs/ai-page-actions.md index 1ba6fdc..ab501a7 100644 --- a/docs/ai-page-actions.md +++ b/docs/ai-page-actions.md @@ -50,6 +50,32 @@ The plugin loads `llms_config.json` from the project root (the directory contain The widget uses the same CSS classes as the table widget (`ai-file-actions.css`). The H1 wrapper layout is controlled by `.h1-ai-actions-wrapper`, which includes mobile responsive styles that stack the H1 and widget vertically on small screens. +### CSS classes + +Every widget shares the base container class. Modifier classes are added automatically based on context, giving you clean CSS selectors for each placement: + +| Class | Present on | +| :--- | :--- | +| `.ai-file-actions-container` | Every widget (base class) | +| `.ai-file-actions-container--dropdown` | Per-page widget when `ai_page_actions_style: dropdown` | +| `.ai-file-actions-container--table` | Widgets inside the AI resources page tables | + +### Targeting each widget independently + +```css +/* All widgets */ +.ai-file-actions-container { } + +/* Per-page split style only (default) */ +.ai-file-actions-container:not(.ai-file-actions-container--dropdown):not(.ai-file-actions-container--table) { } + +/* Per-page dropdown style only */ +.ai-file-actions-container--dropdown { } + +/* Resources table widgets only */ +.ai-file-actions-container--table { } +``` + ## Client-Side Behavior The widget relies on `ai-file-actions.js` for all client-side interactions (copy, download, dropdown toggle, keyboard navigation, analytics). No additional JavaScript is needed. diff --git a/helper_lib/ai_file_utils/ai_file_utils.py b/helper_lib/ai_file_utils/ai_file_utils.py index df7c5d0..2a88f80 100644 --- a/helper_lib/ai_file_utils/ai_file_utils.py +++ b/helper_lib/ai_file_utils/ai_file_utils.py @@ -359,14 +359,25 @@ def generate_dropdown_html( site_url: str = "", label_replace: dict[str, str] | None = None, content: str = "", + style: str = "split", + dropdown_label: str = "Markdown for LLMs", + extra_classes: str = "", ) -> str: """ - Generate the HTML for the AI file actions split-button. + Generate the HTML for the AI file actions widget. - The action marked ``primary: true`` in the JSON renders - as the left-side button; all other actions render as - dropdown items. The primary action is automatically - excluded from the dropdown. + Two styles are supported: + + ``"split"`` (default) + The action marked ``primary: true`` in the JSON renders as a + left-side button; all other actions render as dropdown items. + The primary action is automatically excluded from the dropdown. + + ``"dropdown"`` + A single trigger button labelled ``dropdown_label`` (default + ``"Markdown for LLMs"``) opens a menu that contains *all* + actions, including the primary one. No separate copy button + is rendered. Args: url: The relative URL of the file to act upon. @@ -375,12 +386,18 @@ def generate_dropdown_html( from the dropdown. primary_label: Optional label override for the primary button (e.g., "Copy page" vs default "Copy file"). + Only used in ``"split"`` style. site_url: The base site URL (e.g., "https://docs.polkadot.com/"). When provided, ``page_url`` passed to prompt templates will be the fully-qualified URL. label_replace: Optional dict of string replacements to apply to dropdown item labels (e.g., ``{"file": "page"}``). content: Optional page content for prompt template interpolation. + style: Widget style — ``"split"`` or ``"dropdown"``. + dropdown_label: Trigger button label used in ``"dropdown"`` style. + extra_classes: Additional CSS class(es) to append to the container + div (e.g., ``"ai-file-actions-container--table"``). + Multiple classes can be space-separated. Returns: The HTML string for the component. @@ -396,10 +413,71 @@ def generate_dropdown_html( page_url=url, filename=filename, content=content, prompt_page_url=full_url ) - # Separate primary action from dropdown actions + exclude_set = set(exclude) if exclude else set() + + # Build container class string — style modifier is auto-added, then + # any caller-supplied extra_classes are appended on top. + container_classes = "ai-file-actions-container" + if style == "dropdown": + container_classes += " ai-file-actions-container--dropdown" + if extra_classes: + container_classes += f" {extra_classes}" + + chevron = ( + '' + ) + + # ------------------------------------------------------------------ # + # "dropdown" style — single trigger button, all actions in the menu # + # ------------------------------------------------------------------ # + if style == "dropdown": + menu_items = "" + for action in actions: + if action.get("id") in exclude_set: + continue + if label_replace and "label" in action: + for old, new in label_replace.items(): + action["label"] = action["label"].replace(old, new) + menu_items += self._render_action_item(action, url) + + safe_url = html.escape(url, quote=True) + escaped_label = html.escape(dropdown_label, quote=True) + trigger_btn = ( + '" + ) + dropdown_menu = ( + '" + ) + return ( + f'
' + f"{trigger_btn}" + f"{dropdown_menu}" + "
" + ) + + # ------------------------------------------------------------------ # + # "split" style (default) — primary copy button + chevron trigger # + # ------------------------------------------------------------------ # primary_action = None dropdown_actions = [] - exclude_set = set(exclude) if exclude else set() for action in actions: if action.get("primary"): @@ -416,15 +494,6 @@ def generate_dropdown_html( ) # Dropdown trigger (right side of split button) - chevron = ( - '' - ) dropdown_btn = ( '