Files
wp-agentic-writer/IMPLEMENTATION_PLAN-block-refinement-hybrid.md
2026-01-28 00:26:00 +07:00

25 KiB

Implementation Plan: Hybrid Block Refinement with @ Mention Support

Overview

Implement a hybrid refinement system that combines:

  1. Current workflow: "AI Refine" button in block toolbar (beginner-friendly)
  2. New workflow: @block mentions in chat (power-user friendly)

This provides both discoverability for beginners and speed for experts.


Current State Analysis

Existing Refinement Flow

  • Location: assets/js/block-refine.js
  • Trigger: Click "AI Refine" in block toolbar
  • Flow: Modal opens → Type request → Submit → Block replaced
  • Status: Working correctly after recent fixes

Chat System

  • Location: assets/js/sidebar.js
  • Current behavior: Article generation and chat messages
  • Status: Working with tabbed interface

Implementation Plan

Phase 1: Backend - Add Refinement Endpoint to Chat System

File: includes/class-gutenberg-sidebar.php

Changes Needed:

  1. Add new REST endpoint: /refine-from-chat (or modify /generate-plan to handle refinement requests)
  2. Parse mentions from chat messages: Extract @block-ref patterns
  3. Handle special mention syntax:
    • @this → Currently selected block
    • @previous → Previous block
    • @next → Next block
    • @all → All blocks
    • @paragraph-1, @heading-2 → Block by sequential ID

API Structure:

{
  "topic": "Refine @this to be more engaging",
  "context": "User's full message with mentions",
  "postId": 123,
  "selectedBlockClientId": "abc123",
  "stream": true
}

Phase 2: Frontend - Add @ Mention Detection

File: assets/js/sidebar.js

Add to sendMessage() function:

  1. Detect @ mentions in user input
  2. Extract mention patterns:
    const mentionRegex = /@(\w+(?:-\d+)?|this|previous|next|all)/g;
    
  3. Check if message contains refinement keywords:
    • "refine", "rewrite", "edit", "improve", "change", "make it"
  4. If both detected → Treat as refinement request

New Message Handler:

const handleRefinementRequest = async (message) => {
    // Extract mentions
    const mentions = message.match( /@(\w+(?:-\d+)?|this|previous|next|all)/g );

    // Resolve to actual block client IDs
    const blocksToRefine = resolveMentionsToBlocks( mentions );

    if ( blocksToRefine.length === 0 ) {
        // No valid mentions, treat as normal chat
        return false;
    }

    // Call refinement endpoint
    await refineBlocksFromChat( blocksToRefine, message );
    return true; // Handled as refinement
};

Phase 3: Backend - Chat Refinement Endpoint

New Method: refine_from_chat() in class-gutenberg-sidebar.php

Process:

  1. Receive chat message with mentions
  2. Resolve mentions to actual blocks
  3. Call existing stream_block_refine() for each block
  4. Stream responses back to chat

Implementation:

public function handle_refine_from_chat( $request ) {
    $params = $request->get_json_params();
    $message = $params['topic'] ?? '';
    $selected_block = $params['selectedBlockClientId'] ?? '';
    $post_id = $params['postId'] ?? 0;

    // Parse mentions from message
    preg_match_all( '/@(\w+(?:-\d+)?|this|previous|next|all)/i', $message, $matches );

    // Resolve mentions to block client IDs
    $blocks_to_refine = $this->resolve_mentions_to_blocks( $matches, $selected_block, $post_id );

    if ( empty( $blocks_to_refine ) ) {
        // No blocks mentioned, treat as normal chat
        return $this->handle_generate_plan( $request );
    }

    // Stream refinement for each mentioned block
    $this->stream_refinement_from_chat( $blocks_to_refine, $message, $post_id );
}

Phase 4: Block Resolution Logic

New Method: resolve_mentions_to_blocks() in class-gutenberg-sidebar.php

Resolution Rules:

