@sammy-labs/walkthroughs
Version:
Sammy Labs Walkthroughs
655 lines (634 loc) • 20.6 kB
TypeScript
import * as react_jsx_runtime from 'react/jsx-runtime';
import { ReactNode } from 'react';
/**
* Declares the shape of errors (`FlowError`) and standardized results (`FlowResult`) that may occur while running a walkthrough. These help maintain consistent error handling and output across the library.
*/
type FlowError = {
type: "ELEMENT_NOT_FOUND" | "STEP_NOT_FOUND" | "DRIVER_ERROR" | "FLOW_NOT_FOUND" | "URL_MISMATCH" | "API_ERROR" | "NETWORK_ERROR" | "UNEXPECTED_ERROR";
message: string;
details?: any;
};
/**
* Implements the creation, updating, and removal of the dark overlay behind the highlighted element. This includes generating paths with cutouts for the highlighted region and animating transitions between steps.
*/
type StageDefinition = {
x: number;
y: number;
width: number;
height: number;
hideHighlight?: boolean;
};
/**
* Implements a simple global state object (`currentState`) to track active elements, steps, and transitions within the driver. This is the local, in-memory backbone for step-by-step progress in a single walkthrough session.
*/
type State = {
isInitialized?: boolean;
activeIndex?: number;
activeElement?: Element;
activeStep?: DriveStep;
previousElement?: Element;
previousStep?: DriveStep;
popover?: PopoverDOM;
__previousElement?: Element;
__activeElement?: Element;
__previousStep?: DriveStep;
__activeStep?: DriveStep;
__activeOnDestroyed?: Element;
__resizeTimeout?: number;
__transitionCallback?: () => void;
__activeStagePosition?: StageDefinition;
__overlaySvg?: SVGSVGElement;
__didFirstHighlight?: boolean;
__userReplayId?: string;
__activeStepId?: string;
__stepIndex?: number;
__isRedirecting?: boolean;
__didFallback?: boolean;
__infoHighlightSvg?: SVGSVGElement;
__infoHighlightTarget?: Element;
__flowId?: string;
__flowVersionId?: string;
__activePopover?: any;
draggedPopoverPosition?: {
left: number;
top: number;
};
__hasFallbackBeenLogged?: boolean;
__hasDeoptedToInformational?: boolean;
};
/**
* Generates, positions, and styles the popover elements that appear over or around highlighted DOM elements. Handles button rendering, progress display, and arrow positioning in the step popover.
*/
type Side = "top" | "right" | "bottom" | "left" | "over";
type Alignment = "start" | "center" | "end";
type AllowedButtons = "next" | "previous" | "close";
type Popover = {
title?: string;
description?: string;
side?: Side;
align?: Alignment;
showButtons?: AllowedButtons[];
showProgress?: boolean;
disableButtons?: AllowedButtons[];
popoverClass?: string;
fallbackScreenshot?: string;
progressText?: string;
doneBtnText?: string;
nextBtnText?: string;
prevBtnText?: string;
video_info?: {
youtube_url: string;
start_time?: number;
};
onPopoverRender?: (popover: PopoverDOM, opts: {
config: Config;
state: State;
driver: Driver;
}) => void;
onNextClick?: DriverHook;
onPrevClick?: DriverHook;
onCloseClick?: DriverHook;
};
type PopoverDOM = {
wrapper: HTMLElement;
arrow: HTMLElement;
title: HTMLElement;
description: HTMLElement;
footer: HTMLElement;
progress: HTMLElement;
previousButton: HTMLButtonElement;
nextButton: HTMLButtonElement;
closeButton: HTMLButtonElement;
dragHandle: HTMLElement;
footerButtons: HTMLElement;
reactionContainer?: HTMLElement;
thumbsUpButton?: HTMLButtonElement;
thumbsDownButton?: HTMLButtonElement;
};
/**
* Outlines the core data structures (`WalkthroughResponse`, `HistoryStep`, etc.) used when describing a walkthrough's steps, screenshots, and embedded media. It covers everything needed to store and process the step-by-step instructions for a Sammy flow.
*/
interface ModelState {
memory: string;
next_goal: string;
evaluation_previous_goal: string;
}
interface ModelAction {
[key: string]: {
[key: string]: string;
};
}
interface ModelOutput {
action: ModelAction[];
current_state: ModelState;
}
interface TabState {
url: string;
title: string;
page_id: number;
}
interface InteractiveElement {
xpath: string;
attributes?: Record<string, string>;
el_text?: string;
tag_name?: string;
textContent?: string;
entire_parent_branch_path?: string[];
highlight_index?: number;
shadow_root?: boolean;
}
interface StateInfo {
url: string;
path?: string;
tabs: TabState[];
title: string;
screenshot: string;
interacted_element: InteractiveElement[];
}
interface ResultInfo {
is_done: boolean;
extracted_content: string;
include_in_memory: boolean;
screenshot?: string;
is_loop?: boolean;
}
interface HistoryStep {
state: StateInfo;
result: ResultInfo[];
step_num: number;
user_text: string;
model_output: ModelOutput;
/**
* Indicates how this step behaves.
* "interactive" = default behavior (user must click/enter text in the highlighted element)
* "informational" = user can interact freely with the page, advancing only when they click the "Next" button
*/
step_type?: "interactive" | "informational";
/**
* Optional information for embedding a YouTube video
*/
video_info?: {
/**
* Full YouTube URL (can include timestamp)
* e.g. https://www.youtube.com/watch?v=videoId&t=120
*/
youtube_url: string;
/**
* Optional start time in seconds (alternative to including it in the URL)
*/
start_time?: number;
};
/**
* Optional observer element for fallback matching
*/
observer_element?: InteractiveElement;
/**
* Optional FAQ questions for informational steps
*/
faq?: Array<{
q?: string;
a?: string;
question?: string;
answer?: string;
}>;
faqs?: Array<{
q?: string;
a?: string;
question?: string;
answer?: string;
}>;
/**
* Optional email configuration for informational steps
*/
emailTitle?: string;
email_title?: string;
emailPlaceholder?: string;
email_placeholder?: string;
emailButtonText?: string;
email_button_text?: string;
emailAddress?: string;
email_address?: string;
emailSubject?: string;
email_subject?: string;
/**
* Optional highlight control for informational steps
*/
disableHighlight?: boolean;
disable_highlight?: boolean;
}
interface WalkthroughResponse {
flow_id: string;
flow_run_id?: string;
flow_version_id?: string;
title?: string;
history: HistoryStep[];
video_url?: string;
replay_screenshots?: string[];
}
/**
* Interface for the search result format returned by the API
*/
interface WalkthroughSearchResult {
flow_id: string;
flow_version_id?: string;
title?: string;
similarity_score?: number;
content?: string;
walkthrough_data: {
history: HistoryStep[];
video_url?: string;
};
}
/**
* Houses the global settings for all walkthroughs, like default wait times, debug mode, or domain overriding. This config is the bigger-picture counterpart to the more per-step driver configuration.
*/
/**
* Default global configuration for walkthroughs.
* This replaces the old CONFIG object from flow-guide.tsx.
*/
declare const DEFAULT_GLOBAL_CONFIG: {
flowIdQueryParam: string;
navigateToWalkthroughViaUrlParam: boolean;
waitTimeAfterLoadMs: number;
maxElementFindAttempts: number;
elementFindTimeoutMs: number;
domStabilityMs: number;
maxDomStabilityWaitMs: number;
debug: boolean;
apiBaseUrl: string;
logoUrl: string;
imageBaseUrl: string;
overrideDomainUrl: string;
askInput: boolean;
enableLogging: boolean;
enableLocationChangeEvents: boolean;
locationChangePollInterval: number;
locationChangeDebug: boolean;
domStabilityMsRedirect: number;
maxDomStabilityWaitMsRedirect: number;
};
/**
* Global walkthrough configuration type
*/
type WalkthroughConfig = typeof DEFAULT_GLOBAL_CONFIG;
/**
* Step types
*/
declare const STEP_TYPES: {
INTERACTIVE: string;
INFORMATIONAL: string;
};
/**
* Defines the main `driver` object which controls the highlight lifecycle (start, move to next step, destroy, etc.). It's the core orchestrator of highlighting and popover sequencing within a single walkthrough session.
*/
type DriveStep = {
element?: string | Element | (() => Element);
onHighlightStarted?: DriverHook;
onHighlighted?: DriverHook;
onDeselected?: DriverHook;
popover?: Popover;
disableActiveInteraction?: boolean;
step_type?: typeof STEP_TYPES.INTERACTIVE | typeof STEP_TYPES.INFORMATIONAL;
url?: string;
faq?: Array<{
q?: string;
a?: string;
question?: string;
answer?: string;
}>;
faqs?: Array<{
q?: string;
a?: string;
question?: string;
answer?: string;
}>;
emailTitle?: string;
email_title?: string;
emailPlaceholder?: string;
email_placeholder?: string;
emailButtonText?: string;
email_button_text?: string;
emailAddress?: string;
email_address?: string;
emailSubject?: string;
email_subject?: string;
disableHighlight?: boolean;
disable_highlight?: boolean;
};
interface Driver {
isActive: () => boolean;
refresh: () => void;
drive: (stepIndex?: number) => void;
setConfig: (config: Config) => void;
setSteps: (steps: DriveStep[]) => void;
getConfig: () => Config;
getState: (key?: string) => any;
getActiveIndex: () => number | undefined;
isFirstStep: () => boolean;
isLastStep: () => boolean;
getActiveStep: () => DriveStep | undefined;
getActiveElement: () => Element | undefined;
getPreviousElement: () => Element | undefined;
getPreviousStep: () => DriveStep | undefined;
moveNext: () => void;
movePrevious: () => void;
moveTo: (index: number) => void;
hasNextStep: () => boolean;
hasPreviousStep: () => boolean;
highlight: (step: DriveStep) => void;
destroy: () => void;
}
/**
* Stores the active driver config as a global object and provides functions to merge new config. It's the single place where the default driver settings (like popover offset) get combined with user overrides.
*/
type DriverHook = (element: Element | undefined, step: DriveStep, opts: {
config: Config;
state: State;
driver: Driver;
}) => void;
interface Config {
steps?: DriveStep[];
enableLogging?: boolean;
animate?: boolean;
overlayColor?: string;
overlayOpacity?: number;
smoothScroll?: boolean;
allowClose?: boolean;
overlayClickBehavior?: "close" | "nextStep";
stagePadding?: number;
stageRadius?: number;
disableActiveInteraction?: boolean;
allowKeyboardControl?: boolean;
popoverClass?: string;
popoverOffset?: number;
showButtons?: AllowedButtons[];
disableButtons?: AllowedButtons[];
showProgress?: boolean;
askInput?: boolean;
progressText?: string;
nextBtnText?: string;
prevBtnText?: string;
doneBtnText?: string;
logoUrl?: string;
onPopoverRender?: (popover: PopoverDOM, opts: {
config: Config;
state: State;
driver: Driver;
}) => void;
onHighlightStarted?: DriverHook;
onHighlighted?: DriverHook;
onDeselected?: DriverHook;
onDestroyStarted?: DriverHook;
onDestroyed?: DriverHook;
onNextClick?: DriverHook;
onPrevClick?: DriverHook;
onCloseClick?: DriverHook;
onFirstStepActive?: DriverHook;
showOverlay?: boolean;
scrollIntoViewOptions?: ScrollIntoViewOptions;
allowMovePopover?: boolean;
debug?: boolean;
domStabilityMsRedirect?: number;
maxDomStabilityWaitMsRedirect?: number;
domStabilityMs?: number;
maxDomStabilityWaitMs?: number;
}
/**
* Keep the Hook (`useWalkthrough`) focused on:
- Reading relevant state out of context (e.g., isLoading, error).
- Exposing simple, friendly functions to start or stop a walkthrough (`start`, `stop`).
- Extra convenience methods (like a `startWithData(...)`) to let people directly pass flow data.
* TODO: I need to liunk this file with the functionality of the driver
* So we can initiate the walkthroughs easily
*
* Hook (`useWalkthrough`) focused on:
* - Reading relevant state out of context (e.g., isLoading, error).
* - Exposing simple, friendly functions to start or stop a walkthrough (`start`, `stop`).
* - Extra convenience methods (like a `startWithData(...)`) to let people directly pass flow data.
* Main hook that coordinates walkthrough functionality, now using specialized hooks for specific responsibilities
*/
interface UseWalkthroughOptions {
/**
* Check for walkthrough query parameter on mount
*/
checkQueryOnMount?: boolean;
/**
* Custom error handler for walkthrough errors
*/
onError?: (error: FlowError) => void;
/**
* Wait time in milliseconds before starting walkthrough after page load
*/
waitTime?: number;
/**
* Driver configuration options
*/
driverConfig?: Partial<Config>;
/**
* Global configuration options (replaces the old CONFIG object from flow-guide)
*/
config?: Partial<WalkthroughConfig>;
/**
* Disable automatic URL redirection when the current URL doesn't match the walkthrough's initial URL
* When true, the walkthrough will attempt to run on the current page regardless of URL mismatch
*/
disableRedirects?: boolean;
/**
* Automatically start a pending walkthrough on component mount
*/
autoStartPendingWalkthrough?: boolean;
/**
* Maximum time (in ms) to wait for DOM element to be found before using fallback
* Defaults to 10000 (10s) if not provided
*/
fallbackTimeout?: number;
}
/**
* Hook to interact with walkthrough functionality
*/
declare function useWalkthrough(options?: UseWalkthroughOptions): {
/**
* Start a walkthrough by flow ID using the API
* @param flowId The ID of the walkthrough flow to start
*/
startWithId: (flowId: string | number) => Promise<boolean>;
/**
* Start a walkthrough with pre-fetched data
* @param data The walkthrough data to use. Supports various formats:
* - WalkthroughResponse (history at top level)
* - WalkthroughSearchResult (with walkthrough_data.history)
* - Older format with extra_metadata.history
*/
startWithData: (data: WalkthroughResponse | WalkthroughSearchResult | any) => Promise<boolean>;
/**
* Alias for startWithId for backward compatibility
* @deprecated Use startWithId instead
*/
start: (flowId: string | number) => Promise<boolean>;
/**
* Stop the currently active walkthrough
* @returns True if a walkthrough was stopped, false otherwise
*/
stop: () => boolean;
/**
* Check if a walkthrough is currently active
*/
isActive: () => boolean;
/**
* Configure the driver with custom options
*/
configure: (driverOptions: Partial<Config>) => void;
/**
* Configure global settings (replaces old CONFIG object)
*/
configureGlobal: (config: Partial<{
flowIdQueryParam: string;
navigateToWalkthroughViaUrlParam: boolean;
waitTimeAfterLoadMs: number;
maxElementFindAttempts: number;
elementFindTimeoutMs: number;
domStabilityMs: number;
maxDomStabilityWaitMs: number;
debug: boolean;
apiBaseUrl: string;
logoUrl: string;
imageBaseUrl: string;
overrideDomainUrl: string;
askInput: boolean;
enableLogging: boolean;
enableLocationChangeEvents: boolean;
locationChangePollInterval: number;
locationChangeDebug: boolean;
domStabilityMsRedirect: number;
maxDomStabilityWaitMsRedirect: number;
}>) => void;
/**
* Check for pending walkthroughs manually
*/
checkForPendingWalkthrough: () => Promise<void>;
/**
* Current walkthrough state
*/
state: {
isTokenValid: boolean;
isLoading: boolean;
error: FlowError | null;
token: string | null;
baseUrl: string;
isActive: boolean;
config: {
flowIdQueryParam: string;
navigateToWalkthroughViaUrlParam: boolean;
waitTimeAfterLoadMs: number;
maxElementFindAttempts: number;
elementFindTimeoutMs: number;
domStabilityMs: number;
maxDomStabilityWaitMs: number;
debug: boolean;
apiBaseUrl: string;
logoUrl: string;
imageBaseUrl: string;
overrideDomainUrl: string;
askInput: boolean;
enableLogging: boolean;
enableLocationChangeEvents: boolean;
locationChangePollInterval: number;
locationChangeDebug: boolean;
domStabilityMsRedirect: number;
maxDomStabilityWaitMsRedirect: number;
};
};
};
declare enum LogEventType {
START = "start",
STEP = "step",
FINISH = "finish",
ABANDON = "abandon",
REDIRECT = "redirect",
FALLBACK = "fallback",
REACTION = "reaction"
}
interface LogEventBase {
event_type: LogEventType;
user_replay_id: string;
}
interface StartEvent extends LogEventBase {
event_type: LogEventType.START;
flow_id: string;
flow_version_id: string;
started_at?: string;
}
interface StepEvent extends LogEventBase {
event_type: LogEventType.STEP;
user_replay_step_id: string;
status?: string;
step_number?: number;
step_initiated_at?: string;
element_found_at?: string;
element_interacted_at?: string;
}
interface FinishEvent extends LogEventBase {
event_type: LogEventType.FINISH;
user_replay_step_id: string;
status?: string;
step_number?: number;
step_initiated_at?: string;
element_found_at?: string;
element_interacted_at?: string;
ended_at?: string;
}
interface AbandonEvent extends LogEventBase {
event_type: LogEventType.ABANDON;
user_replay_step_id: string;
status?: string;
step_number?: number;
step_initiated_at?: string;
element_found_at?: string;
element_interacted_at?: string;
ended_at?: string;
}
interface RedirectEvent extends LogEventBase {
event_type: LogEventType.REDIRECT;
user_replay_step_id: string;
status?: string;
step_number?: number;
step_initiated_at?: string;
element_found_at?: string;
element_interacted_at?: string;
ended_at?: string;
}
interface FallbackEvent extends LogEventBase {
event_type: LogEventType.FALLBACK;
user_replay_step_id: string;
status?: string;
step_number?: number;
step_initiated_at?: string;
element_found_at?: string;
element_interacted_at?: string;
ended_at?: string;
flow_id: string;
flow_version_id: string;
}
interface ReactionEvent extends LogEventBase {
event_type: LogEventType.REACTION;
user_replay_step_id: string;
reaction: "THUMBS_UP" | "THUMBS_DOWN" | "NONE";
step_number?: number;
}
type LogEvent = StartEvent | StepEvent | FinishEvent | AbandonEvent | RedirectEvent | FallbackEvent | ReactionEvent;
interface WalkthroughProviderProps {
children: ReactNode;
token?: string;
baseUrl?: string;
onTokenExpired?: () => void;
onError?: (error: FlowError) => void;
driverConfig?: Partial<Config>;
config?: Partial<WalkthroughConfig>;
logoUrl?: string;
locationChangeEvents?: boolean;
locationChangePollInterval?: number;
locationChangeDebug?: boolean;
onWalkthroughEvent?: (event: LogEvent) => void;
}
declare function WalkthroughProvider({ children, token: initialToken, baseUrl, onTokenExpired, onError, driverConfig, config: userConfig, logoUrl, locationChangeEvents, locationChangePollInterval, locationChangeDebug, onWalkthroughEvent, }: WalkthroughProviderProps): react_jsx_runtime.JSX.Element;
export { DEFAULT_GLOBAL_CONFIG, Config as DriverConfig, FlowError, HistoryStep, InteractiveElement, UseWalkthroughOptions, WalkthroughConfig, WalkthroughProvider, WalkthroughProviderProps, WalkthroughResponse, useWalkthrough };