import * as v from 'valibot' export const LATEST_WEBCHAT_CONFIG_VERSION = 'v2' as const const ipv4CidrRegex = /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\/(?:[0-9]|[1-2]\d|3[0-2]))?$/ export const webchatIpBlocklistEntrySchema = v.pipe( v.string(), v.regex(ipv4CidrRegex, 'Each entry must be a valid IPv4 address or CIDR range') ) // A short list of common starter icons surfaced first in the dashboard / Viber icon pickers. // The `icon` field accepts ANY lucide icon name (kebab-case), so this is only a convenience set — // not a validation constraint. The widget lazy-loads the named icon and falls back to DEFAULT. export const suggestedConversationStarterIcons = [ 'sparkles', 'message-circle', 'package', 'truck', 'zap', 'rotate-ccw', 'headset', 'circle-help', 'credit-card', 'gift', 'user', 'settings', 'search', 'calendar', 'mail', 'star', ] as const export const DEFAULT_CONVERSATION_STARTER_ICON = 'sparkles' export const conversationStarterDisplayStyles = ['chips', 'cards', 'grid'] as const export const conversationStarterSchema = v.object({ id: v.string(), // The message sent on click; also the default display title. text: v.string(), title: v.optional(v.string()), description: v.optional(v.string()), // Any lucide icon name (kebab-case, e.g. 'package', 'message-circle'). Optional — the widget // lazy-loads it and falls back to DEFAULT_CONVERSATION_STARTER_ICON when unset or unknown. icon: v.optional(v.string()), // Defaults to enabled. Disabled starters stay saved but don't render in the widget. enabled: v.optional(v.boolean()), }) export const configSchemaV0 = v.object({ botName: v.optional(v.string()), botAvatar: v.optional(v.string()), botDescription: v.optional(v.string()), fabImage: v.optional(v.string()), phone: v.fallback(v.optional(v.object({ title: v.optional(v.string()), link: v.optional(v.string()) })), undefined), email: v.fallback(v.optional(v.object({ title: v.optional(v.string()), link: v.optional(v.string()) })), undefined), website: v.fallback(v.optional(v.object({ title: v.optional(v.string()), link: v.optional(v.string()) })), undefined), termsOfService: v.fallback( v.optional(v.object({ title: v.optional(v.string()), link: v.optional(v.string()) })), undefined ), privacyPolicy: v.fallback( v.optional(v.object({ title: v.optional(v.string()), link: v.optional(v.string()) })), undefined ), composerPlaceholder: v.optional(v.string()), color: v.optional(v.string()), radius: v.optional(v.pipe(v.number(), v.minValue(0.5), v.maxValue(4))), themeMode: v.fallback(v.optional(v.picklist(['light', 'dark'])), undefined), variant: v.fallback(v.optional(v.picklist(['solid', 'soft'])), undefined), fontFamily: v.fallback(v.optional(v.string()), undefined), showPoweredBy: v.fallback(v.optional(v.boolean()), undefined), additionalStylesheet: v.optional(v.string()), additionalStylesheetUrl: v.optional(v.string()), allowFileUpload: v.fallback(v.optional(v.boolean()), undefined), storageLocation: v.fallback(v.optional(v.picklist(['localStorage', 'sessionStorage'])), undefined), }) const { showPoweredBy, ...v0EntriesWithoutShowPoweredBy } = configSchemaV0.entries const configSchemaV1 = v.object({ ...v0EntriesWithoutShowPoweredBy, footer: v.optional(v.string()), headerVariant: v.optional(v.picklist(['solid', 'glass'])), feedbackEnabled: v.optional(v.boolean()), }) const configSchemaV2 = v.object({ ...configSchemaV1.entries, soundEnabled: v.optional(v.boolean()), adminSecret: v.optional(v.string()), embeddedChatId: v.optional(v.string()), toggleChatId: v.optional(v.string()), proactiveMessageEnabled: v.optional(v.boolean()), proactiveBubbleMessage: v.optional(v.string()), proactiveBubbleTriggerType: v.optional(v.picklist(['afterDelay', 'userIdle'])), proactiveBubbleDelayTime: v.optional(v.pipe(v.number(), v.minValue(0), v.maxValue(60))), _debug: v.optional(v.boolean()), conversationHistory: v.optional(v.boolean()), storageLocation: v.fallback(v.optional(v.picklist(['localStorage', 'sessionStorage', 'cookieStorage'])), undefined), cookieDomain: v.optional(v.string()), rateLimit: v.optional( v.object({ enabled: v.optional(v.boolean()), requests: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1))), windowMinutes: v.optional(v.pipe(v.number(), v.integer(), v.minValue(1))), }) ), ipBlocklist: v.optional(v.array(webchatIpBlocklistEntrySchema)), // Which webchat UI major a bot is opted into (v4 = classic, v5 = new). Undefined = v4. // This is the persisted opt-in pointer; the `version` field above is the config *schema* version. webchatMajorVersion: v.optional(v.picklist(['v4', 'v5'])), // Home page (welcome screen, v5 only). Master toggle for the custom welcome screen. // When off, the widget shows the default bot-name marquee. homePageEnabled: v.optional(v.boolean()), // Hero text — when a heading is set it replaces the centered bot-name marquee. welcomeHeading: v.optional(v.string()), welcomeSubtitle: v.optional(v.string()), // Main "ask a question" CTA card. Off by default. mainCardEnabled: v.optional(v.boolean()), mainCardTitle: v.optional(v.string()), mainCardSubtitle: v.optional(v.string()), // Conversation starters shown on the welcome screen. Off by default; order = display order. conversationStartersEnabled: v.optional(v.boolean()), conversationStarters: v.optional(v.array(conversationStarterSchema)), conversationStartersDisplayStyle: v.optional(v.picklist(conversationStarterDisplayStyles)), // Inline citation pills + Sources footer on messages that carry // `metadata.citations` (v5 only). DISABLED unless explicitly true — the // webchat integration is not versioned, so existing bots must keep their // behavior. Viber stamps it true on its (always-v5) bots; echo's webchat // integration definition needs the key (default false) so the generated // public config file carries it to published webchats. citationsEnabled: v.optional(v.boolean()), }) export const configSchema = v.union([configSchemaV0, configSchemaV1, configSchemaV2]) export const latestVersionedConfigSchema = v.object({ ...configSchemaV2.entries, version: v.literal('v2'), }) export const versionedConfigSchema = v.variant('version', [ //NOTE: Latest needs to be in first or else the parser will always return v0 latestVersionedConfigSchema, v.object({ ...configSchemaV1.entries, version: v.literal('v1') }), v.object({ ...configSchemaV0.entries, version: v.fallback(v.literal('v0'), 'v0') }), ]) export const webchatConfigFileSchema = v.object({ botId: v.string(), configuration: versionedConfigSchema, clientId: v.string(), }) export type LatestWebchatConfig = v.InferOutput export type ConversationStarter = v.InferOutput export type ConversationStarterDisplayStyle = (typeof conversationStarterDisplayStyles)[number] export type LatestWebchatTheme = Pick< LatestWebchatConfig, | 'color' | 'fontFamily' | 'radius' | 'themeMode' | 'variant' | 'headerVariant' | 'additionalStylesheet' | 'additionalStylesheetUrl' > export type VersionedWebchatConfig = v.InferOutput export type AllPossibleConfigFields = Partial< v.InferOutput & v.InferOutput & v.InferOutput > export type LatestVersionedWebchatConfig = v.InferOutput export const DEFAULT_TOGGLE_CHAT_ID = 'bp-toggle-chat' export const DEFAULT_EMBEDDED_CHAT_ID = 'bp-embedded-webchat' export const initialConfig: Pick< LatestWebchatConfig, | 'color' | 'themeMode' | 'radius' | 'variant' | 'headerVariant' | 'fontFamily' | 'footer' | 'feedbackEnabled' | 'soundEnabled' | 'proactiveMessageEnabled' | 'proactiveBubbleMessage' | 'proactiveBubbleTriggerType' | 'proactiveBubbleDelayTime' | 'conversationHistory' | 'rateLimit' | 'ipBlocklist' | 'homePageEnabled' | 'mainCardEnabled' | 'conversationStartersEnabled' | 'conversationStarters' | 'conversationStartersDisplayStyle' > = { color: '#3276EA', themeMode: 'light', radius: 4, variant: 'solid', headerVariant: 'glass', fontFamily: 'inter', footer: '[⚡ by Botpress](https://botpress.com/?from=webchat)', feedbackEnabled: false, soundEnabled: false, proactiveMessageEnabled: false, proactiveBubbleMessage: 'Hi! 👋 Need help?', proactiveBubbleTriggerType: 'afterDelay', proactiveBubbleDelayTime: 10, conversationHistory: false, rateLimit: { enabled: false, requests: 25, windowMinutes: 1, }, ipBlocklist: [], homePageEnabled: false, mainCardEnabled: false, conversationStartersEnabled: false, conversationStarters: [], conversationStartersDisplayStyle: 'cards', }