private function resolve_mentions_to_blocks( $mentions, $selected_block, $post_id ) {
    $all_blocks = get_blocks( $post_id );
    $resolved = array();

    foreach ( $mentions as $mention ) {
        $mention_type = strtolower( $mention[1] );

        switch ( $mention_type ) {
            case 'this':
                if ( $selected_block ) {
                    $resolved[] = $selected_block;
                }
                break;

            case 'previous':
                if ( $selected_block ) {
                    $index = array_search( $selected_block, $all_blocks );
                    if ( $index > 0 ) {
                        $resolved[] = $all_blocks[ $index - 1 ]['clientId'];
                    }
                }
                break;

            case 'next':
                if ( $selected_block ) {
                    $index = array_search( $selected_block, $all_blocks );
                    if ( $index < count( $all_blocks ) - 1 ) {
                        $resolved[] = $all_blocks[ $index + 1 ]['clientId'];
                    }
                }
                break;

            case 'all':
                // Return all paragraph and heading blocks
                foreach ( $all_blocks as $block ) {
                    if ( in_array( $block['name'], array( 'core/paragraph', 'core/heading' ) ) ) {
                        $resolved[] = $block['clientId'];
                    }
                }
                break;

            default:
                // Handle sequential mentions like "paragraph-1", "heading-2"
                if ( preg_match( '/^(\w+)-(\d+)$/', $mention_type, $matches ) ) {
                    $block_type = $matches[1]; // "paragraph", "heading"
                    $index = (int) $matches[2] - 1; // Convert to 0-based

                    foreach ( $all_blocks as $block ) {
                        if ( $block['name'] === 'core/' . $block_type ) ) {
                            if ( $index === 0 ) {
                                $resolved[] = $block['clientId'];
                                $index--;
                            }
                        }
                    }
                }
                break;
        }
    }

    return array_unique( $resolved );
}

Phase 5: Visual Feedback

5.1. Block Highlighting

File: assets/css/editor.css

Add CSS for mentioned blocks:

.wpaw-block-mentioned {
    outline: 2px solid #2271b1 !important;
    outline-offset: 2px;
    box-shadow: 0 0 0 4px rgba(34, 113, 177, 0.2);
    animation: wpaw-pulse 1.5s infinite;
}

@keyframes wpaw-pulse {
    0%, 100% { box-shadow: 0 0 0 0px rgba(34, 113, 177, 0.2); }
    50% { box-shadow: 0 0 0 6px rgba(34, 113, 177, 0.4); }
}

5.2. Autocomplete UI

File: assets/js/sidebar.js

Add mention autocomplete:

// When user types @ in chat input
const showMentionAutocomplete = (searchTerm) => {
    const allBlocks = wp.data.select( 'core/block-editor' ).getBlocks();
    const filtered = allBlocks
        .filter( b => b.name === 'core/paragraph' || b.name === 'core/heading' )
        .map( ( b, index ) => ( {
            const label = b.attributes.content || b.name;
            const shortLabel = b.name === 'core/paragraph'
                ? `Paragraph ${index + 1}`
                : `Heading ${index + 1}: ${label}`;

            return {
                id: `${b.name}-${index}`,
                label: `${shortLabel}: "${label.substring( 0, 30 )}"`,
                clientId: b.clientId,
            };
        } ) );

    // Show dropdown with filtered options
    showAutocompleteDropdown( filtered );
};

Phase 6: Chat Integration

Modify: sendMessage() in assets/js/sidebar.js

Flow:

const sendMessage = async () => {
    const userMessage = input.trim();

    // Check if this is a refinement request
    const isRefinement = /refine|rewrite|edit|improve|change|make (it|them|this)/i.test( userMessage );
    const hasMentions = /@/.test( userMessage );

    if ( isRefinement && hasMentions ) {
        // Handle as refinement request
        await handleRefinementRequest( userMessage );
    } else {
        // Handle as normal chat/generation
        // ...existing chat logic...
    }
};

Phase 7: Enhanced Chat Experience

Add refinement summary to chat:

When refinement is requested:

  1. Add user message as chat bubble: "Refine @paragraph-3 to be more engaging"
  2. Add AI response: "✓ I've refined paragraph 3"
  3. Update block content in editor
  4. Show cost in Cost tab

Example Chat Flow:

User: Refine @this to be more concise

[Message added to chat]

AI: ✓ Refining current paragraph...

[Status timeline showing progress]

AI: ✅ Done! I've made the paragraph more concise while keeping the key information.

[Block updated in editor]

Detailed Implementation

Step 1: Backend Chat Refinement Endpoint

File: includes/class-gutenberg-sidebar.php

Add new REST route:

register_rest_route(
    'wp-agentic-writer/v1',
    '/refine-from-chat',
    array(
        'methods'  => 'POST',
        'callback' => array( $this, 'handle_refine_from_chat' ),
        'permission_callback' => array( $this, 'check_edit_permission' ),
    )
);

Implementation:

  • Handle chat messages with @ mentions
  • Parse and resolve mentions to block client IDs
  • Stream refinement responses back to chat
  • Update blocks in real-time

Step 2: Frontend Mention Detection

File: assets/js/sidebar.js

Add to sendMessage() function:

