# How to Build Plugins for Canvas-Chat

This comprehensive guide shows you how to build plugins for Canvas-Chat, covering JavaScript-only plugins, Python-only plugins, and paired JavaScript/Python plugins that work together.

## Table of Contents

1. [Plugin Types Overview](#plugin-types-overview)
2. [JavaScript-Only Plugins](#javascript-only-plugins)
3. [Paired JavaScript + Python Plugins](#paired-javascript--python-plugins)
4. [Python-Only Plugins](#python-only-plugins)
5. [Making API Calls from JavaScript to Python](#making-api-calls-from-javascript-to-python)
6. [Configuration](#configuration)
7. [Testing Your Plugin](#testing-your-plugin)
8. [Best Practices](#best-practices)

## Plugin Types Overview

Canvas-Chat supports three types of plugins:

| Type | Frontend (JS) | Backend (Python) | Use Case |
|------|---------------|------------------|----------|
| **JavaScript-only** | ✅ | ❌ | Custom node types, slash commands, UI interactions |
| **Python-only** | ❌ | ✅ | Backend-only functionality (e.g., file processing) |
| **Paired** | ✅ | ✅ | Features requiring both frontend UI and backend processing |

### When to Use Each Type

**JavaScript-only plugins** are best for:

- Custom node rendering (polls, charts, forms)
- Slash commands that only need frontend APIs
- UI interactions that don't require server-side processing
- Example: `poll.js` - creates interactive poll nodes with voting

**Python-only plugins** are best for:

- File upload handlers (PDF, Word, etc.)
- Backend-only processing that doesn't need UI
- Server-side integrations that shouldn't expose logic to frontend

**Paired plugins** are best for:

- Features that need both UI and server processing
- Research features (frontend displays results, backend calls APIs)
- File uploads with custom node types (frontend creates nodes, backend processes files)
- Example: Research plugin (`research.js` + backend endpoints), PDF handler (`pdf-node.js` + `pdf_handler.py`)

## How to Prompt for Plugins

Before diving into examples, here are general tips for creating effective prompts that will help AI assistants build your plugins:

### General Prompting Principles

1. **Be Specific, Not Vague**
   - ❌ Bad: "Make a plugin for polls"
   - ✅ Good: "Create a plugin that lets users create interactive polls with voting, where users click options to vote and see vote counts update in real-time"

2. **Describe the User Experience**
   - Explain what happens from the user's perspective: "User types /poll, enters a question, sees a poll node appear with options generated by AI, clicks options to vote, sees vote bars update"

3. **List All Features Explicitly**
   - Don't assume anything is obvious
   - List every button, action, and interaction
   - Mention edge cases (what if no input? what if file is too large?)

4. **Include Visual Details**
   - Describe icons, colors, layouts
   - Specify node sizes and appearance
   - Mention how content should be formatted

5. **Provide Examples**
   - Give concrete examples of inputs and outputs
   - Show what the final result should look like

6. **Mention Technical Requirements**
   - File size limits
   - Accepted file formats
   - External APIs or services needed
   - Configuration requirements

7. **Reference Similar Plugins**
   - If you know of similar plugins, mention them
   - This helps the AI understand the patterns to follow

### Prompt Structure Template

For any plugin, structure your prompt like this:

```text
I want to create a Canvas-Chat plugin for [FEATURE NAME].

1. **What it does**: [Clear description of the feature]

2. **User interaction**:
   - [Step 1 of user flow]
   - [Step 2 of user flow]
   - [Step 3 of user flow]

3. **Visual appearance**:
   - [Node appearance, icons, colors, layout]

4. **Data storage**:
   - [What data needs to be stored]

5. **Actions/Buttons**:
   - [List all interactive elements]

6. **Edge cases**:
   - [What happens in error cases]

7. **Technical requirements**:
   - [File sizes, formats, APIs, etc.]

Please create a [JavaScript-only / Python-only / Paired] plugin following Canvas-Chat patterns.
```

## JavaScript-Only Plugins

JavaScript-only plugins are the simplest type. They run entirely in the browser and can create custom node types, add slash commands, and interact with the canvas.

### Example: Poll Plugin

The poll plugin (`example-plugins/poll.js`) demonstrates a complete JavaScript-only plugin:

```javascript
/**
 * Poll Plugin - JavaScript-only example
 * Combines custom node type (PollNode) with feature plugin (PollFeature)
 */

import { BaseNode, Actions } from '../node-protocols.js';
import { NodeRegistry } from '../node-registry.js';
import { FeaturePlugin } from '../feature-plugin.js';
import { createNode } from '../graph-types.js';

// =============================================================================
// 1. Define Custom Node Protocol
// =============================================================================

class PollNode extends BaseNode {
    getTypeLabel() {
        return 'Poll';
    }

    getTypeIcon() {
        return '📊';
    }

    renderContent(canvas) {
        const question = this.node.question || 'No question set';
        const options = this.node.options || [];
        const votes = this.node.votes || {};

        // Render poll HTML with voting bars
        let html = `<div class="poll-content">`;
        html += `<div class="poll-question">${canvas.escapeHtml(question)}</div>`;
        // ... render options with vote counts ...
        return html;
    }

    getActions() {
        return [
            { id: 'add-option', label: '➕ Add Option' },
            { id: 'reset-votes', label: '🔄 Reset' },
            Actions.COPY,
        ];
    }

    getEventBindings() {
        return [
            {
                selector: '.poll-option',
                multiple: true,
                handler: (nodeId, e, canvas) => {
                    const index = parseInt(e.currentTarget.dataset.index);
                    canvas.emit('pollVote', nodeId, index);
                },
            },
        ];
    }
}

// Register the node type
NodeRegistry.register({
    type: 'poll',
    protocol: PollNode,
    defaultSize: { width: 400, height: 300 },
    css: `/* Custom CSS for poll nodes */`,
});

// =============================================================================
// 2. Define Feature Plugin (Slash Commands)
// =============================================================================

export class PollFeature extends FeaturePlugin {
    // Pattern 2: Implement getSlashCommands() for command menu metadata
    // This provides description and placeholder for the slash command menu UI
    getSlashCommands() {
        return [
            {
                command: '/poll',
                description: 'Generate a poll from natural language',
                placeholder: 'e.g., "What should we have for lunch?"',
            },
        ];
    }

    async handleCommand(command, args, contextObj) {
        const input = args.trim();
        if (!input) {
            this.showToast?.('Please provide a poll question', 'warning');
            return;
        }

        // Create poll node
        const loadingNode = createNode('poll', '', {
            position: this.graph.autoPosition([]),
            question: '🔄 Generating poll...',
            options: [],
            votes: {},
        });
        this.graph.addNode(loadingNode);
        this.canvas.renderNode(loadingNode);

        // Use LLM to generate poll options
        const model = this.modelPicker.value;
        const prompt = `Generate a poll based on: "${input}"\n\nReturn JSON: {"question": "...", "options": ["...", "..."]}`;

        await this.chat.sendMessage(
            [{ role: 'user', content: prompt }],
            model,
            // onChunk - stream response
            (chunk, accumulated) => {
                // Update node during streaming
                this.graph.updateNode(loadingNode.id, {
                    question: `🔄 Generating... (${accumulated.length} chars)`,
                });
                this.canvas.renderNode(this.graph.getNode(loadingNode.id));
            },
            // onDone - parse JSON and finalize
            () => {
                // Parse JSON response and update node
                const pollData = JSON.parse(fullResponse);
                this.graph.updateNode(loadingNode.id, {
                    question: pollData.question,
                    options: pollData.options,
                    votes: {},
                });
                this.canvas.renderNode(this.graph.getNode(loadingNode.id));
                this.saveSession?.();
            },
            // onError
            (error) => {
                this.showToast?.(`Error: ${error.message}`, 'error');
            }
        );
    }

    // Handle canvas events from PollNode
    getCanvasEventHandlers() {
        return {
            pollVote: this.handlePollVote.bind(this),
        };
    }

    handlePollVote(nodeId, optionIndex) {
        const node = this.graph.getNode(nodeId);
        if (!node.votes) node.votes = {};
        node.votes[optionIndex] = (node.votes[optionIndex] || 0) + 1;
        this.graph.updateNode(nodeId, { votes: node.votes });
        this.canvas.renderNode(node);
        this.saveSession?.();
    }
}

// Auto-register when plugin loads
if (typeof window !== 'undefined') {
    // Registration code (see full example in poll.js)
}
```

### Key Components

1. **Custom Node Protocol** (`PollNode`):
   - Extends `BaseNode` from `node-protocols.js`
   - Defines rendering (`renderContent()`)
   - Defines actions (`getActions()`)
   - Defines event bindings (`getEventBindings()`)
   - Registered with `NodeRegistry.register()`

2. **Feature Plugin** (`PollFeature`):
   - Extends `FeaturePlugin` from `feature-plugin.js`
   - Registers slash commands (`getSlashCommands()`)
   - Handles commands (`handleCommand()`)
   - Handles canvas events (`getCanvasEventHandlers()`)

3. **Self-Contained**:
   - Both node protocol and feature plugin in one file
   - Auto-registers when loaded
   - No backend dependencies

### Poll Plugin Configuration

Add to `config.yaml`:

```yaml
plugins:
  - path: ./src/canvas_chat/static/js/example-plugins/poll.js
```

Or use the new format:

```yaml
plugins:
  - js: ./plugins/my-poll.js
```

### External Plugin Auto-Registration

When a plugin is loaded from `config.yaml`, it needs to register itself with the plugin system. The poll plugin demonstrates the pattern:

```javascript
// At the end of poll.js
if (typeof window !== 'undefined') {
    const registerFeature = (app) => {
        if (app && app.featureRegistry && app.featureRegistry._appContext) {
            app.featureRegistry.register({
                id: 'poll',
                feature: PollFeature,
                slashCommands: [
                    {
                        command: '/poll',
                        handler: 'handleCommand',
                    },
                ],
                priority: 500, // OFFICIAL priority for external plugins
            }).then(() => {
                console.log('[Poll Plugin] PollFeature registered successfully');
            }).catch((err) => {
                console.error('[Poll Plugin] Failed to register PollFeature:', err);
            });
        }
    };

    // Try immediate registration if app is already available
    if (window.app) {
        registerFeature(window.app);
    }

    // Also listen for the plugin system ready event
    window.addEventListener('app-plugin-system-ready', (event) => {
        registerFeature(event.detail.app);
    });
}
```

**Key points:**

1. **Check for window**: Only run in browser environment (not in tests)
2. **Check plugin system ready**: Verify `app.featureRegistry._appContext` exists before registering
3. **Try immediate registration**: If `window.app` exists, register immediately
4. **Listen for ready event**: Also listen for `app-plugin-system-ready` event in case plugin loads before app is ready
5. **Priority**: Use `PRIORITY.OFFICIAL` (500) or `PRIORITY.COMMUNITY` (100) for external plugins, not `PRIORITY.BUILTIN` (1000)

### How to Request This Plugin

If you want to build a similar JavaScript-only plugin (like a poll, survey, or interactive node), here's a detailed prompt you can use:

**Prompt Template:**

```text
I want to create a Canvas-Chat plugin for [DESCRIBE YOUR FEATURE].

Here's what I need:

1. **Node Type**: Create a custom node type called "[NODE_NAME]" that displays [DESCRIBE WHAT THE NODE SHOWS].
   - The node should have an icon: [EMOJI OR DESCRIPTION]
   - The node should show: [DESCRIBE THE VISUAL CONTENT]
   - When users interact with it: [DESCRIBE CLICKS, BUTTONS, OR ACTIONS]

2. **Slash Command**: Add a slash command "/[COMMAND_NAME]" that [DESCRIBE WHAT THE COMMAND DOES].
   - When users type "/[COMMAND_NAME] [INPUT]", it should [DESCRIBE THE BEHAVIOR]
   - The command should accept: [DESCRIBE INPUT FORMAT - e.g., "natural language question", "comma-separated list", etc.]

3. **Functionality**:
   - [DESCRIBE KEY FEATURE 1 - e.g., "Users can vote on options by clicking them"]
   - [DESCRIBE KEY FEATURE 2 - e.g., "Show vote counts and percentages"]
   - [DESCRIBE KEY FEATURE 3 - e.g., "Allow adding new options via a button"]

4. **Data Storage**: The node should store [DESCRIBE WHAT DATA - e.g., "question text, list of options, vote counts for each option"].

5. **Visual Design**:
   - [DESCRIBE LAYOUT - e.g., "Question at top, options in a vertical list below"]
   - [DESCRIBE STYLING - e.g., "Show vote bars with percentages, use colors to indicate popularity"]

6. **Actions**: The node should have these action buttons:
   - [ACTION 1 - e.g., "Add Option button"]
   - [ACTION 2 - e.g., "Reset Votes button"]
   - [STANDARD ACTIONS - e.g., "Copy button"]

Please create a complete JavaScript-only plugin file that:
- Extends BaseNode for the custom node type
- Extends FeaturePlugin for the slash command
- Registers both with the appropriate registries
- Handles all user interactions
- Follows Canvas-Chat plugin architecture patterns
- Is self-contained in a single file

The plugin should work entirely in the browser (no backend needed).
```

**Example for a Poll Plugin:**

```text
I want to create a Canvas-Chat plugin for interactive polls.

Here's what I need:

1. **Node Type**: Create a custom node type called "Poll" that displays a question and voting options.
   - The node should have an icon: 📊 (bar chart emoji)
   - The node should show: A question at the top, then a list of options with vote bars showing percentages
   - When users click on an option: It should increment that option's vote count and update the display

2. **Slash Command**: Add a slash command "/poll" that generates a poll from natural language.
   - When users type "/poll What should we have for lunch?", it should use an LLM to generate a poll with a question and 3-5 relevant options
   - The command should accept: Any natural language description of what the poll should be about

3. **Functionality**:
   - Users can vote on options by clicking them (each click adds one vote)
   - Show vote counts and percentages as visual bars
   - Allow adding new options via an "Add Option" button
   - Allow resetting all votes via a "Reset" button

4. **Data Storage**: The node should store the question text, a list of option strings, and a votes object mapping option indices to vote counts.

5. **Visual Design**:
   - Question displayed prominently at the top in bold
   - Options displayed in a vertical list below
   - Each option shows a colored bar indicating vote percentage
   - Vote counts and percentages displayed next to each option

6. **Actions**: The node should have these action buttons:
   - "➕ Add Option" button that opens a modal to add a new option
   - "🔄 Reset" button that clears all votes (with confirmation)
   - Standard "Copy" button to copy poll results

Please create a complete JavaScript-only plugin file that:
- Extends BaseNode for the PollNode custom node type
- Extends FeaturePlugin for the PollFeature slash command handler
- Registers both with NodeRegistry and FeatureRegistry
- Handles voting, adding options, and resetting votes
- Uses LLM to generate polls from natural language
- Follows Canvas-Chat plugin architecture patterns
- Is self-contained in a single file

The plugin should work entirely in the browser (no backend needed).
```

**Tips for Non-Programmers:**

- **Be Specific**: Instead of "make it interactive," say "users click options to vote, and vote counts update immediately"
- **Describe the User Experience**: Explain what happens step-by-step from the user's perspective
- **Include Visual Details**: Mention colors, layouts, icons, and how things should look
- **List All Features**: Don't assume anything is obvious - list every feature you want
- **Provide Examples**: Give concrete examples of inputs and expected outputs
- **Mention Edge Cases**: If relevant, mention what should happen in error cases (e.g., "if no options are provided, show an error message")

## Paired JavaScript + Python Plugins

Paired plugins combine frontend JavaScript with backend Python processing. This is ideal for features that need server-side APIs, file processing, or complex backend logic.

### Example 1: Research Plugin

The research plugin demonstrates a paired plugin where:

- **Frontend** (`research.js`): Handles UI, creates nodes, streams results
- **Backend** (`app.py`): Provides `/api/exa/research` and `/api/ddg/research` endpoints

#### Frontend (JavaScript)

```javascript
/**
 * Research Feature - Paired Plugin Example
 * Frontend handles UI and calls backend API endpoints
 */

import { FeaturePlugin } from '../feature-plugin.js';
import { NodeType, createNode } from '../graph-types.js';
import { readSSEStream } from '../sse.js';
import { apiUrl } from '../utils.js';
import { storage } from '../storage.js';

export class ResearchFeature extends FeaturePlugin {
    getSlashCommands() {
        return [
            {
                command: '/research',
                description: 'Perform deep research on a topic',
                placeholder: 'e.g., "What are the latest developments in AI?"',
            },
        ];
    }

    async handleCommand(command, args, contextObj) {
        const instructions = args.trim();
        const selectedContext = contextObj?.text || null;

        // Check which provider to use (Exa or DuckDuckGo fallback)
        const hasExa = storage.hasExaApiKey();
        const exaKey = hasExa ? storage.getExaApiKey() : null;
        const model = hasExa ? 'exa-research' : this.modelPicker.value;

        // Create research node
        const researchNode = createNode(NodeType.RESEARCH, `Research: ${instructions}`, {
            position: this.graph.autoPosition([]),
            width: 500,
        });
        this.graph.addNode(researchNode);
        this.canvas.renderNode(researchNode);

        // Call backend API (SSE stream)
        let response;
        if (hasExa) {
            // Exa research endpoint
            response = await fetch(apiUrl('/api/exa/research'), {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    instructions: instructions,
                    api_key: exaKey,
                    model: 'exa-research',
                }),
            });
        } else {
            // DuckDuckGo fallback endpoint
            response = await fetch(apiUrl('/api/ddg/research'), {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(
                    this.buildLLMRequest({
                        instructions: instructions,
                        context: selectedContext || null,
                        max_iterations: 4,
                        max_sources: 40,
                    })
                ),
            });
        }

        if (!response.ok) {
            throw new Error(`Research failed: ${response.statusText}`);
        }

        // Parse SSE stream
        let reportContent = `**Research:** ${instructions}\n\n`;
        let sources = [];

        await readSSEStream(response, {
            onEvent: (eventType, data) => {
                if (eventType === 'status') {
                    // Update status
                    this.canvas.updateNodeContent(
                        researchNode.id,
                        `${reportContent}*${data}*`,
                        true
                    );
                } else if (eventType === 'content') {
                    // Accumulate report content
                    reportContent += data;
                    this.canvas.updateNodeContent(researchNode.id, reportContent, true);
                    this.graph.updateNode(researchNode.id, { content: reportContent });
                } else if (eventType === 'sources') {
                    // Parse sources
                    sources = JSON.parse(data);
                }
            },
            onDone: () => {
                // Finalize report with sources
                if (sources.length > 0) {
                    reportContent += '\n\n---\n**Sources:**\n';
                    for (const source of sources) {
                        reportContent += `- [${source.title}](${source.url})\n`;
                    }
                }
                this.canvas.updateNodeContent(researchNode.id, reportContent, false);
                this.graph.updateNode(researchNode.id, { content: reportContent });
                this.saveSession?.();
            },
            onError: (err) => {
                this.showToast?.(`Research error: ${err.message}`, 'error');
            },
        });
    }
}
```

#### Backend (Python)

The backend provides the API endpoints:

```python
# In app.py

@app.post("/api/exa/research")
async def exa_research(request: ExaResearchRequest):
    """Perform deep research using Exa's Research API.

    Returns SSE stream with research progress and final report.
    """
    async def generate():
        try:
            exa = Exa(api_key=request.api_key)

            # Create research task
            research = exa.research.create(
                instructions=request.instructions,
                model=request.model,
            )

            # Stream results
            yield {"event": "status", "data": "Research started..."}

            for event in exa.research.get(research.research_id, stream=True):
                if hasattr(event, "status"):
                    yield {"event": "status", "data": event.status}
                if hasattr(event, "output") and event.output:
                    formatted = format_research_output(event.output)
                    if formatted:
                        yield {"event": "content", "data": formatted}
                if hasattr(event, "sources") and event.sources:
                    sources_data = [
                        {"title": s.title, "url": s.url} for s in event.sources
                    ]
                    yield {"event": "sources", "data": json.dumps(sources_data)}

            yield {"event": "done", "data": ""}
        except Exception as e:
            yield {"event": "error", "data": str(e)}

    return EventSourceResponse(generate())
```

### Example 2: PDF File Upload Handler (Paired Plugin)

The PDF handler demonstrates a paired plugin for file uploads:

- **Frontend** (`pdf-node.js`): Handles file upload UI, creates PDF nodes
- **Backend** (`pdf_handler.py`): Processes PDF files, extracts text

#### PDF Handler Frontend (JavaScript)

```javascript
/**
 * PDF Node Plugin - Paired Plugin Example
 * Frontend handles file upload and node creation
 */

import { BaseNode } from '../node-protocols.js';
import { NodeRegistry } from '../node-registry.js';
import { FileUploadHandlerPlugin } from '../file-upload-handler-plugin.js';
import { FileUploadRegistry, PRIORITY } from '../file-upload-registry.js';
import { NodeType, createNode } from '../graph-types.js';
import { apiUrl } from '../utils.js';

// 1. Define PDF Node Protocol
class PdfNode extends BaseNode {
    getTypeLabel() {
        return 'PDF';
    }

    getTypeIcon() {
        return '📑';
    }

    getActions() {
        return [Actions.REPLY, Actions.SUMMARIZE, Actions.CREATE_FLASHCARDS, Actions.COPY];
    }
}

NodeRegistry.register({
    type: 'pdf',
    protocol: PdfNode,
    defaultSize: { width: 640, height: 480 },
});

// 2. Define File Upload Handler
class PdfFileUploadHandler extends FileUploadHandlerPlugin {
    async handleUpload(file, position = null, context = {}) {
        // Validate file
        if (file.type !== 'application/pdf') {
            throw new Error('Please select a PDF file.');
        }

        const MAX_SIZE = 25 * 1024 * 1024; // 25 MB
        this.validateFile(file, MAX_SIZE, 'PDF');

        // Create placeholder node
        const pdfNode = createNode(NodeType.PDF, `Processing PDF: ${file.name}...`, {
            position: position || this.graph.autoPosition([]),
        });
        this.addNodeToCanvas(pdfNode);

        try {
            // Upload to backend
            const formData = new FormData();
            formData.append('file', file);

            const response = await fetch(apiUrl('/api/upload-file'), {
                method: 'POST',
                body: formData,
            });

            if (!response.ok) {
                const error = await response.json();
                throw new Error(error.detail || 'Failed to process PDF');
            }

            const data = await response.json();

            // Update node with extracted content
            this.updateNodeAfterProcessing(pdfNode.id, data.content, {
                title: data.title,
                page_count: data.page_count,
            });

            return pdfNode;
        } catch (err) {
            this.handleError(pdfNode.id, file, err);
            throw err;
        }
    }
}

// Register file upload handler
FileUploadRegistry.register({
    id: 'pdf',
    mimeTypes: ['application/pdf'],
    extensions: ['.pdf'],
    handler: PdfFileUploadHandler,
    priority: PRIORITY.BUILTIN,
});
```

#### PDF Handler Backend (Python)

```python
# In plugins/pdf_handler.py

from canvas_chat.file_upload_handler_plugin import FileUploadHandlerPlugin
from canvas_chat.file_upload_registry import PRIORITY, FileUploadRegistry
import pymupdf

class PdfFileUploadHandler(FileUploadHandlerPlugin):
    """Handler for PDF file uploads."""

    async def process_file(self, file_bytes: bytes, filename: str) -> dict:
        """Extract text from PDF bytes.

        Returns:
            Dictionary with:
            - content: Extracted text with warning banner
            - title: Filename (without extension)
            - page_count: Number of pages
        """
        # Validate file size
        MAX_PDF_SIZE = 25 * 1024 * 1024
        self.validate_file_size(file_bytes, MAX_PDF_SIZE, "PDF")

        # Extract text from PDF
        text, page_count = self.extract_text_from_pdf(file_bytes)
        content = PDF_WARNING_BANNER + text

        # Extract title from filename
        title = filename.rsplit(".", 1)[0] if "." in filename else filename

        return {
            "content": content,
            "title": title,
            "page_count": page_count,
        }

    def extract_text_from_pdf(self, pdf_bytes: bytes) -> tuple[str, int]:
        """Extract text from PDF bytes using pymupdf."""
        doc = pymupdf.open(stream=pdf_bytes, filetype="pdf")
        page_count = len(doc)

        text_parts = []
        for page_num, page in enumerate(doc, start=1):
            page_text = page.get_text()
            if page_text.strip():
                text_parts.append(f"## Page {page_num}\n\n{page_text.strip()}")

        doc.close()

        full_text = (
            "\n\n".join(text_parts) if text_parts else "(No text content found in PDF)"
        )
        return full_text, page_count

# Register PDF handler
FileUploadRegistry.register(
    id="pdf",
    mime_types=["application/pdf"],
    extensions=[".pdf"],
    handler=PdfFileUploadHandler,
    priority=PRIORITY["BUILTIN"],
)
```

#### Backend Generic Endpoint

The backend provides a generic endpoint that routes to registered handlers:

```python
# In app.py

@app.post("/api/upload-file")
async def upload_file(file: UploadFile = File(...)):
    """Generic file upload endpoint that routes to registered handlers."""
    # Read file bytes
    file_bytes = await file.read()

    # Find handler for this file type
    handler_class = FileUploadRegistry.find_handler(
        mime_type=file.content_type,
        extension=Path(file.filename).suffix
    )

    if not handler_class:
        raise HTTPException(status_code=400, detail="Unsupported file type")

    # Instantiate and process
    handler = handler_class()
    result = await handler.process_file(file_bytes, file.filename)

    return result
```

### Configuration for Paired Plugins

```yaml
plugins:
  # Paired plugin with explicit ID
  - js: ./plugins/research.js
    py: ./plugins/research_handler.py
    id: research

  # Or let the system derive IDs from filenames
  - js: ./plugins/pdf-node.js
    py: ./plugins/pdf_handler.py
```

### How to Request a Research-Style Plugin

If you want to build a similar paired plugin that calls external APIs or performs server-side processing, here's a detailed prompt:

**Prompt Template:**

```text
I want to create a Canvas-Chat plugin for [DESCRIBE YOUR FEATURE] that requires both frontend UI and backend API calls.

Here's what I need:

1. **Frontend (JavaScript)**:
   - **Slash Command**: Add a slash command "/[COMMAND_NAME]" that [DESCRIBE WHAT IT DOES]
   - **Node Creation**: When the command runs, create a [NODE_TYPE] node that [DESCRIBE INITIAL STATE]
   - **Streaming Display**: The node should show [DESCRIBE HOW RESULTS APPEAR - e.g., "progress updates as they come in, then final results"]
   - **User Input**: The command accepts [DESCRIBE INPUT FORMAT]

2. **Backend (Python)**:
   - **API Endpoint**: Create a POST endpoint at "/api/[ENDPOINT_NAME]" that [DESCRIBE WHAT IT DOES]
   - **External Service**: The endpoint should call [DESCRIBE EXTERNAL API OR SERVICE]
   - **Streaming Response**: The endpoint should stream results using Server-Sent Events (SSE) with these event types:
     - "status": [DESCRIBE STATUS UPDATES - e.g., "progress messages like 'Searching...', 'Analyzing...'"]
     - "content": [DESCRIBE CONTENT CHUNKS - e.g., "pieces of the final report as they're generated"]
     - "sources": [DESCRIBE SOURCE DATA - e.g., "list of URLs and titles used"]
     - "done": [DESCRIBE COMPLETION - e.g., "signals when all processing is complete"]
   - **Error Handling**: If the external service fails, return an "error" event with the error message

3. **Integration**:
   - Frontend should call the backend endpoint with [DESCRIBE REQUEST PARAMETERS]
   - Frontend should handle streaming by updating the node content in real-time
   - Frontend should display final results when "done" event is received

4. **Configuration**:
   - The plugin may need [DESCRIBE CONFIGURATION - e.g., "API key from user settings"]
   - Fallback behavior: [DESCRIBE WHAT HAPPENS IF CONFIGURATION IS MISSING - e.g., "use alternative free service"]

Please create:
- A JavaScript file that extends FeaturePlugin and handles the slash command
- A Python endpoint in app.py that processes the request and streams results
- Proper error handling on both sides
- Following Canvas-Chat patterns for paired plugins
```

**Example for a Research Plugin:**

```text
I want to create a Canvas-Chat plugin for deep research that uses both frontend UI and backend API calls.

Here's what I need:

1. **Frontend (JavaScript)**:
   - **Slash Command**: Add a slash command "/research" that performs deep research on a topic
   - **Node Creation**: When the command runs, create a RESEARCH node that shows "Research: [topic]" and displays "Starting research..." initially
   - **Streaming Display**: The node should show status updates (like "Searching sources...", "Analyzing results..."), then stream in the research report content as it's generated, and finally show a list of sources at the bottom
   - **User Input**: The command accepts any research question or topic description

2. **Backend (Python)**:
   - **API Endpoint**: Create a POST endpoint at "/api/exa/research" that performs research using the Exa Research API
   - **External Service**: The endpoint should call Exa's Research API with the user's research instructions
   - **Streaming Response**: The endpoint should stream results using Server-Sent Events (SSE) with these event types:
     - "status": Progress messages like "Research started...", "Gathering sources...", "Generating report..."
     - "content": Chunks of the research report markdown as they're generated
     - "sources": JSON array of sources with title and URL: [{"title": "...", "url": "..."}]
     - "done": Empty event signaling completion
   - **Error Handling**: If Exa API fails, return an "error" event with the error message

3. **Integration**:
   - Frontend should call the backend endpoint with: instructions (the research question), api_key (from user settings), and model ("exa-research")
   - Frontend should handle streaming by accumulating content chunks and updating the node in real-time
   - Frontend should display final results with sources when "done" event is received

4. **Configuration**:
   - The plugin needs an Exa API key from user settings (storage.getExaApiKey())
   - Fallback behavior: If no Exa key is configured, use a DuckDuckGo-based research endpoint instead

Please create:
- A JavaScript file (research.js) that extends FeaturePlugin and handles the /research command
- A Python endpoint in app.py that calls Exa Research API and streams results
- Proper error handling on both sides
- Following Canvas-Chat patterns for paired plugins with SSE streaming
```

**Tips for Non-Programmers:**

- **Describe the Flow**: Explain what happens from start to finish - "user types command → node appears → status updates show → results stream in → final report displayed"
- **Be Specific About Data**: Instead of "show results," say "show a research report with paragraphs of text, then a list of source URLs at the bottom"
- **Mention External Services**: If you're using an external API, mention the service name and what it does
- **Describe Streaming**: Explain that results appear gradually, not all at once
- **Include Error Scenarios**: Mention what should happen if the API is down or credentials are missing

## Python-Only Plugins

Python-only plugins are backend-only and don't need a frontend component. They're useful for file upload handlers that don't create custom node types, or backend-only integrations.

### Example: CSV File Handler (Python-Only)

```python
# In plugins/csv_handler.py

from canvas_chat.file_upload_handler_plugin import FileUploadHandlerPlugin
from canvas_chat.file_upload_registry import PRIORITY, FileUploadRegistry
import csv
import io

class CsvFileUploadHandler(FileUploadHandlerPlugin):
    """Handler for CSV file uploads (backend-only processing)."""

    async def process_file(self, file_bytes: bytes, filename: str) -> dict:
        """Parse CSV and return structured data."""
        # Validate file size
        self.validate_file_size(file_bytes, 10 * 1024 * 1024, "CSV")

        # Parse CSV
        text = file_bytes.decode('utf-8')
        reader = csv.DictReader(io.StringIO(text))
        rows = list(reader)

        # Format as markdown table
        if not rows:
            content = "CSV file is empty."
        else:
            headers = list(rows[0].keys())
            content = "| " + " | ".join(headers) + " |\n"
            content += "| " + " | ".join(["---"] * len(headers)) + " |\n"
            for row in rows[:100]:  # Limit to 100 rows
                content += "| " + " | ".join(str(row.get(h, "")) for h in headers) + " |\n"
            if len(rows) > 100:
                content += f"\n*Showing first 100 of {len(rows)} rows*"

        return {
            "content": content,
            "title": filename.rsplit(".", 1)[0] if "." in filename else filename,
            "row_count": len(rows),
        }

# Register handler
FileUploadRegistry.register(
    id="csv",
    mime_types=["text/csv", "application/csv"],
    extensions=[".csv"],
    handler=CsvFileUploadHandler,
    priority=PRIORITY["BUILTIN"],
)
```

### Configuration for Python-Only Plugins

```yaml
plugins:
  - py: ./plugins/csv_handler.py
    id: csv
```

### How to Request a Python-Only File Handler

If you want to build a backend-only file handler (like the CSV handler), here's a detailed prompt:

**Prompt Template:**

```text
I want to create a backend-only file upload handler for [FILE_TYPE] files that processes uploaded files and displays them in standard Canvas-Chat nodes (no custom node type needed).

Here's what I need:

1. **File Processing**:
   - **Accepted Formats**: Files with extensions [LIST - e.g., ".csv"] and MIME types [LIST - e.g., "text/csv, application/csv"]
   - **Maximum Size**: [SIZE - e.g., "10 MB"]
   - **Processing Method**: [DESCRIBE HOW TO PROCESS - e.g., "Parse CSV using Python's csv module, convert to markdown table format"]

2. **Output Format**:
   - The processed content should be formatted as [DESCRIBE FORMAT - e.g., "a markdown table with headers and rows"]
   - Include [ADDITIONAL INFO - e.g., "row count, column names"]
   - If the file is empty or invalid: [DESCRIBE ERROR HANDLING]

3. **Node Display**:
   - The file should create a standard [NODE_TYPE - e.g., "NOTE"] node (not a custom type)
   - The node should display the processed content in [DESCRIBE FORMAT - e.g., "a scrollable markdown table"]

4. **Error Handling**:
   - If file is too large: Show error "[MESSAGE]"
   - If file format is invalid: Show error "[MESSAGE]"
   - If processing fails: Show error "[MESSAGE]"

5. **No Frontend Needed**: This is a Python-only plugin - the frontend will automatically use the generic file upload handler

Please create:
- A Python file that extends FileUploadHandlerPlugin
- Proper validation (file size, format)
- Processing logic that returns content in the specified format
- Error handling with clear messages
- Registration with FileUploadRegistry
- Following Canvas-Chat patterns for Python-only file handlers (similar to csv_handler.py)
```

**Example for a CSV Handler:**

```text
I want to create a backend-only file upload handler for CSV files that parses CSV data and displays it as a markdown table in a standard NOTE node.

Here's what I need:

1. **File Processing**:
   - **Accepted Formats**: Files with extensions [".csv"] and MIME types ["text/csv", "application/csv"]
   - **Maximum Size**: 10 MB
   - **Processing Method**: Parse CSV using Python's csv.DictReader module, then format the data as a markdown table with headers in the first row and data rows below

2. **Output Format**:
   - The processed content should be formatted as a markdown table
   - First row should be headers (column names)
   - Each subsequent row should be a data row
   - Include a note at the bottom showing total row count
   - If the file has more than 100 rows, only show the first 100 rows with a note saying "Showing first 100 of [total] rows"
   - If the file is empty: Return content "CSV file is empty."

3. **Node Display**:
   - The file should create a standard NOTE node (not a custom type)
   - The node should display the markdown table in a scrollable format

4. **Error Handling**:
   - If file is too large (>10 MB): Raise ValueError with message "CSV file exceeds 10 MB limit"
   - If file format is invalid (not valid CSV): Raise ValueError with message "Invalid CSV format: [error details]"
   - If processing fails: Raise ValueError with message "Failed to process CSV file: [error details]"

5. **No Frontend Needed**: This is a Python-only plugin - the frontend will automatically use the generic file upload handler to upload files and create NOTE nodes

Please create:
- A Python file (csv_handler.py) that extends FileUploadHandlerPlugin
- Proper validation using validate_file_size() method
- CSV parsing logic using csv.DictReader
- Markdown table formatting
- Error handling with clear messages
- Registration with FileUploadRegistry for CSV MIME types and extensions
- Following Canvas-Chat patterns for Python-only file handlers
```

**Tips for Non-Programmers:**

- **Focus on Output**: Describe what the final result should look like (e.g., "a table with data")
- **Mention Limits**: Specify size limits and row limits if applicable
- **Describe Format**: Explain how data should be formatted (table, list, plain text, etc.)
- **Error Messages**: Describe what error messages users should see
- **No Frontend Needed**: Emphasize that this is backend-only - the frontend handles uploads automatically

### How to Request a File Upload Handler Plugin (Paired)

If you want to build a file upload handler (either Python-only or paired), here's a detailed prompt:

**Prompt Template for Paired File Upload Handler:**

```text
I want to create a Canvas-Chat plugin for uploading and processing [FILE_TYPE] files.

Here's what I need:

1. **Frontend (JavaScript)**:
   - **Node Type**: Create a custom node type called "[NODE_NAME]" that displays [DESCRIBE WHAT THE NODE SHOWS - e.g., "the extracted text content from the file"]
   - **File Upload Handler**: Register a handler that accepts [FILE_EXTENSIONS - e.g., ".doc, .docx"] files with MIME types [MIME_TYPES - e.g., "application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document"]
   - **Upload Process**: When a user drops or selects a [FILE_TYPE] file:
     - Show a placeholder node with "Processing [filename]..." message
     - Upload the file to the backend endpoint "/api/upload-file"
     - Update the node with extracted content when processing completes
   - **File Validation**:
     - Maximum file size: [SIZE - e.g., "10 MB"]
     - Accepted formats: [LIST FORMATS]

2. **Backend (Python)**:
   - **File Handler Class**: Create a class that extends FileUploadHandlerPlugin
   - **Processing Method**: Implement process_file() that:
     - Validates file size (max [SIZE])
     - Extracts/processes the file content using [DESCRIBE PROCESSING - e.g., "python-docx library to extract text from Word documents"]
     - Returns a dictionary with:
       - "content": [DESCRIBE CONTENT FORMAT - e.g., "extracted text as markdown"]
       - "title": [DESCRIBE TITLE - e.g., "filename without extension"]
       - [ADDITIONAL FIELDS - e.g., "page_count", "word_count", etc.]
   - **Error Handling**: If processing fails, raise a ValueError with a clear error message
   - **Registration**: Register the handler with FileUploadRegistry for the specified MIME types and extensions

3. **Node Actions**: The [NODE_NAME] node should support these actions:
   - [ACTION 1 - e.g., "Reply" - to chat about the content]
   - [ACTION 2 - e.g., "Summarize" - to create a summary]
   - [ACTION 3 - e.g., "Copy" - to copy content]

4. **Visual Design**:
   - Node icon: [EMOJI OR DESCRIPTION]
   - Node default size: [WIDTH] x [HEIGHT] pixels
   - Content display: [DESCRIBE HOW CONTENT IS SHOWN - e.g., "formatted markdown with proper line breaks"]

Please create:
- A JavaScript file that defines the node type and file upload handler
- A Python file that processes the uploaded file
- Proper error handling and validation
- Following Canvas-Chat patterns for file upload plugins
```

**Example for a Word Document Handler:**

```text
I want to create a Canvas-Chat plugin for uploading and processing Word document files (.doc and .docx).

Here's what I need:

1. **Frontend (JavaScript)**:
   - **Node Type**: Create a custom node type called "Word Document" that displays the extracted text content from the Word file
   - **File Upload Handler**: Register a handler that accepts .doc and .docx files with MIME types "application/msword" and "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
   - **Upload Process**: When a user drops or selects a Word file:
     - Show a placeholder node with "Processing: [filename]..." message
     - Upload the file to the backend endpoint "/api/upload-file" using FormData
     - Update the node with extracted text content when processing completes
   - **File Validation**:
     - Maximum file size: 10 MB
     - Accepted formats: .doc (older Word format) and .docx (modern Word format)

2. **Backend (Python)**:
   - **File Handler Class**: Create a class called WordFileUploadHandler that extends FileUploadHandlerPlugin
   - **Processing Method**: Implement process_file() that:
     - Validates file size (max 10 MB)
     - Extracts text from the Word document using the python-docx library
     - Processes all paragraphs and combines them into a single text string
     - Returns a dictionary with:
       - "content": The extracted text with paragraphs separated by double line breaks
       - "title": The filename without the .doc or .docx extension
   - **Error Handling**: If the file is corrupted or not a valid Word document, raise a ValueError with message "Failed to extract text from Word document: [error details]"
   - **Registration**: Register the handler with FileUploadRegistry for MIME types ["application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"] and extensions [".doc", ".docx"]

3. **Node Actions**: The Word Document node should support these actions:
   - "Reply" - to chat about the document content
   - "Summarize" - to create a summary of the document
   - "Copy" - to copy the document text

4. **Visual Design**:
   - Node icon: 📄 (document emoji)
   - Node default size: 640 x 480 pixels
   - Content display: Plain text with proper paragraph breaks, scrollable if content is long

Please create:
- A JavaScript file (word-node.js) that defines the WordNode class and WordFileUploadHandler class
- A Python file (word_handler.py) that processes Word documents using python-docx
- Proper error handling for invalid files, size limits, and processing errors
- Following Canvas-Chat patterns for file upload plugins (similar to pdf-node.js and pdf_handler.py)
```

**Prompt Template for Python-Only File Handler:**

```text
I want to create a backend-only file upload handler for [FILE_TYPE] files that doesn't need a custom node type.

Here's what I need:

1. **Backend (Python)**:
   - **File Handler Class**: Create a class that extends FileUploadHandlerPlugin
   - **Processing Method**: Implement process_file() that:
     - Validates file size (max [SIZE])
     - Processes the file using [DESCRIBE PROCESSING METHOD]
     - Returns a dictionary with:
       - "content": [DESCRIBE CONTENT FORMAT - e.g., "formatted markdown table"]
       - "title": [DESCRIBE TITLE - e.g., "filename without extension"]
       - [ADDITIONAL FIELDS if needed]
   - **File Types**: Accept files with extensions [LIST] and MIME types [LIST]
   - **Error Handling**: Raise ValueError with clear messages for invalid files
   - **Registration**: Register with FileUploadRegistry

2. **Node Behavior**:
   - The file should create a standard [NODE_TYPE - e.g., "NOTE"] node (not a custom type)
   - The node should display the processed content

3. **No Frontend Needed**: This is a Python-only plugin - no JavaScript file required

Please create:
- A Python file that processes the uploaded file
- Proper validation and error handling
- Following Canvas-Chat patterns for Python-only file upload handlers
```

**Tips for Non-Programmers:**

- **Specify File Types Clearly**: List exact file extensions (.doc, .docx) and describe what they are
- **Describe Processing**: Explain what should be extracted (text, images, metadata, etc.)
- **Mention File Size Limits**: Specify maximum file size in MB
- **Describe Output Format**: Explain how the extracted content should be formatted (plain text, markdown, table, etc.)
- **Include Error Cases**: Mention what should happen if the file is corrupted, too large, or wrong format
- **Specify Node Appearance**: Describe the icon, size, and how content should be displayed

## Making API Calls from JavaScript to Python

When building paired plugins, you'll need to call backend API endpoints from your JavaScript code. Here are the patterns:

### Pattern 1: Simple JSON Request/Response

```javascript
// In your FeaturePlugin

async handleCommand(command, args, contextObj) {
    const response = await fetch(apiUrl('/api/my-endpoint'), {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            query: args,
            context: contextObj?.text || null,
        }),
    });

    if (!response.ok) {
        throw new Error(`Request failed: ${response.statusText}`);
    }

    const data = await response.json();
    // Use data...
}
```

### Pattern 2: Server-Sent Events (SSE) Streaming

For streaming responses (like research, LLM generation):

```javascript
import { readSSEStream } from '../sse.js';

async handleCommand(command, args, contextObj) {
    const response = await fetch(apiUrl('/api/stream-endpoint'), {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            input: args,
        }),
    });

    if (!response.ok) {
        throw new Error(`Request failed: ${response.statusText}`);
    }

    let accumulatedContent = '';

    await readSSEStream(response, {
        onEvent: (eventType, data) => {
            if (eventType === 'content') {
                accumulatedContent += data;
                this.canvas.updateNodeContent(nodeId, accumulatedContent, true);
            } else if (eventType === 'status') {
                // Update status
            }
        },
        onDone: () => {
            this.canvas.updateNodeContent(nodeId, accumulatedContent, false);
            this.saveSession?.();
        },
        onError: (err) => {
            this.showToast?.(`Error: ${err.message}`, 'error');
        },
    });
}
```

### Pattern 3: File Upload (FormData)

For file uploads:

```javascript
async handleUpload(file, position, context) {
    const formData = new FormData();
    formData.append('file', file);

    const response = await fetch(apiUrl('/api/upload-file'), {
        method: 'POST',
        body: formData, // Don't set Content-Type header - browser sets it with boundary
    });

    if (!response.ok) {
        const error = await response.json();
        throw new Error(error.detail || 'Upload failed');
    }

    const data = await response.json();
    // Use data...
}
```

### Pattern 4: Using LLM Request Builder

For endpoints that need LLM request format:

```javascript
async handleCommand(command, args, contextObj) {
    const response = await fetch(apiUrl('/api/llm-endpoint'), {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(
            this.buildLLMRequest({
                user_query: args,
                context: contextObj?.text || null,
                // ... other params
            })
        ),
    });
    // ...
}
```

The `buildLLMRequest()` method (from `FeaturePlugin`) automatically includes:

- Model selection
- API key (from storage or admin mode)
- Base URL (if configured)
- Other LLM request parameters

## Configuration

### Plugin Configuration Formats

Canvas-Chat supports three configuration formats for plugins:

#### Format 1: JavaScript-Only (Backwards Compatible)

```yaml
plugins:
  - path: ./plugins/my-plugin.js
  # OR
  - ./plugins/my-plugin.js
```

#### Format 2: Paired JavaScript + Python

```yaml
plugins:
  - js: ./plugins/research.js
    py: ./plugins/research_handler.py
    id: research  # Optional: explicit plugin ID
```

#### Format 3: Python-Only

```yaml
plugins:
  - py: ./plugins/csv_handler.py
    id: csv  # Optional: explicit plugin ID
```

### Plugin ID

The `id` field is optional. If not provided:

- For JS-only: Derived from JS filename (e.g., `my-plugin.js` → `my-plugin`)
- For Python-only: Derived from Python filename (e.g., `csv_handler.py` → `csv_handler`)
- For paired: Derived from JS filename (Python filename is ignored)

**Use explicit `id` when:**

- JS and Python filenames don't match (e.g., `research.js` + `research_backend.py`)
- You want a specific plugin ID for pairing

### Loading Order

1. **Python plugins** are loaded at application startup (when config is loaded)
2. **JavaScript plugins** are injected into HTML and loaded by the browser
3. **Registration** happens when JavaScript modules execute

## Testing Your Plugin

### Testing JavaScript Plugins

Use `PluginTestHarness` for unit testing:

```javascript
import { PluginTestHarness } from '../src/canvas_chat/static/js/plugin-test-harness.js';
import { MyFeature } from './my-feature.js';

asyncTest('MyFeature handles command', async () => {
    const harness = new PluginTestHarness();

    await harness.loadPlugin({
        id: 'my-feature',
        feature: MyFeature,
        slashCommands: [
            { command: '/mycommand', handler: 'handleCommand' },
        ],
    });

    await harness.executeCommand('/mycommand', 'test args', {
        text: 'context',
    });

    const nodes = harness.mockCanvas.getRenderedNodes();
    assertTrue(nodes.length === 1);

    await harness.unloadPlugin('my-feature');
});
```

### Testing Python Plugins

Test Python file upload handlers:

```python
# In tests/test_csv_handler.py

from canvas_chat.plugins.csv_handler import CsvFileUploadHandler

async def test_csv_handler():
    handler = CsvFileUploadHandler()

    # Create test CSV
    csv_content = b"name,age\nAlice,30\nBob,25"

    result = await handler.process_file(csv_content, "test.csv")

    assert "name" in result["content"]
    assert "age" in result["content"]
    assert result["row_count"] == 2
```

### Manual Testing

1. **Start the server:**

   ```bash
   pixi run python -m canvas_chat launch --config config.yaml
   ```

2. **Load your plugin:**
   - Add to `config.yaml`
   - Restart server
   - Check browser console for plugin loading messages

3. **Test functionality:**
   - Try slash commands
   - Test file uploads
   - Verify API calls work
   - Check browser Network tab for API requests

## Best Practices

### JavaScript Plugins

1. **Use AppContext APIs:**

   ```javascript
   // ✅ DO: Use injected APIs
   this.graph.addNode(node);
   this.canvas.renderNode(node);
   this.chat.sendMessage(...);

   // ❌ DON'T: Access globals directly
   window.app.graph.addNode(node);
   ```

2. **Handle Errors Gracefully:**

   ```javascript
   try {
       await this.doAsyncWork();
   } catch (error) {
       this.showToast?.(`Error: ${error.message}`, 'error');
       console.error('[MyFeature] Error:', error);
   }
   ```

3. **Use Map for Concurrent Operations:**

   ```javascript
   // ✅ DO: Per-instance state
   this.activeOperations = new Map();

   // ❌ DON'T: Single variable
   this.currentOperation = null; // Only one operation can run
   ```

4. **Clean Up in onUnload():**

   ```javascript
   async onUnload() {
       // Abort operations
       for (const [id, op] of this.activeOperations.entries()) {
           op.abortController.abort();
       }
       this.activeOperations.clear();
   }
   ```

### Python Plugins

1. **Validate Input:**

   ```python
   async def process_file(self, file_bytes: bytes, filename: str) -> dict:
       # Always validate file size
       self.validate_file_size(file_bytes, MAX_SIZE, "FileType")

       # Validate file format
       if not self.is_valid_format(file_bytes):
           raise ValueError("Invalid file format")
   ```

2. **Handle Errors:**

   ```python
   async def process_file(self, file_bytes: bytes, filename: str) -> dict:
       try:
           # Process file
           result = self.extract_data(file_bytes)
           return {"content": result, "title": filename}
       except Exception as e:
           logger.error(f"Failed to process {filename}: {e}")
           raise HTTPException(status_code=500, detail=str(e))
   ```

3. **Log Important Events:**

   ```python
   logger.info(f"Processing {filename} ({len(file_bytes)} bytes)")
   logger.warning(f"Large file: {filename} ({len(file_bytes)} bytes)")
   ```

### Paired Plugins

1. **Keep IDs Consistent:**
   - Use explicit `id` in config if filenames don't match
   - Ensure frontend and backend use the same identifier

2. **Error Handling:**
   - Frontend should handle API errors gracefully
   - Backend should return clear error messages
   - Use appropriate HTTP status codes

3. **API Design:**
   - Use RESTful endpoints (`/api/resource/action`)
   - Return consistent JSON structures
   - Use SSE for streaming responses

## Complete Example: Word Document Handler

Here's a complete example of a paired plugin for Word document uploads, along with the prompt that would generate it:

### Frontend (`word-node.js`)

```javascript
import { BaseNode, Actions } from '../node-protocols.js';
import { NodeRegistry } from '../node-registry.js';
import { FileUploadHandlerPlugin } from '../file-upload-handler-plugin.js';
import { FileUploadRegistry, PRIORITY } from '../file-upload-registry.js';
import { NodeType, createNode } from '../graph-types.js';
import { apiUrl } from '../utils.js';

class WordNode extends BaseNode {
    getTypeLabel() {
        return 'Word Document';
    }

    getTypeIcon() {
        return '📄';
    }

    getActions() {
        return [Actions.REPLY, Actions.SUMMARIZE, Actions.COPY];
    }
}

NodeRegistry.register({
    type: 'word',
    protocol: WordNode,
    defaultSize: { width: 640, height: 480 },
});

class WordFileUploadHandler extends FileUploadHandlerPlugin {
    async handleUpload(file, position = null, context = {}) {
        // Validate MIME types for Word documents
        const validTypes = [
            'application/msword',
            'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
        ];

        if (!validTypes.includes(file.type)) {
            throw new Error('Please select a Word document (.doc or .docx)');
        }

        const MAX_SIZE = 10 * 1024 * 1024; // 10 MB
        this.validateFile(file, MAX_SIZE, 'Word document');

        const wordNode = createNode(NodeType.WORD, `Processing: ${file.name}...`, {
            position: position || this.graph.autoPosition([]),
        });
        this.addNodeToCanvas(wordNode);

        try {
            const formData = new FormData();
            formData.append('file', file);

            const response = await fetch(apiUrl('/api/upload-file'), {
                method: 'POST',
                body: formData,
            });

            if (!response.ok) {
                const error = await response.json();
                throw new Error(error.detail || 'Failed to process Word document');
            }

            const data = await response.json();
            this.updateNodeAfterProcessing(wordNode.id, data.content, {
                title: data.title,
            });

            return wordNode;
        } catch (err) {
            this.handleError(wordNode.id, file, err);
            throw err;
        }
    }
}

FileUploadRegistry.register({
    id: 'word',
    mimeTypes: [
        'application/msword',
        'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    ],
    extensions: ['.doc', '.docx'],
    handler: WordFileUploadHandler,
    priority: PRIORITY.COMMUNITY,
});
```

### Backend (`word_handler.py`)

```python
from canvas_chat.file_upload_handler_plugin import FileUploadHandlerPlugin
from canvas_chat.file_upload_registry import PRIORITY, FileUploadRegistry
from docx import Document
import io
import logging

logger = logging.getLogger(__name__)

class WordFileUploadHandler(FileUploadHandlerPlugin):
    """Handler for Word document uploads."""

    async def process_file(self, file_bytes: bytes, filename: str) -> dict:
        """Extract text from Word document.

        Returns:
            Dictionary with:
            - content: Extracted text
            - title: Filename (without extension)
        """
        MAX_SIZE = 10 * 1024 * 1024  # 10 MB
        self.validate_file_size(file_bytes, MAX_SIZE, "Word document")

        try:
            # Parse Word document
            doc = Document(io.BytesIO(file_bytes))

            # Extract text from all paragraphs
            text_parts = []
            for para in doc.paragraphs:
                if para.text.strip():
                    text_parts.append(para.text.strip())

            content = "\n\n".join(text_parts) if text_parts else "(No text content found)"

            # Extract title from filename
            title = filename.rsplit(".", 1)[0] if "." in filename else filename

            logger.info(f"Successfully extracted text from Word document: {filename}")

            return {
                "content": content,
                "title": title,
            }
        except Exception as e:
            logger.error(f"Failed to process Word document {filename}: {e}")
            raise ValueError(f"Failed to extract text from Word document: {str(e)}")

# Register handler
FileUploadRegistry.register(
    id="word",
    mime_types=[
        "application/msword",
        "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
    ],
    extensions=[".doc", ".docx"],
    handler=WordFileUploadHandler,
    priority=PRIORITY["COMMUNITY"],
)
```

### Word Plugin Configuration

```yaml
plugins:
  - js: ./plugins/word-node.js
    py: ./plugins/word_handler.py
    id: word
```

### How to Request the Word Plugin

**The Prompt That Generated This Example:**

```text
I want to create a Canvas-Chat plugin for uploading and processing Word document files (.doc and .docx).

Here's what I need:

1. **Frontend (JavaScript)**:
   - **Node Type**: Create a custom node type called "Word Document" that displays the extracted text content from the Word file
   - **File Upload Handler**: Register a handler that accepts .doc and .docx files with MIME types "application/msword" and "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
   - **Upload Process**: When a user drops or selects a Word file:
     - Show a placeholder node with "Processing: [filename]..." message
     - Upload the file to the backend endpoint "/api/upload-file" using FormData
     - Update the node with extracted text content when processing completes
   - **File Validation**:
     - Maximum file size: 10 MB
     - Accepted formats: .doc (older Word format) and .docx (modern Word format)

2. **Backend (Python)**:
   - **File Handler Class**: Create a class called WordFileUploadHandler that extends FileUploadHandlerPlugin
   - **Processing Method**: Implement process_file() that:
     - Validates file size (max 10 MB)
     - Extracts text from the Word document using the python-docx library
     - Processes all paragraphs and combines them into a single text string
     - Returns a dictionary with:
       - "content": The extracted text with paragraphs separated by double line breaks
       - "title": The filename without the .doc or .docx extension
   - **Error Handling**: If the file is corrupted or not a valid Word document, raise a ValueError with message "Failed to extract text from Word document: [error details]"
   - **Registration**: Register the handler with FileUploadRegistry for MIME types ["application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document"] and extensions [".doc", ".docx"]

3. **Node Actions**: The Word Document node should support these actions:
   - "Reply" - to chat about the document content
   - "Summarize" - to create a summary of the document
   - "Copy" - to copy the document text

4. **Visual Design**:
   - Node icon: 📄 (document emoji)
   - Node default size: 640 x 480 pixels
   - Content display: Plain text with proper paragraph breaks, scrollable if content is long

Please create:
- A JavaScript file (word-node.js) that defines the WordNode class and WordFileUploadHandler class
- A Python file (word_handler.py) that processes Word documents using python-docx
- Proper error handling for invalid files, size limits, and processing errors
- Following Canvas-Chat patterns for file upload plugins (similar to pdf-node.js and pdf_handler.py)
```

**Key Elements of a Good Prompt:**

1. **Clear Feature Description**: "Word document uploads" is specific and clear
2. **Detailed Requirements**: Lists exact file types, sizes, and formats
3. **Step-by-Step Process**: Describes what happens from upload to display
4. **Technical Details**: Mentions specific libraries (python-docx) and endpoints
5. **Error Handling**: Specifies what should happen when things go wrong
6. **Visual Specifications**: Describes icon, size, and content display
7. **Reference Examples**: Mentions similar plugins (pdf-node.js) for context

## Next Steps

- Read [How to Create Feature Plugins](./create-feature-plugins.md) for detailed feature plugin development
- See [Feature Plugin API Reference](../reference/feature-plugin-api.md) for complete API documentation
- Study built-in plugins: `research.js`, `pdf-node.js`, `poll.js`
- Check [Plugin Architecture Explanation](../explanation/plugin-architecture.md) for design rationale

## Troubleshooting

### Plugin Not Loading

- **Check browser console** for import errors
- **Verify file paths** in `config.yaml` are correct
- **Check file permissions** (Python plugins need to be importable)
- **Verify exports** - JavaScript plugins must export classes

### API Calls Failing

- **Check Network tab** in browser DevTools
- **Verify endpoint exists** in `app.py`
- **Check CORS** if calling from different origin
- **Verify request format** matches backend expectations

### Python Plugin Not Registering

- **Check imports** - Python plugins must import registry
- **Verify registration code** runs at module import time
- **Check logs** for import errors
- **Ensure plugin is in Python path**

### File Upload Not Working

- **Verify MIME types** match registered handlers
- **Check file size limits** in handler validation
- **Verify FormData** is sent correctly (don't set Content-Type header)
- **Check backend logs** for processing errors
