Skip to content
Draft
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ A collection of custom [MkDocs](https://www.mkdocs.org/) plugins designed to ext

Currently included:

- **[AI Resources Page](https://github.com/papermoonio/mkdocs-plugins/blob/main/docs/ai-resources-page.md)**: Automate the generation of an "AI Resources" page with a dynamic table of artifact files.
- **[Copy Markdown](https://github.com/papermoonio/mkdocs-plugins/blob/main/docs/copy-md.md)**: Serve raw Markdown files by copying them directly to your site's build folder.
- **[Minify](https://github.com/papermoonio/mkdocs-plugins/blob/main/docs/minify.md)**: Minify HTML, JS, and CSS files globally or by scope to optimize your site's performance.
- **[Page Toggle](https://github.com/papermoonio/mkdocs-plugins/blob/main/docs/page-toggle.md)**: Create variant pages for the same content and display them with an interactive toggle interface.
Expand All @@ -23,6 +24,7 @@ Enable one or more plugins in your `mkdocs.yml`:

```yaml
plugins:
- ai_resources_page
- copy_md:
source_dir: docs/.example
target_dir: example
Expand Down
73 changes: 73 additions & 0 deletions docs/ai-file-utils.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# AI File Utils

The `ai_file_utils` module serves as a centralized "contract" and utility service for defining and resolving actions related to AI artifacts. It allows you to separate the *definition* of actions (like "View Markdown", "Open in ChatGPT") from the *implementation* in the UI.

This is not a standalone MkDocs plugin but a shared library that other plugins (like `resolve_md`) can import to generate standardized action lists for any documentation page.

## 🔹 Usage

Since this is a helper library, you do not need to add it to your `mkdocs.yml` plugins list.

### Using in Python Code

Import the utility class directly in your code. The primary API is `resolve_actions`, which takes page context and returns a list of fully resolved action objects.

```python
from plugins.ai_file_utils.ai_file_utils import AIFileUtils

# Instantiate
utils = AIFileUtils()

# Resolve actions for a specific page context
actions = utils.resolve_actions(
page_url="https://docs.example.com/ai/pages/my-page.md",
filename="my-page.md",
content="# My Page Content..."
)
```

### Schema Fields

| Field | Type | Description |
| :--- | :--- | :--- |
| `type` | `string` | The category of action. Currently supports `link` (navigation) and `clipboard` (copy text). |
| `id` | `string` | Unique identifier (e.g., `view-markdown`, `open-chat-gpt`). |
| `label` | `string` | The human-readable text displayed in the UI. |
| `analyticsKey` | `string` | A standardized key for tracking usage events. |
| `href` | `string` | (Link only) The destination URL. Supports interpolation. |
| `download` | `string` | (Link only) If present, triggers a file download with this filename. |
| `clipboardContent` | `string` | (Clipboard only) The text to be copied. |
| `promptTemplate` | `string` | (LLM only) A template used to generate the `{{ prompt }}` variable. |

### Interpolation Variables

The module supports dynamic injection of context using `{{ variable }}` syntax.

| Variable | Description |
| :--- | :--- |
| `{{ page_url }}` | The full URL to the resolved AI artifact (the markdown file). |
| `{{ filename }}` | The filename of the markdown file (e.g., `basics.md`). |
| `{{ content }}` | The full text content of the markdown file. |
| `{{ prompt }}` | A special variable generated by processing the `promptTemplate` and URL-encoding the result. |

## 🔹 Adding New Actions

To add a new action (e.g., "Open in Gemini"), you modify `plugins/ai_file_utils/ai_file_actions.json`. You do not need to write new Python code.

### Example: Adding a New LLM

```json
{
"type": "link",
"id": "open-gemini",
"label": "Open in Gemini",
"href": "https://gemini.google.com/app?q={{ prompt }}",
"promptTemplate": "Read {{ page_url }} and explain it to me.",
"analyticsKey": "open_page_markdown_gemini"
}
```

This configuration will automatically:
1. Resolve `{{ page_url }}` in the prompt template.
2. URL-encode the result into `{{ prompt }}`.
3. Inject it into the `href`.
73 changes: 73 additions & 0 deletions docs/ai-resources-page.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# AI Resources Page Plugin

The AI Resources Page plugin automates the generation of an "AI Resources" page for your documentation site. It processes a configuration file (`llms_config.json`) to dynamically build an overview section and a table of resources (global files and category bundles) optimized for LLMs.

## Installation

This plugin is included in the `papermoon-mkdocs-plugins` package.

```bash
pip install papermoon-mkdocs-plugins
```

## Configuration

Add the plugin to your `mkdocs.yml`:

```yaml
plugins:
- ai_resources_page
```

### `llms_config.json`

The plugin relies on an `llms_config.json` file in your project root to determine what content to generate.

Key sections used by this plugin:

- **`project`**:
- `name`: The name of your project (e.g., "Polkadot"). **Required**.
- **`content`**:
- `categories_order`: A list of category names to appear in the table.
- `categories_info`: A dictionary where keys match `categories_order` and values contain metadata like `description`.
- **`outputs`**:
- `public_root`: The URL path where AI artifacts are served (default: `/ai/`).

#### Example Config

```json
{
"project": {
"name": "My Project"
},
"content": {
"categories_order": ["Basics", "Reference"],
"categories_info": {
"Basics": {
"description": "General knowledge base and overview content."
},
"Reference": {
"description": "API references and glossary."
}
}
},
"outputs": {
"public_root": "/ai/"
}
}
```

## How It Works

1. **Detection**: The plugin hooks into the `on_page_markdown` event and looks for a page named `ai-resources.md` (by filename).
2. **Generation**:
* It replaces the page content with a standard Introduction/Overview using the `project.name`.
* It generates a table including:
* **Standard Files**: `llms.txt`, `site-index.json`, `llms-full.jsonl`.
* **Categories**: Iterates through `categories_order` to create rows for each category bundle, using descriptions from `categories_info`.
3. **Client-Side Actions**: It generates HTML for "View", "Copy", and "Download" buttons that match the CSS classes (`.llms-view`, `.llms-copy`, `.llms-dl`) expected by the site's JavaScript.

## Notes

- This plugin is designed to work in tandem with the `resolve_md` plugin (which generates the actual artifact files) and specific frontend logic (like `main.html` overrides) to handle the button actions.
- If `project.name` is missing from `llms_config.json`, the build will fail with an error to prevent incorrect branding.
Empty file.
42 changes: 42 additions & 0 deletions plugins/ai_file_utils/ai_file_actions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"actions": [
{
"type": "link",
"id": "view-markdown",
"label": "View Markdown",
"href": "{{ page_url }}",
"analyticsKey": "view_page_markdown"
},
{
"type": "link",
"id": "download-markdown",
"label": "Download Markdown",
"href": "{{ page_url }}",
"download": "{{ filename }}",
"analyticsKey": "download_page_markdown"
},
{
"type": "clipboard",
"id": "copy-markdown",
"label": "Copy Markdown",
"clipboardContent": "{{ content }}",
"analyticsKey": "copy_page_markdown"
},
{
"type": "link",
"id": "open-chat-gpt",
"label": "Open in ChatGPT",
"href": "https://chatgpt.com/?q={{ prompt }}",
"promptTemplate": "Analyze the documentation at https://r.jina.ai/{{ page_url }}. Focus on the technical implementation details and code examples. I want to ask you questions about implementing these protocols.",
"analyticsKey": "open_page_markdown_chatgpt"
},
{
"type": "link",
"id": "open-claude",
"label": "Open in Claude",
"href": "https://claude.ai/new?q={{ prompt }}",
"promptTemplate": "Read {{ page_url }} so I can ask questions about it.",
"analyticsKey": "open_page_markdown_claude"
}
]
}
115 changes: 115 additions & 0 deletions plugins/ai_file_utils/ai_file_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import json
import copy
import urllib.parse
from pathlib import Path
from typing import Any, Dict, List
from mkdocs.utils import log

class AIFileUtils:
"""
A utility class that provides methods for resolving AI file actions.
This acts as a shared library/service for plugins to resolve
links, clipboard content, and LLM prompts based on a defined schema.
"""

def __init__(self):
self._actions_schema = None
self._actions_config_path = Path(__file__).parent / "ai_file_actions.json"

Comment on lines +15 to +18
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

AIFileUtils loads ai_file_actions.json from the package directory at runtime, but the repo doesn’t appear to configure setuptools to include JSON files in the built wheel/sdist. In an installed package, this file may be missing, causing all actions to resolve as empty. Add package-data configuration (e.g. tool.setuptools.package-data / MANIFEST.in) or embed defaults in Python.

Copilot uses AI. Check for mistakes.
def _load_actions_schema(self):
"""
Loads the actions definition from the JSON file.
"""
try:
if self._actions_config_path.exists():
text = self._actions_config_path.read_text(encoding="utf-8")
self._actions_schema = json.loads(text)
log.info(f"[ai_file_utils] Loaded actions schema from {self._actions_config_path}")
else:
log.warning(f"[ai_file_utils] Actions schema file not found at {self._actions_config_path}")
self._actions_schema = {"actions": []}
except json.JSONDecodeError as e:
log.error(f"[ai_file_utils] Failed to parse actions schema JSON: {e}")
self._actions_schema = {"actions": []}
except Exception as e:
log.error(f"[ai_file_utils] Unexpected error loading actions schema: {e}", exc_info=True)
self._actions_schema = {"actions": []}

def resolve_actions(self, page_url: str, filename: str, content: str) -> List[Dict[str, Any]]:
"""
Resolves the list of actions for a given page context.

Args:
page_url: The absolute URL to the markdown file (ai artifact).
filename: The name of the file (e.g., 'page.md').
content: The actual text content of the markdown file.

Returns:
A list of action dictionaries with all placeholders resolved.
"""
if not self._actions_schema:
self._load_actions_schema()

resolved_actions = []
raw_actions = self._actions_schema.get("actions", [])

for action_def in raw_actions:
try:
resolved_action = self._resolve_single_action(action_def, page_url, filename, content)
resolved_actions.append(resolved_action)
except Exception as e:
log.warning(f"[ai_file_utils] Failed to resolve action {action_def.get('id')}: {e}", exc_info=True)

return resolved_actions

def _resolve_single_action(self, action_def: Dict[str, Any], page_url: str, filename: str, content: str) -> Dict[str, Any]:
"""
Resolves a single action definition by replacing placeholders.
"""
# Create a deep copy to avoid modifying the schema if it has nested structures
action = copy.deepcopy(action_def)

# 1. Resolve Prompt if a template exists
prompt_text = ""
if "promptTemplate" in action:
tpl = action["promptTemplate"]
# Apply replacements to the prompt template first
# We construct a specific dict for prompt replacements to avoid circular dependency with "{{ prompt }}"
# and to handle content/url availability
prompt_replacements = {
"{{ content }}": content,
"{{ page_url }}": page_url,
"{{ filename }}": filename
}
for placeholder, replacement in prompt_replacements.items():
if placeholder in tpl:
tpl = tpl.replace(placeholder, replacement)
prompt_text = tpl

# Remove the template from the output as it's processed
action.pop("promptTemplate")

# 2. Prepare Context Variables
# URL encode the prompt for use in query parameters
encoded_prompt = urllib.parse.quote_plus(prompt_text)

replacements = {
"{{ page_url }}": page_url,
"{{ filename }}": filename,
"{{ content }}": content, # Be careful with large content in attributes, but for clipboard it's needed
"{{ prompt }}": encoded_prompt
}

# 3. Interpolate values into specific fields
# Fields that support interpolation
target_fields = ["href", "download", "clipboardContent"]

for field in target_fields:
if field in action and isinstance(action[field], str):
val = action[field]
for placeholder, replacement in replacements.items():
if placeholder in val:
val = val.replace(placeholder, replacement)
action[field] = val

return action
Empty file.
Loading
Loading