From b4ea9025b1534ec545d66c40851c3d92b4fc87f5 Mon Sep 17 00:00:00 2001 From: Dwindi Ramadhana Date: Sat, 6 Jun 2026 00:58:08 +0700 Subject: [PATCH] fix: session persistence + h3 readability + outline error messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- assets/css/sidebar.css | 26 +++++++++++ assets/js/sidebar.js | 64 +++++++++++++++++++++++++--- includes/class-gutenberg-sidebar.php | 35 ++++++++++++++- 3 files changed, 118 insertions(+), 7 deletions(-) diff --git a/assets/css/sidebar.css b/assets/css/sidebar.css index 8b1172b..f750852 100644 --- a/assets/css/sidebar.css +++ b/assets/css/sidebar.css @@ -4554,6 +4554,32 @@ input.wpaw-plan-section-check:checked::before { /* =========================== 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 { display: inline-flex; align-items: center; diff --git a/assets/js/sidebar.js b/assets/js/sidebar.js index 686b9c3..59138b0 100644 --- a/assets/js/sidebar.js +++ b/assets/js/sidebar.js @@ -1078,6 +1078,39 @@ } }, [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(() => { if (!currentSessionId) { return; @@ -1205,10 +1238,10 @@ postSessions = Array.isArray(postData?.sessions) ? postData.sessions : []; } - // Auto-draft fallback: - // if this auto-draft has no linked sessions yet, surface active carry-over sessions - // (unassigned or still auto-draft) so users can continue unfinished work. - if (postSessions.length === 0 && currentPostStatus === 'auto-draft') { + // Fallback: if this post has no linked sessions, surface active unassigned sessions + // so users can continue unfinished work from other tabs or prior page loads. + // This covers auto-draft AND draft posts that haven't had a session linked yet. + if (postSessions.length === 0) { const activeRes = await fetch(`${wpAgenticWriter.apiUrl}/conversations?status=active&limit=50`, { method: 'GET', headers, @@ -1219,7 +1252,8 @@ unassignedSessions = allActive.filter((s) => { const pid = Number(s?.post_id || 0); 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; setCurrentSessionId(sessionId); 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)); hydrateSessionStateFromMessages(sessionMessages); setMessages(sessionMessages); 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(() => { isHydratingSessionRef.current = false; }, 0); diff --git a/includes/class-gutenberg-sidebar.php b/includes/class-gutenberg-sidebar.php index ee3ddf9..fd9bcff 100644 --- a/includes/class-gutenberg-sidebar.php +++ b/includes/class-gutenberg-sidebar.php @@ -2296,13 +2296,25 @@ Keep sections focused and actionable. Include H2 headings only. For technical ar // Debug: log the raw response 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 ) { wpaw_debug_log( 'extract_plan_from_response returned null. Content preview: ' . substr( $content, 0, 500 ) ); return new WP_Error( 'invalid_json', sprintf( /* 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 ) ), array( 'status' => 500 ) @@ -2904,6 +2916,25 @@ Keep sections focused and actionable. Include H2 headings only. For technical ar $content = $response['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 ); 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( array( '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"; flush();