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:
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user