fix: session persistence + h3 readability + outline error messages

Session issues fixed:
- Removed auto-draft-only gate for showing unassigned sessions on new posts
  (now shows on any post that has no linked sessions)
- Auto-link unassigned session to current post when user opens it
- Added beforeunload/pagehide flush to persist messages before page close
  (prevents data loss from 700ms debounce not firing)
- Added warning log when session loads with 0 messages for debugging

UI fix:
- Override WP editor h3 shrinkage (11px/uppercase → 15px/normal/white)
- Fix h2/h4-h6 headings in response content for dark theme readability

Outline error messages:
- Separate empty response from parse failure with distinct actionable messages
- Show model name + token count on empty response for debugging
- Reassure user that parse failures are usually one-time issues
This commit is contained in:
Dwindi Ramadhana
2026-06-06 00:58:08 +07:00
parent f7bf1f5153
commit b4ea9025b1
3 changed files with 118 additions and 7 deletions

View File

@@ -4554,6 +4554,32 @@ input.wpaw-plan-section-check:checked::before {
/* =========================== /* ===========================
AUDIT FIXES: Mode Indicator Badge AUDIT FIXES: Mode Indicator Badge
=========================== */ =========================== */
/* Override WordPress editor-sidebar h3 shrinkage inside our panel */
#wp-agentic-writer\:wp-agentic-writer .interface-complementary-area h3,
#wp-agentic-writer\:wp-agentic-writer h3,
.wpaw-response-content h3,
.wpaw-messages-inner h3 {
font-size: 15px !important;
text-transform: none !important;
font-weight: 700 !important;
color: #e0e4ea !important;
margin-bottom: 0.5em !important;
letter-spacing: normal !important;
}
.wpaw-response-content h2 {
font-size: 17px !important;
color: #e8ecf2 !important;
}
.wpaw-response-content h4,
.wpaw-response-content h5,
.wpaw-response-content h6 {
font-size: 13px !important;
color: #d0d5dd !important;
}
.wpaw-mode-badge { .wpaw-mode-badge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;

View File

@@ -1078,6 +1078,39 @@
} }
}, [sanitizeMessagesForStorage]); }, [sanitizeMessagesForStorage]);
// Flush pending message persistence on page unload to prevent data loss
React.useEffect(() => {
const flushOnUnload = () => {
if (messagesSaveTimeoutRef.current) {
clearTimeout(messagesSaveTimeoutRef.current);
}
if (!currentSessionId || isHydratingSessionRef.current) {
return;
}
const sanitized = sanitizeMessagesForStorage(messages);
const serialized = JSON.stringify(sanitized);
if (serialized === lastPersistedMessagesRef.current) {
return;
}
// Synchronous XHR as last resort during unload (sendBeacon can't set headers)
try {
const xhr = new XMLHttpRequest();
xhr.open('POST', `${wpAgenticWriter.apiUrl}/conversations/${currentSessionId}/messages`, false); // sync
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('X-WP-Nonce', wpAgenticWriter.nonce);
xhr.send(JSON.stringify({ messages: sanitized }));
} catch (e) {
// Best effort - ignore errors during unload
}
};
window.addEventListener('beforeunload', flushOnUnload);
window.addEventListener('pagehide', flushOnUnload);
return () => {
window.removeEventListener('beforeunload', flushOnUnload);
window.removeEventListener('pagehide', flushOnUnload);
};
}, [currentSessionId, messages, sanitizeMessagesForStorage]);
React.useEffect(() => { React.useEffect(() => {
if (!currentSessionId) { if (!currentSessionId) {
return; return;
@@ -1205,10 +1238,10 @@
postSessions = Array.isArray(postData?.sessions) ? postData.sessions : []; postSessions = Array.isArray(postData?.sessions) ? postData.sessions : [];
} }
// Auto-draft fallback: // Fallback: if this post has no linked sessions, surface active unassigned sessions
// if this auto-draft has no linked sessions yet, surface active carry-over sessions // so users can continue unfinished work from other tabs or prior page loads.
// (unassigned or still auto-draft) so users can continue unfinished work. // This covers auto-draft AND draft posts that haven't had a session linked yet.
if (postSessions.length === 0 && currentPostStatus === 'auto-draft') { if (postSessions.length === 0) {
const activeRes = await fetch(`${wpAgenticWriter.apiUrl}/conversations?status=active&limit=50`, { const activeRes = await fetch(`${wpAgenticWriter.apiUrl}/conversations?status=active&limit=50`, {
method: 'GET', method: 'GET',
headers, headers,
@@ -1219,7 +1252,8 @@
unassignedSessions = allActive.filter((s) => { unassignedSessions = allActive.filter((s) => {
const pid = Number(s?.post_id || 0); const pid = Number(s?.post_id || 0);
const postStatus = String(s?.post_status || '').toLowerCase(); const postStatus = String(s?.post_status || '').toLowerCase();
return pid === 0 || postStatus === 'auto-draft'; // Show sessions that are unassigned, or linked to auto-drafts/drafts
return pid === 0 || postStatus === 'auto-draft' || postStatus === '';
}); });
} }
} }
@@ -1281,10 +1315,30 @@
isHydratingSessionRef.current = true; isHydratingSessionRef.current = true;
setCurrentSessionId(sessionId); setCurrentSessionId(sessionId);
const sessionMessages = Array.isArray(data?.messages) ? data.messages : []; const sessionMessages = Array.isArray(data?.messages) ? data.messages : [];
// If session has no messages but has a post_id, it may have been improperly persisted
if (sessionMessages.length === 0) {
wpawLog.warn('Session loaded with 0 messages:', sessionId);
}
lastPersistedMessagesRef.current = JSON.stringify(sanitizeMessagesForStorage(sessionMessages)); lastPersistedMessagesRef.current = JSON.stringify(sanitizeMessagesForStorage(sessionMessages));
hydrateSessionStateFromMessages(sessionMessages); hydrateSessionStateFromMessages(sessionMessages);
setMessages(sessionMessages); setMessages(sessionMessages);
setShowWelcome(false); setShowWelcome(false);
// Auto-link unassigned session to current post for continuity
const sessionPostId = Number(data?.post_id || 0);
if (postId && postId > 0 && sessionPostId === 0) {
fetch(`${wpAgenticWriter.apiUrl}/conversations/${sessionId}/link-post`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': wpAgenticWriter.nonce,
},
body: JSON.stringify({ postId: postId }),
}).catch(() => {}); // Non-blocking
}
setTimeout(() => { setTimeout(() => {
isHydratingSessionRef.current = false; isHydratingSessionRef.current = false;
}, 0); }, 0);

View File

@@ -2296,13 +2296,25 @@ Keep sections focused and actionable. Include H2 headings only. For technical ar
// Debug: log the raw response // Debug: log the raw response
wpaw_debug_log( 'Plan generation raw response length: ' . strlen( $content ) ); wpaw_debug_log( 'Plan generation raw response length: ' . strlen( $content ) );
if ( empty( trim( (string) $content ) ) ) {
$model_used = $response['model'] ?? 'unknown';
return new WP_Error(
'empty_response',
sprintf(
__( 'The AI model (%s) returned an empty response. Try a different planning model or simplify your topic.', 'wp-agentic-writer' ),
$model_used
),
array( 'status' => 500 )
);
}
if ( null === $plan_json ) { if ( null === $plan_json ) {
wpaw_debug_log( 'extract_plan_from_response returned null. Content preview: ' . substr( $content, 0, 500 ) ); wpaw_debug_log( 'extract_plan_from_response returned null. Content preview: ' . substr( $content, 0, 500 ) );
return new WP_Error( return new WP_Error(
'invalid_json', 'invalid_json',
sprintf( sprintf(
/* translators: %s: model output preview */ /* translators: %s: model output preview */
__( 'The model responded, but the outline format could not be parsed. Preview: %s', 'wp-agentic-writer' ), __( 'The AI responded but the outline couldn\'t be parsed as JSON. Try again — this is usually a one-time formatting issue. Preview: %s', 'wp-agentic-writer' ),
$this->build_model_output_preview( $content ) $this->build_model_output_preview( $content )
), ),
array( 'status' => 500 ) array( 'status' => 500 )
@@ -2904,6 +2916,25 @@ Keep sections focused and actionable. Include H2 headings only. For technical ar
$content = $response['content']; $content = $response['content'];
wpaw_debug_log( 'stream_generatePlan content length: ' . strlen( $content ) ); wpaw_debug_log( 'stream_generatePlan content length: ' . strlen( $content ) );
// Handle empty response gracefully
if ( empty( trim( (string) $content ) ) ) {
$model_used = $response['model'] ?? 'unknown';
$input_tokens = $response['input_tokens'] ?? 0;
echo "data: " . wp_json_encode(
array(
'type' => 'error',
'message' => sprintf(
'The AI model (%s) returned an empty response. This usually means the model couldn\'t process the request. Try: 1) Use a different planning model in Settings, 2) Simplify your topic, or 3) Try again. (Tokens sent: %d)',
$model_used,
$input_tokens
),
)
) . "\n\n";
flush();
exit;
}
$plan_json = $this->extract_plan_from_response( $content, $topic ); $plan_json = $this->extract_plan_from_response( $content, $topic );
if ( null === $plan_json ) { if ( null === $plan_json ) {
@@ -2912,7 +2943,7 @@ Keep sections focused and actionable. Include H2 headings only. For technical ar
echo "data: " . wp_json_encode( echo "data: " . wp_json_encode(
array( array(
'type' => 'error', 'type' => 'error',
'message' => 'The model responded, but the outline format could not be parsed. Preview: ' . $preview, 'message' => 'The AI responded but the outline couldn\'t be parsed as JSON. This sometimes happens when the model adds extra text. Trying again usually fixes this. Preview: ' . $preview,
) )
) . "\n\n"; ) . "\n\n";
flush(); flush();