diff --git a/inc/Main.php b/inc/Main.php index e547a02d..cc5251da 100644 --- a/inc/Main.php +++ b/inc/Main.php @@ -806,6 +806,7 @@ public function get_frontend_data( $overrides = [] ) { 'openChat' => __( 'Open chat', 'hyve-lite' ), 'closeChat' => __( 'Close chat', 'hyve-lite' ), 'sendMessage' => __( 'Send message', 'hyve-lite' ), + 'scrollToLatest' => __( 'Scroll to the latest messages', 'hyve-lite' ), 'previewNotice' => __( 'Preview mode — test your assistant here. These messages aren\'t saved.', 'hyve-lite' ), /** * Filters the chat privacy notice text. Use a single %s where the diff --git a/inc/OpenAI.php b/inc/OpenAI.php index dbb71e29..ab1b94bd 100644 --- a/inc/OpenAI.php +++ b/inc/OpenAI.php @@ -97,6 +97,7 @@ class OpenAI { b) If current context is empty but previous context is relevant: Use previous context to answer. c) If the input is a greeting: Respond appropriately. d) If neither current nor previous context addresses the question: Respond with an empty response and success: false. +e) If the input is a general-purpose task rather than a question about the site or its content (fixing or writing code, translating text, solving exercises, generating unrelated content), and the context does not cover it: Respond with an empty response and success: false, even though you could do it from general knowledge. 3. Response Formulation - Use information from the current context primarily. If current context is insufficient, refer to previous context for follow-up questions. @@ -154,6 +155,15 @@ class OpenAI { "success": true } +5. Unrelated Task +Context: [Empty] +Question: .style{color:blue} fix this CSS, please +Response: +{ +"response": "", +"success": false +} + Error Handling: For invalid inputs or unrecognized question formats, respond with: { @@ -173,6 +183,7 @@ class OpenAI { - Prioritize using the current context for answers. - For follow-up questions with empty current context, refer to previous context if relevant. - If information isn't available in current or previous context, indicate this with an empty response and success: false. +- You are this website's assistant, not a general-purpose AI: never perform tasks or produce content that the context does not support. - Always strive to provide the most accurate and relevant information based on available context. PROMPT; diff --git a/src/frontend/App.js b/src/frontend/App.js index 13b6067b..c0484166 100644 --- a/src/frontend/App.js +++ b/src/frontend/App.js @@ -2026,6 +2026,36 @@ class App { } ); } + // The jump-back-down chip: appears once the visitor scrolls up through + // the conversation, hides again near the bottom. New replies force a + // scroll to the bottom, which also hides it through the same listener. + const messageBox = document.getElementById( 'hyve-message-box' ); + const scrollDownButton = document.getElementById( 'hyve-scroll-down' ); + + if ( messageBox && scrollDownButton ) { + messageBox.addEventListener( + 'scroll', + () => { + const fromBottom = + messageBox.scrollHeight - + messageBox.scrollTop - + messageBox.clientHeight; + + scrollDownButton.classList.toggle( + 'is-visible', + 120 < fromBottom + ); + }, + { passive: true } + ); + + scrollDownButton.addEventListener( 'click', () => { + // The box scrolls smoothly via CSS; the resulting scroll + // events hide the chip as the bottom approaches. + messageBox.scrollTop = messageBox.scrollHeight; + } ); + } + // Close menu when clicking outside document.addEventListener( 'click', ( event ) => { const menu = document.querySelector( '.hyve-menu-dropdown' ); @@ -2222,6 +2252,13 @@ class App { window.addEventListener( 'scroll', this.teaserScrollListener, { passive: true, } ); + + // The visitor may already be past the configured depth when the + // trigger arms — an anchor link, the browser restoring the + // scroll position on back/reload, or a page shorter than the + // viewport — and then no scroll event ever fires. Evaluate once + // now so the teaser still appears. + this.teaserScrollListener(); break; default: this.teaserTimeout = window.setTimeout( @@ -2700,6 +2737,19 @@ class App { chatWindow.appendChild( chatMessageBox ); + // A quick way back to the latest messages for visitors who scrolled up + // through the conversation. Hidden until the message box is actually + // scrolled away from the bottom (see setupListeners). + const scrollDownButton = this.createElement( 'button', { + className: 'hyve-scroll-down', + id: 'hyve-scroll-down', + innerHTML: + '', + ariaLabel: strings.scrollToLatest, + } ); + + chatWindow.appendChild( scrollDownButton ); + const privacyNotice = this.renderPrivacyNotice(); if ( privacyNotice ) { diff --git a/src/frontend/style.scss b/src/frontend/style.scss index 1a6cb6d7..1f315d80 100644 --- a/src/frontend/style.scss +++ b/src/frontend/style.scss @@ -640,6 +640,57 @@ body.wp-admin { } } +// Jump back to the latest messages once the visitor has scrolled up. +// Anchored to .hyve-window (fixed when floating, relative when inline), +// floating just above the input box. +.hyve-scroll-down { + align-items: center; + // Fixed light palette on purpose: the chip floats over message bubbles, + // so a theme-colored background could render it invisible on dark setups. + background: #fff; + border: 1px solid #e5e7eb; + border-radius: 999px; + bottom: 84px; + box-shadow: 0 2px 8px rgba(15, 23, 42, 0.16); + color: #111827; + cursor: pointer; + display: flex; + height: 34px; + justify-content: center; + left: 50%; + line-height: 0; + opacity: 0; + padding: 0; + pointer-events: none; + position: absolute; + transform: translate(-50%, 6px); + transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s; + // Fully hidden, not just transparent: keeps the chip out of the tab + // order while it is not offered. + visibility: hidden; + width: 34px; + z-index: 2; + + &.is-visible { + opacity: 1; + pointer-events: auto; + transform: translate(-50%, 0); + visibility: visible; + } + + &:hover { + border-color: #d1d5db; + box-shadow: 0 3px 10px rgba(15, 23, 42, 0.22); + } +} + +@media (prefers-reduced-motion: reduce) { + + .hyve-scroll-down { + transition: none; + } +} + // ---- Message list ---- .hyve-message-box { flex: 1 1 auto; diff --git a/tests/e2e/specs/chat.spec.js b/tests/e2e/specs/chat.spec.js index d6effe10..4e29d016 100644 --- a/tests/e2e/specs/chat.spec.js +++ b/tests/e2e/specs/chat.spec.js @@ -102,6 +102,71 @@ test.describe( 'Chat', () => { ).toBeVisible(); } ); + test( 'scrolling up reveals a jump-back-down chip that returns to the latest message', async ( { + page, + admin, + editor, + } ) => { + await admin.createNewPost( { title: 'Dummy Post' } ); + + await editor.insertBlock( { + name: 'hyve/chat', + attributes: { + variant: 'floating', + }, + } ); + + const postId = await editor.publishPost(); + + await page.goto( `?p=${ postId }` ); + await initializeChatApp( page ); + + await page + .locator( '#hyve-open' ) + .getByRole( 'button' ) + .click( { force: true } ); + await expect( page.locator( '#hyve-window' ) ).toBeVisible(); + + // A conversation long enough to scroll, without burning API calls. + // Seed scrolls jump instantly so the CSS smooth-scroll animation + // cannot race the assertions; the click below still exercises it. + await page.evaluate( () => { + const box = document.getElementById( 'hyve-message-box' ); + + for ( let i = 0; i < 30; i++ ) { + const bubble = document.createElement( 'div' ); + bubble.className = 'hyve-bot-message'; + bubble.innerHTML = `