saltfish
Version:
An interactive video-guided tour system for web applications
478 lines • 13.9 kB
TypeScript
/**
* Player state constants
*/
export declare const PLAYER_STATES: {
readonly IDLE: "idle";
readonly LOADING: "loading";
readonly PLAYING: "playing";
readonly PAUSED: "paused";
readonly WAITING_FOR_INTERACTION: "waitingForInteraction";
readonly AUTOPLAY_BLOCKED: "autoplayBlocked";
readonly IDLE_MODE: "idleMode";
readonly MINIMIZED: "minimized";
readonly ERROR: "error";
readonly COMPLETED_WAITING_FOR_INTERACTION: "completedWaitingForInteraction";
readonly COMPLETED: "completed";
readonly CLOSING: "closing";
};
/**
* Position constants
*/
export declare const POSITIONS: {
readonly BOTTOM_RIGHT: "bottom-right";
readonly BOTTOM_LEFT: "bottom-left";
};
/**
* Storage keys
*/
export declare const STORAGE_KEYS: {
readonly PROGRESS: "saltfish_progress";
readonly SESSION: "saltfish_session";
readonly ANONYMOUS_USER: "saltfish_anonymous_user_data";
readonly PENDING_NAVIGATION: "saltfish_pending_navigation";
};
/**
* Event names
*/
export declare const EVENT_NAMES: {
readonly INITIALIZED: "initialized";
readonly PLAYLIST_STARTED: "playlistStarted";
readonly PLAYLIST_ENDED: "playlistEnded";
readonly PLAYLIST_DISMISSED: "playlistDismissed";
readonly STEP_STARTED: "stepStarted";
readonly STEP_ENDED: "stepEnded";
readonly PLAYER_PAUSED: "playerPaused";
readonly PLAYER_RESUMED: "playerResumed";
readonly PLAYER_MINIMIZED: "playerMinimized";
readonly PLAYER_MAXIMIZED: "playerMaximized";
readonly USER_DATA_LOADED: "userDataLoaded";
readonly INTERACTION_REQUIRED: "interactionRequired";
readonly INTERACTION_COMPLETED: "interactionCompleted";
readonly ERROR: "error";
};
/**
* API constants
*/
export declare const API: {
readonly BASE_URL: "https://player.saltfish.ai";
readonly SHARE_BASE_URL: "https://studio-api.saltfish.ai/studio/flows2/share";
readonly ENDPOINTS: {
readonly VALIDATE_TOKEN: "/validate-token";
readonly USERS: "/clients/{token}/users/{userId}";
};
};
/**
* CSS class names
*/
export declare const CSS_CLASSES: {
readonly PLAYER: "sf-player";
readonly PLAYER_MINIMIZED: "sf-player--minimized";
readonly VIDEO_CONTAINER: "sf-video-container";
readonly CONTROLS_CONTAINER: "sf-controls-container";
readonly LOGO: "sf-player__logo";
readonly ERROR: "saltfish-error";
readonly ERROR_DISPLAY: "sf-error-display";
readonly ERROR_DISPLAY_VISIBLE: "sf-error-display--visible";
readonly ERROR_DISPLAY_CONTENT: "sf-error-display__content";
readonly ERROR_DISPLAY_MESSAGE: "sf-error-display__message";
readonly LOADING_SPINNER: "sf-loading-spinner";
};
/**
* Default configuration
*/
export declare const DEFAULTS: {
readonly POSITION: "bottom-right";
readonly PERSISTENCE: true;
readonly SESSION_RECORDING: false;
readonly ANALYTICS_ENABLED: true;
};
/**
* Represents the state of the Saltfish playlist Player
*/
export type PlayerState = typeof PLAYER_STATES[keyof typeof PLAYER_STATES];
/**
* Available position options
*/
export type Position = typeof POSITIONS[keyof typeof POSITIONS];
/**
* Base interface for managers that can be reset instead of recreated
*/
export interface ResettableManager {
/** Resets the manager to initial state for reuse */
reset(): void;
/** Completely destroys the manager and cleans up resources */
destroy(): void;
}
/**
* Configuration options for initializing the Saltfish playlist Player
*/
export interface SaltfishConfig {
/** API token for authentication */
token: string;
/** Whether to enable analytics data collection and sending (default: true) */
enableAnalytics?: boolean;
/** Whether to show the saltfish logo at the bottom of the widget (set from backend) */
showLogo?: boolean;
}
/**
* Options for starting a playlist
*/
export interface PlaylistOptions {
position?: Position;
startNodeId?: string;
once?: boolean;
persistence?: boolean;
_triggeredByTriggerManager?: boolean;
_startedFromShareLink?: boolean;
_isGlobalShare?: boolean;
}
/**
* User identification data
*/
export interface UserData {
id: string;
[key: string]: any;
}
/**
* Point coordinates for cursor animation
*/
export interface Point {
x: number;
y: number;
time?: number;
}
/**
* Interactive button overlay configuration
*/
export interface ButtonOverlay {
id: string;
text: string;
style?: any;
action: {
type: 'next' | 'goto' | 'url' | 'playlist';
target: string;
url?: string;
};
}
/**
* Expected element size for validation
* Used to validate that found elements match the expected size from recording
*/
export interface ExpectedSize {
width: number;
height: number;
}
/**
* Expected element tag and text for validation
* Used to validate that found elements match the expected tag name and text content from recording
* This is tried first before falling back to size-based validation
*/
export interface ExpectedElement {
tagName: string;
textContent: string;
}
/**
* Cursor animation configuration
*/
export interface CursorAnimation {
easing?: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out';
targetSelector: string;
mode?: 'pointer' | 'selection';
/**
* Time in seconds from video start when the animation should appear
* - Default: 0 (immediate, at video start)
* - Negative values: Treated as 0 (immediate)
* - Values exceeding video duration: Animation triggers immediately once duration is known
* - Invalid values (NaN, Infinity): Treated as 0 (immediate)
* - If video ends and step transitions: Animation is cancelled (won't show on next step)
*/
showAtSeconds?: number;
selectionStyles?: {
borderColor?: string;
borderWidth?: string;
borderRadius?: string;
padding?: string | number;
};
expectedSize?: ExpectedSize;
expectedElement?: ExpectedElement;
}
/**
* Step transition condition
*/
export interface TransitionCondition {
type: 'timeout' | 'dom-click' | 'url-path' | 'dom-element-visible';
target?: string;
value?: any;
timeout?: number;
nextStep: string;
expectedSize?: ExpectedSize;
expectedElement?: ExpectedElement;
}
/**
* Transcript chunk with timing information (word-level)
*/
export interface TranscriptChunk {
text: string;
start: number;
end: number;
}
/**
* Transcript data for a video step
*/
export interface Transcript {
text: string;
chunks: TranscriptChunk[];
}
/**
* Translation data for a step
*/
export interface StepTranslation {
videoUrl: string;
gifUrl: string;
audioUrl: string;
transcript: Transcript;
}
/**
* URL requirement for step validation
*/
export interface UrlRequirement {
pattern: string;
matchType: 'exact' | 'contains' | 'regex';
}
/**
* Step configuration
*/
export interface Step {
id: string;
videoUrl: string;
compressedUrl?: string;
audioUrl?: string;
gifUrl?: string;
position?: Position;
buttons?: ButtonOverlay[];
cursorAnimations?: CursorAnimation[];
transitions: TransitionCondition[];
transcript?: Transcript;
translations?: Record<string, StepTranslation>;
urlRequirement?: UrlRequirement;
}
/**
* Device type options
*/
export type DeviceType = 'desktop' | 'mobile' | 'both';
/**
* Playlist manifest interface
*/
export interface PlaylistManifest {
id: string;
name: string;
version: string;
position: Position;
startStep: string;
steps: Step[];
translations?: Record<string, Record<string, string>>;
cursorColor?: string;
cursorLabel?: string;
deviceType?: DeviceType;
idleMode?: boolean;
compactFirstStep?: boolean;
compactLabel?: string;
isPersistent?: boolean;
captions?: boolean;
avatarThumbnailUrl?: string;
}
/**
* Error event data with enriched diagnostic information
*/
export interface ErrorEventData {
message: string;
stack?: string;
errorType: string;
videoUrl?: string;
mediaErrorCode?: number;
mediaErrorMessage?: string;
failureReason?: 'timeout' | 'media_error' | 'load_error' | 'unknown';
}
/**
* Analytics event
*/
export interface AnalyticsEvent {
type: 'playlistStart' | 'playlistComplete' | 'stepStarted' | 'stepComplete' | 'interaction' | 'error' | 'playerPaused' | 'playerResumed' | 'playerMinimized' | 'playerMaximized';
playlistId: string;
stepId?: string;
runId: string;
timestamp: number;
data?: any;
}
/**
* Available playlist metadata
*/
export interface PlaylistMetadata {
id: string;
name: string;
description?: string;
thumbnailUrl?: string;
}
/**
* Represents a single user attribute condition for trigger evaluation
*/
export interface UserAttributeCondition {
attributeKey: string;
attributeType: 'string' | 'boolean' | 'int' | 'date';
operator: 'equals' | 'notEquals' | 'greaterThan' | 'lessThan';
value: string;
}
/**
* Represents trigger conditions for playlists with automatic triggers
*/
export interface PlaylistTriggers {
maxVisits: number | null;
operators: string[];
playlistNotSeen: string[];
playlistSeen: string[];
url: string | null;
urlMatchType?: 'exact' | 'contains' | 'regex';
elementClicked: string | null;
elementClickedExpectedSize?: ExpectedSize;
elementClickedExpectedElement?: ExpectedElement;
elementVisible: string | null;
elementVisibleExpectedSize?: ExpectedSize;
elementVisibleExpectedElement?: ExpectedElement;
userAttributes: UserAttributeCondition[];
}
/**
* Represents a playlist entry with path info returned by the backend
*/
export interface PlaylistPathInfo {
id: string;
path: string;
hasTriggers: boolean;
autoStart?: boolean;
triggers?: PlaylistTriggers;
deviceType?: DeviceType;
}
/**
* State store
*/
export interface SaltfishStore {
config: SaltfishConfig | null;
user: UserData | null;
userData: BackendUserData | null;
readonly currentState: PlayerState;
manifest: PlaylistManifest | null;
currentStepId: string | null;
isMinimized: boolean;
position: {
x: number;
y: number;
} | null;
progress: Record<string, Record<string, unknown>>;
error: Error | null;
playlistOptions: PlaylistOptions | null;
backendPlaylists?: PlaylistPathInfo[];
isAdmin?: boolean;
isMuted: boolean;
abTests?: ABTestConfig[];
abTestAssignments?: Record<string, ABTestAssignment>;
initialize: (config: SaltfishConfig) => Promise<void>;
identifyUser: (userId: string, userData?: Record<string, unknown>) => void;
setUserData: (userData: BackendUserData) => void;
setManifest: (manifest: PlaylistManifest, startStepId: string) => void;
play: () => void;
pause: () => void;
minimize: () => void;
maximize: () => void;
goToStep: (stepId: string) => void;
reset: () => void;
setError: (error: Error) => void;
setAutoplayFallback: () => void;
setIdleMode: () => void;
setMuted: (muted: boolean) => void;
setPlaylistOptions: (options: PlaylistOptions) => void;
setBackendPlaylists?: (playlists: PlaylistPathInfo[]) => void;
setIsAdmin?: (isAdmin: boolean) => void;
completePlaylist: () => void;
resetForNewPlaylist: () => void;
loadPlaylistProgress: (playlistId: string, progress: Record<string, unknown>) => void;
registerStateMachineActions: (actions: Record<string, any>) => void;
sendStateMachineEvent: (event: any) => void;
updateProgressWithCompletion: (playlistId: string, currentStepId: string) => void;
setABTests: (abTests: ABTestConfig[]) => void;
setABTestAssignments: (assignments: Record<string, ABTestAssignment>) => void;
getFilteredPlaylists: () => PlaylistPathInfo[];
}
/**
* Interface for watched playlist status
*/
export interface WatchedPlaylistStatus {
status: 'completed' | 'in_progress' | 'dismissed';
timestamp?: number;
visitCount?: number;
[key: string]: unknown;
}
/**
* Interface for watched playlists collection
*/
export interface WatchedPlaylists extends Record<string, WatchedPlaylistStatus | undefined> {
}
/**
* A/B Test Configuration
*/
export interface ABTestConfig {
id: string;
name: string;
playlistId: string;
testType?: 'percentage' | 'userList';
percentage?: number;
isActive: boolean;
}
/**
* A/B Test Assignment
*/
export interface ABTestAssignment {
testId: string;
assigned: boolean;
assignedAt: number;
}
/**
* Interface for user data from backend
*/
export interface BackendUserData {
watchedPlaylists: WatchedPlaylists;
abTestAssignments?: Record<string, ABTestAssignment>;
language?: string;
[key: string]: unknown;
}
/**
* Interface for anonymous user data stored in localStorage
*/
export interface AnonymousUserData {
userId: string;
userData: Record<string, unknown>;
watchedPlaylists: WatchedPlaylists;
timestamp: number;
abTestAssignments?: Record<string, ABTestAssignment>;
}
/**
* Pending navigation data for cross-page URL transitions
* Used when a step has a url-path transition and the user navigates to a non-SPA page
* causing a hard refresh. This allows resuming from the correct step on the new page.
*/
export interface PendingNavigation {
playlistId: string;
nextStepId: string;
urlPattern: string;
timestamp: number;
}
/**
* Interface for state machine - flexible structure to avoid circular imports
*/
export interface StateMachine {
send?: (action: string | {
type: string;
[key: string]: unknown;
}) => void;
registerActions?: (actions: Record<string, () => void>) => void;
[key: string]: unknown;
}
//# sourceMappingURL=index.d.ts.map