// Detect if message is refinement request with mentions
const isRefinementWithMentions = /@(\w+(?:-\d+)?|this|previous|next|all)/i.test( userMessage ) &&
    /refine|rewrite|edit|improve|change|make/i.test( userMessage );

if ( isRefinementWithMentions ) {
    // Handle as refinement
    await handleChatRefinement( userMessage, selectedBlockClientId );
    return;
}

Step 3: Block Mention Resolution

Create helper function in assets/js/sidebar.js:

const resolveBlockMentions = ( mentions, selectedBlockId ) => {
    const allBlocks = wp.data.select( 'core/block-editor' ).getBlocks();
    const resolved = [];

    mentions.forEach( mention => {
        const type = mention.toLowerCase();
        const match = mention.match( /^(\w+)-(\d+)$/ );

        switch ( type ) {
            case 'this':
                if ( selectedBlockId ) resolved.push( selectedBlockId );
                break;
            case 'previous':
                const selectedIndex = allBlocks.findIndex( b => b.clientId === selectedBlockId );
                if ( selectedIndex > 0 ) {
                    resolved.push( allBlocks[ selectedIndex - 1 ].clientId );
                }
                break;
            case 'next':
                const selectedIndex = allBlocks.findIndex( b => b.clientId === selectedBlockId );
                if ( selectedIndex < allBlocks.length - 1 ) {
                    resolved.push( allBlocks[ selectedIndex + 1 ].clientId );
                }
                break;
            case 'all':
                allBlocks.forEach( ( block, index ) => {
                    if ( block.name === 'core/paragraph' || block.name === 'core/heading' ) {
                        resolved.push( block.clientId );
                    }
                } );
                break;
            default:
                // Handle "paragraph-1", "heading-2" format
                if ( match ) {
                    const blockType = 'core/' + match[1]; // paragraph or heading
                    const blockIndex = parseInt( match[2] ) - 1; // 1-based to 0-based

                    let currentIndex = 0;
                    allBlocks.forEach( ( block ) => {
                        if ( block.name === blockType ) {
                            if ( currentIndex === blockIndex ) {
                                resolved.push( block.clientId );
                            }
                            currentIndex++;
                        }
                    } );
                }
                break;
        }
    } );

    return [ ...new Set( resolved ) ]; // Remove duplicates
};

Step 4: Chat Refinement Handler

Create new function in assets/js/sidebar.js:

const handleChatRefinement = async ( message, selectedBlockId ) => {
    // Parse mentions from message
    const mentionRegex = /@(\w+(?:-\d+)?|this|previous|next|all)/gi;
    const mentions = [...message.matchAll( mentionRegex )].map( m => m[0] );

    // Resolve to block client IDs
    const blocksToRefine = resolveBlockMentions( mentions, selectedBlockId );

    if ( blocksToRefine.length === 0 ) {
        // No valid mentions found
        alert( 'No valid blocks found to refine. Try mentioning blocks like @this, @previous, or specific blocks like @paragraph-1' );
        return;
    }

    // Add user message to chat
    setMessages( [ ...messages, { role: 'user', content: message } ] );

    // Add timeline entry
    setMessages( prev => [ ...prev, {
        role: 'system',
        type: 'timeline',
        status: 'refining',
        message: `Refining ${blocksToRefine.length} block(s)...`,
        icon: '✏️',
    } ] );

    setIsLoading( true );

    try {
        // Call refinement endpoint
        const response = await fetch( wpAgenticWriter.apiUrl + '/refine-from-chat', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-WP-Nonce': wpAgenticWriter.nonce,
            },
            body: JSON.stringify( {
                topic: message,
                context: message,
                selectedBlockClientId: selectedBlockId,
                blocksToRefine: blocksToRefine,
                postId: wp.data.select( 'core/editor' ).getCurrentPostId(),
                stream: true,
            } ),
        } );

        if ( ! response.ok ) {
            throw new Error( 'Refinement failed' );
        }

        // Handle streaming response
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let refinedCount = 0;

        while ( true ) {
            const { done, value } = await reader.read();
            if ( done ) break;

            const chunk = decoder.decode( value, { stream: true } );
            const lines = chunk.split( '\n' );

            for ( const line of lines ) {
                if ( line.startsWith( 'data: ' ) ) {
                    try {
                        const data = JSON.parse( line.slice( 6 ) );

                        if ( data.type === 'error' ) {
                            throw new Error( data.message );
                        } else if ( data.type === 'block' ) {
                            // Replace block in editor
                            const newBlock = createBlockFromData( data.block );
                            const { replaceBlocks } = wp.data.dispatch( 'core/block-editor' );

                            data.block.blocks.forEach( ( blockData, idx ) => {
                                if ( blockData.clientId ) {
                                    replaceBlocks( blockData.clientId, createBlockFromData( blockData ) );
                                }
                            } );

                            refinedCount++;
                        } else if ( data.type === 'complete' ) {
                            // Show completion message
                            setMessages( prev => [ ...prev, {
                                role: 'assistant',
                                content: `✅ Done! I've refined ${refinedCount} block(s) as requested.`,
                            } ] );

                            setMessages( prev => {
                                const newMessages = [ ...prev ];
                                const lastTimelineIndex = newMessages.findIndex( m => m.type === 'timeline' && m.status !== 'complete' );
                                if ( lastTimelineIndex !== -1 ) {
                                    newMessages[lastTimelineIndex] = {
                                        ...newMessages[lastTimelineIndex],
                                        status: 'complete',
                                        message: `Refined ${refinedCount} block(s) successfully`,
                                        icon: '✅',
                                    };
                                }
                                return newMessages;
                            } );
                        }
                    } catch ( e ) {
                        console.error( 'Failed to parse streaming data:', line, e );
                    }
                }
            }
        }

    } catch ( error ) {
        setMessages( prev => [ ...prev, {
            role: 'system',
            type: 'error',
            content: 'Error: ' + error.message,
        } ] );
    } finally {
        setIsLoading( false );
    }
};

