UNPKG

@sammy-labs/walkthroughs

Version:
655 lines (634 loc) 20.6 kB
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 };