Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
51 changes: 50 additions & 1 deletion docs/ai-docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -52,10 +55,56 @@ Always runs when the plugin is enabled. Processes every documentation markdown f

### AI page actions (`ai_page_actions`)

Injects a split-button dropdown widget next to each page's H1 heading at build time. The widget lets readers copy, download, or open the page's resolved markdown in an LLM tool. Pages listed in `llms_config.json` exclusions, dot-directories, and pages with `hide_ai_actions: true` in their front matter are automatically skipped.
Injects an AI actions widget (split-button by default, or plain dropdown via `ai_page_actions_style`) next to each page's H1 heading at build time. The widget lets readers copy, download, or open the page's resolved markdown in an LLM tool. Pages listed in `llms_config.json` exclusions, dot-directories, and pages with `hide_ai_actions: true` in their front matter are automatically skipped.

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 `<div class="h1-ai-actions-wrapper">` 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 pages

When using `ai_page_actions_anchor` alongside the [`page_toggle` plugin](page-toggle.md), your template must render one anchor element per variant inside the toggle container, each carrying the matching `data-variant` attribute. For example:

```html
<div class="my-page-actions" data-variant="stable"></div>
<div class="my-page-actions" data-variant="latest"></div>
```

A Jinja macro is a convenient way to do this — iterate over your variants and emit the element with the correct `data-variant` for each one.

### 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.
Expand Down
26 changes: 26 additions & 0 deletions docs/ai-page-actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 rendered in dropdown style (applied by `ai_docs` when `ai_page_actions_style: dropdown`) |
| `.ai-file-actions-container--table` | Widgets inside the AI resources page tables |
Comment thread
eshaben marked this conversation as resolved.

### 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 (when used via ai_docs with ai_page_actions_style: dropdown) */
.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.
115 changes: 98 additions & 17 deletions helper_lib/ai_file_utils/ai_file_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -396,10 +413,83 @@ 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 = (
'<svg xmlns="http://www.w3.org/2000/svg"'
' width="24px" height="24px"'
' viewBox="0 0 24 24"'
' class="ai-file-actions-icon'
' ai-file-actions-chevron"'
' aria-hidden="true">'
'<path d="M7 10l5 5 5-5z"/></svg>'
)

# ------------------------------------------------------------------ #
# "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)
markdown_icon = (
'<svg xmlns="http://www.w3.org/2000/svg"'
' width="24px" height="24px"'
' viewBox="0 0 24 24"'
' class="ai-file-actions-icon"'
' aria-hidden="true">'
'<path d="M20.56 18H3.44C2.65 18 2 17.37 2 16.59V7.41C2 6.63 2.65 6'
' 3.44 6h17.12C21.35 6 22 6.63 22 7.41v9.18c0 .78-.65 1.41-1.44'
' 1.41zM6 15v-4.5l2.5 2.5 2.5-2.5V15h2V9h-2l-2.5 2.5L6 9H4v6h2z'
'm11.5 0L20 12h-2V9h-2v3h-2l3.5 4z"/>'
'</svg>'
)
trigger_btn = (
'<button class="ai-file-actions-btn ai-file-actions-trigger"'
' type="button"'
f' aria-label="{escaped_label}"'
' aria-haspopup="true"'
' aria-expanded="false"'
' role="button"'
f' data-url="{safe_url}">'
f'{markdown_icon}'
f'<span class="button-text">{escaped_label}</span>'
f"{chevron}"
"</button>"
)
dropdown_menu = (
'<div class="ai-file-actions-menu" role="menu">'
f"{menu_items}"
"</div>"
)
return (
f'<div class="{container_classes}">'
f"{trigger_btn}"
f"{dropdown_menu}"
"</div>"
)

# ------------------------------------------------------------------ #
# "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"):
Expand All @@ -416,15 +506,6 @@ def generate_dropdown_html(
)

# Dropdown trigger (right side of split button)
chevron = (
'<svg xmlns="http://www.w3.org/2000/svg"'
' width="24px" height="24px"'
' viewBox="0 0 24 24"'
' class="ai-file-actions-icon'
' ai-file-actions-chevron"'
' aria-hidden="true">'
'<path d="M7 10l5 5 5-5z"/></svg>'
)
dropdown_btn = (
Comment thread
eshaben marked this conversation as resolved.
'<button class="ai-file-actions-btn'
' ai-file-actions-trigger"'
Expand All @@ -448,7 +529,7 @@ def generate_dropdown_html(
)

return (
'<div class="ai-file-actions-container">'
f'<div class="{container_classes}">'
f"{copy_btn}"
f"{dropdown_btn}"
f"{dropdown_menu}"
Expand Down
Loading