Step 5: Visual Feedback

File: assets/css/sidebar.css

Add block mention styles:

/* Block mention styles */
.wpaw-block-mentioned {
    outline: 2px solid #2271b1 !important;
    outline-offset: 2px;
    box-shadow: 0 0 0 4px rgba(34, 113, 177, 0.2);
    animation: wpaw-pulse 1.5s infinite;
    transition: all 0.3s ease;
}

.wpaw-block-mentioned:hover {
    outline-width: 3px;
    box-shadow: 0 0 0 8px rgba(34, 113, 177, 0.3);
}

@keyframes wpaw-pulse {
    0%, 100% {
        box-shadow: 0 0 0 0px rgba( 34, 113, 177, 0.2);
    }
    50% {
        box-shadow: 0 0 0 6px rgba(34, 113, 177, 0.4);
    }
}

/* Mention autocomplete */
.wpaw-mention-autocomplete {
    position: absolute;
    background: white;
    border: 1px solid #ddd;
    border-radius: 4px;
    max-height: 200px;
    overflow-y: auto;
    z-index: 1000;
    box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}

.wpaw-mention-option {
    padding: 8px 12px;
    cursor: pointer;
    border-bottom: 1px solid #eee;
}

.wpaw-mention-option:hover {
    background: #f0f0f0;
}

.wpaw-mention-option strong {
    display: block;
    color: #333;
    font-size: 13px;
}

.wpaw-mention-option span {
    display: block;
    color: #666;
    font-size: 12px;
    margin-top: 2px;
}

User Experience

Scenario 1: Beginner (Toolbar Button)

Flow:

  1. Click block to select it
  2. Click "AI Refine" in toolbar
  3. Type: "Make this more engaging"
  4. Click "Refine"
  5. Block is refined

Experience: Clear, guided, discoverable

Scenario 2: Power User (Chat Mention)

Flow:

  1. Type in chat: "Refine @this to be more engaging"
  2. System highlights mentioned block
  3. Press Enter or click Send
  4. Chat shows: " Done! I've refined the current block."
  5. Block is refined immediately

Experience: Fast, conversational, efficient

Scenario 3: Multi-Block Refinement

Flow:

  1. Type: "Refine @paragraph-1, @paragraph-2, and @paragraph-3 to be more concise"
  2. All three blocks get highlighted
  3. Send message
  4. All three blocks refined in sequence
  5. Chat shows summary

Experience: Powerful, batch operation


File Modifications Summary

New Files

  • None (all modifications to existing files)

Modified Files

  1. includes/class-gutenberg-sidebar.php

    • Add handle_refine_from_chat() method
    • Add resolve_mentions_to_blocks() method
    • Add /refine-from-chat REST route
    • Add stream_refinement_from_chat() method
  2. assets/js/sidebar.js

    • Modify sendMessage() to detect refinement mentions
    • Add resolveBlockMentions() function
    • Add handleChatRefinement() function
    • Add mention detection and autocomplete UI
  3. assets/css/sidebar.css

    • Add .wpaw-block-mentioned styles
    • Add .wpaw-pulse animation
    • Add mention autocomplete dropdown styles
  4. assets/css/editor.css

    • Add block highlighting styles for mentioned blocks

Technical Details

Mention Syntax Reference

