Skip to content
Draft
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
92 changes: 92 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,66 @@ export namespace Gleap {
agentId: string,
options?: { context?: any; initialQuestion?: string; initialMessage?: string }
): void;
/**
* What the app tells the sidekick's agent about where the customer is. A plain object without
* `title`, `step` or `data` is taken as `data`.
*/
interface SidekickContext {
/** The page as the customer sees it (default: document.title). */
title?: string;
/** The onboarding step: its title, or { id, title, index (1-based), total }. */
step?: string | { id?: string; title?: string; index?: number; total?: number };
/** What the app knows: plan, seats, a record id, … (plain values or JSON). */
data?: Record<string, string | number | boolean | object | null>;
}
interface SidekickOptions {
/** A customer agent of the workspace; omitted = Kai Support (the project's AI workflow). */
agentId?: string;
/** Replaces the context set with setSidekickContext. */
context?: SidekickContext | Record<string, any>;
/** The agent's first message in the conversation (shown, not answered). */
initialMessage?: string;
/** The customer's first message, answered by the agent. */
initialQuestion?: string;
/**
* Default: the page moves aside and stays usable, no dim; full-width fixed headers/footers get
* narrower with it and come back exactly. It overlays instead when the page would get narrower
* than 720 px, and by default on a page inside an iframe. 'push': push wherever there is room
* (also inside an iframe). 'overlay': over a dimmed page. Phones always get a full-screen sheet.
*/
mode?: "push" | "overlay";
/** Start a new conversation even when a sidekick conversation with this agent is still open. */
newConversation?: boolean;
}
/**
* Opens the sidekick: an agent in a full-height side panel next to the page (a full-screen
* sheet on phones). The agent gets the page's URL (without query and hash), its title and the
* context. Closes with its × button, Escape, a click on the dimmed page (overlay) or
* closeSidekick(); the page comes back exactly as it was.
*/
function openSidekick(options?: SidekickOptions): void;
interface SidekickMessageOptions {
/** A customer agent of the workspace; omitted = Kai Support. */
agentId?: string;
/** The agent's message (plain text, up to 500 characters). */
message: string;
/** Shown on the card; default: the project's bot name and avatar. */
sender?: { name: string; profileImageUrl?: string };
}
/**
* Shows an agent's message as a card above the launcher. Clicking it opens the sidekick with the
* message as the agent's first message; its × hides it (the same card stays away for a day, in
* every tab). Not shown while the widget or the sidekick is open. Returns whether it shows.
*/
function showSidekickMessage(options: SidekickMessageOptions): boolean;
/** Hides the sidekick message card. */
function hideSidekickMessage(): void;
/** Sets (null: clears) the sidekick's context; the next message carries it. */
function setSidekickContext(context: SidekickContext | Record<string, any> | null): void;
function closeSidekick(): void;
function isSidekickOpen(): boolean;
/** Allow (default) or stop proactive sidekick messages (cards and opens) on this page. */
function setSidekickProactive(enabled: boolean): void;
function isOpened(): boolean;
/**
* Sets the data region your Gleap project lives in. Call before initialize.
Expand Down Expand Up @@ -358,6 +418,38 @@ export namespace Gleap {
function on(event: "survey-completed", callback: (data: SurveyCompletedEvent) => void): void;
/** The survey was closed (by the user or after its ending). */
function on(event: "survey-closed", callback: (data: SurveyClosedEvent) => void): void;
/** The sidekick opened (by openSidekick or a proactive open). */
function on(
event: "sidekick-opened",
callback: (data: {
agentId: string | null;
mode: "overlay" | "push" | "sheet";
proactive: boolean;
/** Why a push became an overlay. */
pushFallback?: "iframe";
}) => void
): void;
/** The sidekick closed and the page is back. */
function on(
event: "sidekick-closed",
callback: (data: { reason: "close" | "escape" | "backdrop" | "api" | "replaced" }) => void
): void;
/** A proactive message arrived (a card, or the panel opening): shown, or why not. */
function on(
event: "sidekick-proactive",
callback: (data: {
id: string;
triggerId: string;
display: "card" | "open";
shown: boolean;
reason?: "disabled" | "page_view" | "busy" | "dismissed" | "typing" | "hidden";
}) => void
): void;
/** The customer dismissed a sidekick message card. */
function on(
event: "sidekick-message-dismissed",
callback: (data: { agentId: string | null; id?: string; triggerId?: string }) => void
): void;
function on(
event:
| "initialized"
Expand Down
95 changes: 95 additions & 0 deletions src/Gleap.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import GleapProductTours from './GleapProductTours';
import GleapReplayRecorder from './GleapReplayRecorder';
import GleapSession from './GleapSession';
import GleapShortcutListener from './GleapShortcutListener';
import GleapSidekickManager from './GleapSidekickManager';
import { dispatchSSEEvent, parseSSEStream } from './GleapSSEParser';
import GleapStreamedEvent from './GleapStreamedEvent';
import GleapTabCommunication from './GleapTabCommunication';
Expand Down Expand Up @@ -377,6 +378,7 @@ class Gleap {
GleapNotificationManager.getInstance().clearAllNotifications(true);
GleapTabCommunication.getInstance().stop();
GleapAiChatbarManager.getInstance().destroy();
GleapSidekickManager.getInstance().destroy();
GleapSession.getInstance().clearSession(0, false);
GleapBannerManager.getInstance().removeBannerUI();

Expand Down Expand Up @@ -432,6 +434,98 @@ class Gleap {
} catch (e) {}
}

/**
* Opens the sidekick: an agent in a full-height side panel next to the page. By default it
* pushes the page aside (the page stays visible and usable, no dim; full-width fixed headers and
* footers get narrower with it). It overlays a dimmed page instead when asked for (mode
* 'overlay'), when the page would get narrower than 720 px, and by default inside an iframe
* (mode 'push' forces the push there). Phones get a full-screen sheet. Closing puts the page and
* its fixed bars back exactly as they were. The agent gets the page's context: its URL (without
* query and hash), its title, the onboarding step and what the app passes in.
* @param {object} [options]
* - agentId: a customer agent of the workspace; omitted = Kai Support (the project's AI workflow).
* - context: { title?, step?, data? } (replaces the one from setSidekickContext).
* - initialMessage: the agent's first message in the conversation (shown, not answered).
* - initialQuestion: the customer's first message, answered by the agent.
* - mode: 'push' (forces the push where there is room) or 'overlay'; default: push unless in an iframe.
* - newConversation: start fresh even when a sidekick conversation with this agent is open.
*/
static openSidekick(options) {
try {
GleapSidekickManager.getInstance().open(options || {});
} catch (e) {}
}

/**
* Shows an agent's message as a card above the launcher (avatar, the message, "<agent> · now").
* Clicking it opens the sidekick with the message as the agent's first message; its × hides it,
* and the same card stays away for a day in every tab. Not shown while the widget or the sidekick
* is open.
* @param {object} options
* - agentId: a customer agent of the workspace; omitted = Kai Support.
* - message: the agent's message (plain text, up to 500 characters).
* - sender: { name, profileImageUrl? } shown on the card (default: the project's bot name and avatar).
* @returns {boolean} whether the card shows.
*/
static showSidekickMessage(options) {
try {
return GleapSidekickManager.getInstance().showMessage(options || {});
} catch (e) {
return false;
}
}

/**
* Hides the sidekick message card (no dismissal is remembered).
*/
static hideSidekickMessage() {
try {
GleapSidekickManager.getInstance().removeCard('api');
} catch (e) {}
}

/**
* What the app knows about where the customer is, for the sidekick: { title?, step?, data? }
* (a plain object counts as data). Sent with the next message; null clears it.
* @param {object|null} context
*/
static setSidekickContext(context) {
try {
GleapSidekickManager.getInstance().setContext(context);
} catch (e) {}
}

/**
* Closes the sidekick and gives the page back as it was.
*/
static closeSidekick() {
try {
GleapSidekickManager.getInstance().close('api');
} catch (e) {}
}

/**
* Whether the sidekick is open.
* @returns {boolean}
*/
static isSidekickOpen() {
try {
return GleapSidekickManager.getInstance().isOpen;
} catch (e) {
return false;
}
}

/**
* Allow (default) or stop proactive sidekick messages on this page, cards and opens (e.g. during checkout).
* @param {boolean} enabled
*/
static setSidekickProactive(enabled) {
try {
GleapSidekickManager.getInstance().setProactiveEnabled(enabled);
} catch (e) {}
}

/**
* Set the AI quick actions.
* @param {Array<string>} quickActions
Expand Down Expand Up @@ -1798,6 +1892,7 @@ export {
GleapReplayRecorder,
GleapSession,
GleapShortcutListener,
GleapSidekickManager,
GleapStreamedEvent,
GleapTabCommunication,
GleapTagManager,
Expand Down
47 changes: 43 additions & 4 deletions src/GleapFrameManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import Gleap, {
GleapTranslationManager,
} from './Gleap';
import GleapAgentToolManager from './GleapAgentToolManager';
import GleapSidekickManager from './GleapSidekickManager';
import { getCaptureCapabilities } from './GleapCaptureSettings';
import { forwardSurveyAnalyticsEvent } from './GleapSurveyAnalyticsForwarder';
import { bootstrapGleapFrame, loadFromGleapCache, runFunctionWhenDomIsReady, saveToGleapCache } from './GleapHelper';
Expand Down Expand Up @@ -157,6 +158,10 @@ export default class GleapFrameManager {
}

setAppMode(appMode) {
// Leaving the sidekick for another mode (Gleap.open, a survey, …): close it and give the page back first.
if (this.appMode === 'sidekick' && appMode !== 'sidekick') {
GleapSidekickManager.getInstance().onLeaveSidekickMode();
}
this.appMode = appMode;
// A new survey starts as Surveys 2.0 until the messenger says otherwise (survey-legacy).
this.surveyLegacy = false;
Expand Down Expand Up @@ -245,8 +250,8 @@ export default class GleapFrameManager {
innerContainer.style.height = '';
if (this.appMode === 'survey' || this.isLegacyFullSurvey()) {
innerContainer.style.maxHeight = this.frameHeight > 0 ? `${this.frameHeight}px` : '';
} else if (this.isFullSurvey()) {
// Full screen: the messenger fills the viewport and draws the background itself.
} else if (this.isFullSurvey() || this.appMode === 'sidekick') {
// Full screen / the sidekick's full height: the messenger fills it.
innerContainer.style.maxHeight = 'none';
} else {
innerContainer.style.maxHeight = `${widgetMaxHeight}px`;
Expand Down Expand Up @@ -456,7 +461,7 @@ export default class GleapFrameManager {
if (flowConfig && flowConfig.hideExpandButton) {
return false;
}
return !this.isSurvey() && this.hasRoomToExpand();
return !this.isSurvey() && this.appMode !== 'sidekick' && this.hasRoomToExpand();
}

getWidgetExpandedPreference() {
Expand Down Expand Up @@ -529,6 +534,13 @@ export default class GleapFrameManager {
imageViews[imageViews.length - 1].remove();
return;
}
if (this.appMode === 'sidekick') {
// A push panel leaves the page usable: Escape in the page stays the page's.
if (GleapSidekickManager.getInstance().shouldCloseOnPageEscape(evt)) {
GleapSidekickManager.getInstance().close('escape');
}
return;
}
this.hideWidget();
}
};
Expand Down Expand Up @@ -777,7 +789,9 @@ export default class GleapFrameManager {
const modernStyleLeft = 'gleap-frame-container--modern-left';
const noButtonStyleLeft = 'gleap-frame-container--no-button';
const expandedStyle = 'gleap-frame-container--expanded';
const sidekickStyle = 'gleap-frame-container--sidekick';
const allStyles = [
sidekickStyle,
classicStyle,
classicStyleLeft,
expandedStyle,
Expand Down Expand Up @@ -849,6 +863,9 @@ export default class GleapFrameManager {
if (this.appMode === 'extended') {
this.gleapFrameContainer.classList.add(extendedStyle);
}
if (this.appMode === 'sidekick') {
this.gleapFrameContainer.classList.add(sidekickStyle);
}

// The end user's choice, independent of the page-driven extended mode.
this.appliedWidgetExpanded = this.isWidgetExpanded();
Expand Down Expand Up @@ -920,7 +937,8 @@ export default class GleapFrameManager {
// Full-screen surveys take the keyboard (digits, letters, Enter) right away. The
// messenger can't pull focus into its cross-origin frame without a user gesture
// (a survey shown by a trigger has none), so the host page focuses the frame.
if (this.isFullSurvey() && !this.isLegacyFullSurvey()) {
// The sidekick is a dialog over the page: focus moves into it too (and comes back on close).
if ((this.isFullSurvey() && !this.isLegacyFullSurvey()) || this.appMode === 'sidekick') {
try {
const frame = this.gleapFrameContainer.querySelector('.gleap-frame');
if (frame) frame.focus({ preventScroll: true });
Expand All @@ -929,6 +947,14 @@ export default class GleapFrameManager {
}

updateUI() {
// The sidekick shows one agent conversation: the other unread messages and their badge stay
// (hidden while the panel is open, see UI.js), as they are for a survey.
if (this.appMode === 'sidekick') {
GleapNotificationManager.getInstance().clearAllNotifications(true);
GleapFeedbackButtonManager.getInstance().updateFeedbackButtonState();
return;
}

// Clear notifications only when not opening a survey.
GleapNotificationManager.getInstance().clearAllNotifications(this.isSurvey());

Expand Down Expand Up @@ -1016,6 +1042,10 @@ export default class GleapFrameManager {
}
this.widgetOpened = false;
this.updateWidgetStatus();
// The sidekick gives the page back (dim, push margin, focus) as its panel slides out.
if (this.appMode === 'sidekick') {
GleapSidekickManager.getInstance().onWidgetHidden();
}
GleapFeedbackButtonManager.getInstance().updateFeedbackButtonState();
if (wasVisible) {
GleapEventManager.notifyEvent('close');
Expand Down Expand Up @@ -1104,11 +1134,16 @@ export default class GleapFrameManager {
this.addMessageListener((data) => {
if (data.name === 'ping') {
this.comReady = true;
// The frame says what it can (`caps`); a sidekick it can't show falls back to the AI bar.
const sidekickFellBack = GleapSidekickManager.getInstance().onFramePing(data.data);
this.sendConfigUpdate();
this.sendSessionUpdate();
this.sendCaptureCapabilities();
this.workThroughQueue();
clearTimeout(this.openAfterPingTimeout);
if (sidekickFellBack) {
return;
}
this.openAfterPingTimeout = setTimeout(() => {
this.openAfterPingTimeout = null;
this.runWidgetShouldOpenCallback();
Expand Down Expand Up @@ -1219,6 +1254,10 @@ export default class GleapFrameManager {
this.hideWidget();
}

if (data.name === 'sidekick-close' || data.name === 'sidekick-engaged') {
GleapSidekickManager.getInstance().onFrameMessage(data);
}

// A verified email moved the conversation to another contact: continue
// as that contact (the widget's "Verify email" workflow step).
if (data.name === 'adopt-session') {
Expand Down
Loading
Loading