fix: writing stuck - handle empty model response + no-divider fallback + timeline cleanup

Root causes of writing getting stuck:
1. Model returns empty response for a section → now detected early with
   actionable error message including model name
2. Model responds but without ~~~ARTICLE~~~ divider (happens with fallback
   models like Gemini) → now treats entire response as markdown content
3. Stream ends without 'complete' event (error/exit in PHP) → JS timeline
   entries lingered as 'active' forever. Now deactivated on stream close.
4. Error messages in execution flow now use structured formatAiErrorMessage
   with retry button instead of raw text

Also: deactivateActiveTimelineEntries called in catch block so errors
properly clear the 'Writing section X' status indicator.
This commit is contained in:
Dwindi Ramadhana
2026-06-06 05:30:12 +07:00
parent 23a34b3035
commit 379a72e52d
2 changed files with 46 additions and 2 deletions

View File

@@ -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',
}]);

View File

@@ -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;