Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions inc/Main.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions inc/OpenAI.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
{
Expand All @@ -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;

Expand Down
50 changes: 50 additions & 0 deletions src/frontend/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' );
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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:
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" width="16" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" /></svg>',
ariaLabel: strings.scrollToLatest,
} );

chatWindow.appendChild( scrollDownButton );

const privacyNotice = this.renderPrivacyNotice();

if ( privacyNotice ) {
Expand Down
51 changes: 51 additions & 0 deletions src/frontend/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
65 changes: 65 additions & 0 deletions tests/e2e/specs/chat.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `<div>Filler message ${ i }</div>`;
box.appendChild( bubble );
}

box.scrollTo( { top: box.scrollHeight, behavior: 'instant' } );
} );

// At the bottom: no chip.
await expect( page.locator( '#hyve-scroll-down' ) ).toBeHidden();

// Scroll up through the history: the chip appears.
await page.evaluate( () => {
const box = document.getElementById( 'hyve-message-box' );
box.scrollTo( { top: 0, behavior: 'instant' } );
} );
await expect( page.locator( '#hyve-scroll-down' ) ).toBeVisible();

// Clicking it returns to the bottom and the chip hides again.
await page.locator( '#hyve-scroll-down' ).click();
await expect( page.locator( '#hyve-scroll-down' ) ).toBeHidden();
await expect
.poll( () =>
page.evaluate( () => {
const box = document.getElementById( 'hyve-message-box' );

return box.scrollHeight - box.scrollTop - box.clientHeight;
} )
)
.toBeLessThan( 5 );
} );

test( 'chat inline interaction', async ( { page, admin, editor } ) => {
await admin.createNewPost( { title: 'Dummy Post' } );

Expand Down
130 changes: 130 additions & 0 deletions tests/e2e/specs/proactive.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { test, expect } from '@wordpress/e2e-test-utils-playwright';

/**
* The proactive teaser's scroll trigger. Pro supplies the config through
* `window.hyveClient.proactive`; injecting it here exercises the same lite
* code path a Pro site runs.
*/
test.describe( 'Proactive teaser (scroll trigger)', () => {
/**
* Publish a post with the floating chat block, open it, and make the
* page tall enough to scroll.
*
* @param {import('@playwright/test').Page} page
* @param {import('@wordpress/e2e-test-utils-playwright').Admin} admin
* @param {import('@wordpress/e2e-test-utils-playwright').Editor} editor
*/
async function openTallPageWithChat( page, admin, editor ) {
await admin.createNewPost( { title: 'Proactive Scroll Page' } );

await editor.insertBlock( {
name: 'hyve/chat',
attributes: { variant: 'floating' },
} );

const postId = await editor.publishPost();

await page.goto( `?p=${ postId }` );

await page.evaluate( () => {
if (
! document.querySelector(
'#hyve-open, #hyve-window, .hyve-input-text'
)
) {
window?.hyveApp?.initialize();
}

// Make the page scrollable well past any viewport.
const spacer = document.createElement( 'div' );
spacer.style.height = '5000px';
document.body.appendChild( spacer );
} );

await expect( page.locator( '#hyve-open' ) ).toBeVisible();
}

/**
* Arm the scroll trigger with the given config, as Pro would on load.
*
* @param {import('@playwright/test').Page} page
* @param {number} scrollDepth
*/
async function armScrollTeaser( page, scrollDepth ) {
await page.evaluate( ( depth ) => {
window.hyveClient.proactive = {
trigger: 'scroll',
message: 'Need a hand?',
scrollDepth: depth,
};
window.hyveApp.setupProactive();
}, scrollDepth );
}

test( 'appears when the visitor scrolls past the configured depth', async ( {
page,
admin,
editor,
} ) => {
await openTallPageWithChat( page, admin, editor );
await armScrollTeaser( page, 50 );

// Above the depth: no teaser yet.
await expect( page.locator( '#hyve-teaser' ) ).toBeHidden();

await page.evaluate( () =>
window.scrollTo(
0,
document.documentElement.scrollHeight - window.innerHeight
)
);

await expect( page.locator( '#hyve-teaser' ) ).toBeVisible();
await expect( page.locator( '#hyve-teaser' ) ).toContainText(
'Need a hand?'
);
} );

test( 'appears immediately when the visitor is already past the depth', async ( {
page,
admin,
editor,
} ) => {
await openTallPageWithChat( page, admin, editor );

// Scroll first (anchor link / restored scroll position), then arm:
// no further scroll event will fire, the arm-time check must show it.
await page.evaluate( () =>
window.scrollTo(
0,
document.documentElement.scrollHeight - window.innerHeight
)
);

await armScrollTeaser( page, 50 );

await expect( page.locator( '#hyve-teaser' ) ).toBeVisible();
} );

test( 'stays hidden below the configured depth', async ( {
page,
admin,
editor,
} ) => {
await openTallPageWithChat( page, admin, editor );
await armScrollTeaser( page, 90 );

// Scroll to roughly half the page: under the 90% depth.
await page.evaluate( () =>
window.scrollTo(
0,
( document.documentElement.scrollHeight - window.innerHeight ) *
0.5
)
);

// Give any scroll handler a chance to run before asserting.
await page.waitForTimeout( 250 );
await expect( page.locator( '#hyve-teaser' ) ).toBeHidden();
} );
} );
Loading