diff --git a/assets/js/sidebar.js b/assets/js/sidebar.js index b38d008..21d91ba 100644 --- a/assets/js/sidebar.js +++ b/assets/js/sidebar.js @@ -2912,12 +2912,20 @@ } } clearTimeout(timeout); + // If stream ended without a 'complete' event, deactivate lingering timeline entries + setMessages(prev => { + const hasActive = prev.some(m => m.type === 'timeline' && m.status && !['complete', 'inactive', 'stopped'].includes(m.status)); + if (hasActive) { + return deactivateActiveTimelineEntries(prev); + } + return prev; + }); } catch (error) { setAgentMode(currentPlanRef.current ? 'planning' : 'chat'); - setMessages(prev => [...prev, { + setMessages(prev => [...deactivateActiveTimelineEntries(prev), { role: 'system', type: 'error', - content: 'Error: ' + (error.message || 'Failed to execute outline'), + content: formatAiErrorMessage(error, 'Failed to execute outline'), canRetry: true, retryType: 'execute', }]); diff --git a/includes/class-gutenberg-sidebar.php b/includes/class-gutenberg-sidebar.php index fd9bcff..fc6eb2c 100644 --- a/includes/class-gutenberg-sidebar.php +++ b/includes/class-gutenberg-sidebar.php @@ -3343,6 +3343,42 @@ Remember: You MUST include the ~~~ARTICLE~~~ divider to separate your conversati exit; } + // Handle empty response from model + if ( empty( trim( (string) $accumulated_content ) ) ) { + $model_used = $response['model'] ?? 'unknown'; + wpaw_debug_log( "Section writing got empty response from model: {$model_used}" ); + echo "data: " . wp_json_encode( + array( + 'type' => 'error', + 'message' => sprintf( 'Section "%s" got an empty response from the AI model (%s). Please retry.', $heading, $model_used ), + ) + ) . "\n\n"; + flush(); + exit; + } + + // If divider was never found, treat the entire content as markdown + if ( ! $divider_found ) { + wpaw_debug_log( 'No ~~~ARTICLE~~~ divider found in section response. Using full content as markdown.' ); + $markdown_content = $accumulated_content; + // Strip any leading conversational fluff (first line if it looks like a note) + $lines = explode( "\n", $markdown_content ); + if ( ! empty( $lines[0] ) && ! preg_match( '/^#{1,3}\s/', $lines[0] ) && strlen( $lines[0] ) < 200 ) { + // First line might be a brief conversational note, skip it + $first_line = array_shift( $lines ); + if ( ! empty( $first_line ) ) { + echo "data: " . wp_json_encode( + array( + 'type' => 'conversational', + 'content' => trim( $first_line ), + ) + ) . "\n\n"; + flush(); + } + $markdown_content = implode( "\n", $lines ); + } + } + $section_cost = $response['cost'] ?? 0; $total_cost += $section_cost;