Syntax Description Example
@this Current selected block "Refine @this to be more engaging"
@previous Block before current "Refine @previous to match tone"
@next Block after current "Refine @next for consistency"
@all All blocks "Refine @all to be more concise"
@paragraph-1 1st paragraph "Refine @paragraph-1 to be more exciting"
@heading-2 2nd heading "Refine @heading-2 to be more descriptive"
@list-1 1st list "Refine @list-1 to add more items"

Block Type Aliases

For user-friendly mentions:

  • @paragraph = @paragraph-1 (current paragraph of type paragraph)
  • @heading = @heading-1 (current heading of any level)
  • @list = @list-1 (current list)

Benefits

Hybrid approach - Best of both worlds Beginner-friendly - Toolbar button remains discoverable Power-user features - @ mentions for speed Natural chat integration - Refinement becomes conversational Multi-block refinement - Refine multiple blocks at once Visual feedback - Block highlighting shows what's being refined Backward compatible - Current workflow preserved


Testing Checklist

Basic Functionality:

  • Toolbar button still works
  • @this refines selected block
  • @previous refines previous block
  • @next refines next block
  • @all refines all blocks
  • @paragraph-1 refines specific paragraph
  • @heading-2 refines specific heading

User Experience:

  • Mention autocomplete appears when typing @
  • ] Mentioned blocks get highlighted with pulse animation
  • Multiple blocks can be refined in one message
  • Chat shows refinement summary
  • Cost tracking includes refinement costs
  • Error messages when blocks not found

Edge Cases:

  • Invalid block reference: @paragraph-99 (out of range)
  • No blocks match: @list-5 when only 2 lists exist
  • Refinement request without mention: "Make this more concise" (uses selected block)
  • Mixed: "Refine @paragraph-1 and @paragraph-2 to be more concise"
  • Multiple mentions of same block: "Refine @this @this" (only refines once)

Rollback Plan

If issues occur:

  1. Remove mention detection from sendMessage() in sidebar.js
  2. Remove new endpoints from class-gutenberg-sidebar.php
  3. Remove mention CSS styles
  4. Toolbar button continues to work as fallback

Git commands:

# Rollback frontend changes
git checkout HEAD~1 assets/js/sidebar.js assets/css/sidebar.css

# Rollback backend changes
git checkout HEAD~1 includes/class-gutenberg-sidebar.php

Timeline Estimate

  • Phase 1 (Backend endpoint): 2-3 hours
  • Phase 2 (Frontend detection): 2-3 hours
  • Phase 3 (Block resolution): 1-2 hours
  • Phase 4 (Visual feedback): 1-2 hours
  • Phase 5 (Testing): 1-2 hours

Total: 7-12 hours


Success Criteria

Toolbar button works as before (no regression) @this mentions refine currently selected block @previous and @next work relative to selection @all refines all content blocks @paragraph-N syntax works for specific blocks Chat shows refinement summaries Mentioned blocks get visual highlight Autocomplete suggests available blocks Multi-block refinement works correctly Cost tracking includes refinement costs Error handling for invalid mentions


Future Enhancements

Phase 2 Ideas:

  • Smart suggestions: "Refine all headings to be more descriptive"
  • Batch operations: "Refine all lists to be more concise"
  • Quick refinement: Click block → shows quick-refinement options in popover
  • Refinement history: Undo/redo refinement changes

Advanced Features:

  • Block type suggestions: "Suggest making this a list"
  • Style transfer: "Make @paragraph-1 match the tone of @heading-2"
  • Bulk refinement: "Refine all code blocks to use simpler language"
  • Refinement templates: Predefined refinement options

Open Questions

  1. Should @paragraph-1 be 1-based or 0-based?

    • I suggest 1-based (more intuitive for non-technical users)
  2. Should we support content-based references?

    • "Refine the block about SEO" (searches block content)
    • "Refine the third paragraph" (counts paragraphs automatically)
  3. Should mentions work during article generation?

    • During initial article creation: "Refine @this section to add more examples"
    • Could interrupt plan generation flow
  4. Should we support block attributes?

    • "Refine all blocks with 'team' to match company tone"
    • "Refine code blocks to use simpler variable names"

Recommendation

Implement Phase 1-5 for MVP with:

  • Core mention syntax (@this, @previous, @next, @all, @type-N)
  • Basic visual feedback (highlighting)
  • Toolbar button preservation (current workflow)
  • Chat integration with summaries

Defer Phase 2 features (advanced usage patterns) to future iteration.


Ready to implement? Let me know if you want to:

  1. Proceed with implementation
  2. Adjust the approach
  3. Add/remove features
  4. Explore specific aspects in more detail