/** Locales the package ships translations for. Add a value here, ship a * matching catalog in `locales/`, and update `SUPPORTED_LOCALES`. */ type Locale = 'en' | 'es' | 'pt-br'; /** All supported locales, as a runtime array. Mirrors `Locale`. */ declare const SUPPORTED_LOCALES: ReadonlyArray; /** Fallback locale used when an unsupported value is passed or a key is * missing in the requested locale. */ declare const DEFAULT_LOCALE: Locale; /** Type guard for narrowing an unknown string to `Locale`. */ declare const isLocale: (value: unknown) => value is Locale; /** * One metric tile in the analytics digests. `value` is rendered verbatim * (callers format it). When `change` and `change_type` are both provided * a trend chip is shown; `change_type === 'increase'` styles green, any * other value (e.g. `'decrease'`) styles red. */ type Statistic = { /** Pre-formatted value string (e.g. "1,240", "98%", "1.2s"). */ value: string; /** Pre-formatted delta string (e.g. "+12%", "-3"). */ change?: string; /** Direction of the change. `'increase'` styles positive; anything else styles negative. */ change_type?: string; }; /** Per-code account-wide hallucination breakdown. `factual` merges the * factual + link hallucination codes into one number for display. */ type SafetyBreakdown = { /** Factual + link hallucinations combined. */ factual: number; /** Prompts that attempted to bypass policies — refused safely. */ jailbreak: number; /** Questions outside the agent's scope, declined gracefully. */ out_of_scope: number; }; /** * One insight row rendered under the Top AI agent metrics. Selected by a * ranker (`score = total_count * (1 - top_share)`) so we surface insights * with real volume AND real spread — see `getBrainTopSignals` in the * analytics app. Booleans render as a `true`/`false` pair with an `n=` * sample-size caption; categoricals render every bucket above the * min-share floor as its own row. */ type TopAgentInsight = TopAgentBooleanInsight | TopAgentCategoricalDistribution; type TopAgentBooleanInsight = { kind: 'boolean'; insight_name: string; /** Share of conversations where the insight fired true, 0-100 integer. */ triggered_pct: number; /** Total scored conversations — for the (n=…) caption. */ total_count: number; }; type TopAgentCategoricalDistribution = { kind: 'categorical_distribution'; insight_name: string; /** Buckets sorted by share desc; final entry may have `is_other: true` * to roll up the long tail when its combined share is meaningful. */ buckets: Array<{ label: string; share_pct: number; is_other?: boolean; }>; total_count: number; }; /** * Busiest AI agent for the period, with its three headline metrics and * (optionally) the brain's top categorical insights. Each metric and * the insights list are independently optional — the section degrades * gracefully when any are missing and hides entirely when all three * metrics are absent. */ type TopAgent = { /** Brain UUID, used by next-generation analytics templates for deep links. */ agent_id?: string; /** Display name from the orchestrator brains table. */ agent_name: string; /** Conversations handled in the current period. */ count: number; /** Average user CSAT for this brain's conversations, 0-5, 1-decimal. */ csat?: number; /** Containment rate, 0-100, pre-rounded. */ containment_pct?: number; /** Goal-achieved share, 0-100, pre-rounded. */ goal_pct?: number; /** Top insights for this brain, ranked by informativeness (volume × spread). * Mixed booleans + categoricals; an empty array hides the section. */ top_insights?: TopAgentInsight[]; }; /** One row in the digest's "Top human agents" table. */ type HumanTeamAgentRow = { /** Raw agent id from analytics (UUID for resolved members, or literals * like "external-agent"). Optional in the type, but the analytics * backend always sets it; used as a stable React key since display * names can collide. */ agent_id?: string; /** Display name from the orchestrator `members_view`; falls back to the * raw `agent_id` (UUID or literals like "external-agent") when the * agent has no matching user. */ agent_name: string; /** Conversations the agent participated in during the period. */ total_conversations: number; /** Avg seconds between user message and the agent's reply, across all * the agent's replies in the period. `null` when the agent had no * scoreable replies. */ avg_response_seconds: number | null; /** Avg CSAT received on conversations the agent handled, 0–5 scale. * `null` when the agent received no ratings. */ avg_csat_human_agent: number | null; }; /** * "Human team" digest section — for accounts that route a meaningful * share of conversations to live agents (WhatsApp + campaign-heavy * customers). Each headline tile is independently optional; the * section as a whole hides when `top_agents` is empty. */ type HumanTeam = { /** Total conversations a human agent participated in over the period. */ total_conversations: number; /** Avg seconds to first agent reply on a conversation, account-wide. */ avg_first_response_seconds?: number; /** Avg CSAT across human-handled conversations, 0–5 scale. */ avg_csat?: number; /** Avg seconds from conversation start to resolution, account-wide. */ avg_resolution_seconds?: number; /** Top 5 busiest agents by `total_conversations`, ordered desc. */ top_agents: HumanTeamAgentRow[]; }; /** * Region-aware URLs the consumer always knows about. Optional on every template * so callers can pass them once from a single config source. */ type EmailRegionContext = { /** Region-specific console URL, e.g. `https://console.eu.moveo.ai`. */ console_url?: string; /** Region-specific public API URL, e.g. `https://api.eu.moveo.ai`. */ api_url?: string; }; /** * White-label brand override. When set on any template's props, the email * renders with this brand instead of Moveo.AI — every "Moveo.AI" mention * in the copy switches to `brand.name`, the Moveo logo becomes brand-name * text, the footer attribution / address / Privacy & Terms links flip to * the customer's values (or hide when not provided), and Moveo-specific * CTAs (Welcome's Calendly link) are removed. * * Only `name` is required. Omitting `company_address` / `privacy_url` / * `terms_url` hides those footer elements entirely — we do NOT fall * back to Moveo's values. */ type BrandContext = { brand?: { name: string; company_address?: string; privacy_url?: string; terms_url?: string; }; }; /** Props for the `welcome` template — first-touch onboarding email. */ type WelcomeProps = EmailRegionContext & BrandContext & { first_name: string; /** "Reply-to" address shown in body copy and as the mailto: link. */ sender_email: string; /** Onboarding-call URL (e.g. Calendly / HubSpot meetings). When omitted in * Moveo mode the call-CTA is hidden. White-label always hides it. */ meeting_url?: string; }; /** Props for the `invitation` template — invites a user (new or existing) * to join an account. Toggle `isNew` to switch the CTA copy. */ type InvitationProps = EmailRegionContext & BrandContext & { /** True for users who don't yet exist — shows the registration CTA * instead of the sign-in CTA. */ isNew: boolean; recipient_name: string; inviter_name: string; account_name: string; account_avatar?: string; roles: string[]; link: string; /** Days until `link` expires. Shown verbatim; not enforced by the package. */ expires_in_days?: number; }; /** Props for the `request_access` template — notifies an account owner that * a user has requested a specific permission on a resource. */ type RequestAccessProps = EmailRegionContext & BrandContext & { /** Display name of the person requesting access. */ request_name: string; /** Permission identifier (e.g. "brains.read"). Rendered verbatim. */ request_permission: string; /** Human-readable account name shown in subject + body + footer. */ account_name: string; /** Account slug. Part of the orchestrator schema contract; not currently * rendered, kept for future deep-link construction inside the template. */ account_slug: string; /** Deep link to the access-management page in the console. */ manage_access_url: string; /** Optional free-text justification from the requester. */ request_message?: string; /** Existing roles on this account that already grant `request_permission`, * ordered least-privilege first (caller is expected to cap the list). * Rendered as a chip row above the CTA; the whole block hides when * omitted or empty. */ suggested_roles?: string[]; }; type ConversationAssignedUser = { display_name?: string; email?: string; locale?: string; phone?: string; }; /** Props for the `conversation_assigned` template — hand-off notification * to a support agent. */ type ConversationAssignedProps = EmailRegionContext & BrandContext & { recipient_name: string; /** Who routed the conversation (previous assignee or a bot). */ inviter_name: string; account_name: string; account_slug: string; account_avatar?: string; conversation_url: string; /** ISO-8601 timestamp of the latest message; formatted in the recipient's locale. */ last_message_at: string; last_message_text?: string; /** Channel label (e.g. "WhatsApp", "Web"). Rendered verbatim. */ channel: string; user?: ConversationAssignedUser; }; /** Priority bucket for an `AlertItem`. Drives the chip color. */ type AlertPriority = 'high' | 'medium' | 'low'; type AlertItem = { priority: AlertPriority; /** Short, human-readable summary (e.g. "Jailbreak attempt: \"Ignore prior…\""). */ text: string; /** Optional deep link to the offending conversation or review record. */ link?: string; }; /** * Generic alerts email. Works for both real-time single-alert sends * (`alerts.length === 1`, no `unsubscribe_url`) and digest sends * (`alerts.length > 1`, `unsubscribe_url` required by sender policy). */ type AlertsProps = EmailRegionContext & BrandContext & { account_name: string; account_slug: string; /** Pre-formatted period or timestamp string. Caller picks. Examples: * "May 11, 2026 at 14:22", "Week of May 4 – 10", "May 2026". */ period_label?: string; alerts: AlertItem[]; /** CTA URL — typically the alerts/review queue in the console. */ review_url: string; /** Mute-this-digest URL. Optional for one-off real-time sends. */ unsubscribe_url?: string; }; /** Shared metric fields for the weekly and monthly analytics digests. * Each `Statistic` renders as a tile with an optional trend chip. */ type AnalyticsBaseProps = EmailRegionContext & { account_name: string; account_id: string; account_slug: string; /** Count of AI agents that handled at least one conversation in the * period. Used by both weekly and monthly digests. Hidden when omitted * or zero — the header row collapses to a single column. */ active_ai_agents?: number; analytics_url: string; /** Meaningful conversations count — conversations in which the user sent ≥1 * message. Rendered as the headline number in the Activity tile; the share * against `total_conversations` is computed in-template and shown as a * secondary caption. */ meaningful_conversations: Statistic; total_conversations: Statistic; /** Distinct end-users who started a conversation in the period. */ users: Statistic; /** "Insights calculated" tile. Hidden when omitted or value === '0'. */ insights_calculated?: Statistic; /** "Safety signals caught" tile — sum of factual + link hallucinations, * jailbreaks, and out-of-scope deflections. Hidden when omitted or '0'. */ safety_signals_caught?: Statistic; /** "Outbound conversations" tile — proactive (campaign-initiated) * conversations in the period. Hidden when omitted or '0'. */ outbound_conversations?: Statistic; /** "Inbound conversations" tile — user-initiated conversations in the * period. Pairs with `outbound_conversations`; both render together. */ inbound_conversations?: Statistic; /** Busiest AI agent with its CSAT / containment / goal-achieved metrics * plus the brain's top categorical insights. Section hidden when * omitted. */ top_agent?: TopAgent; /** Account-wide hallucination breakdown rendered as a 3-column row * (factual, jailbreak, out_of_scope). Hidden when omitted or all zero. */ safety_breakdown?: SafetyBreakdown; /** "Human team" section: account-wide tiles for live-agent handling * (total convs, first-response time, CSAT, resolution time) plus the * top-5 busiest agents. Hidden when omitted. */ human_team?: HumanTeam; }; /** Mixed into any digest-style template to require an unsubscribe URL. */ type DigestProps = { /** Region-specific URL where the recipient can mute this digest. */ unsubscribe_url: string; }; /** * Props for the `weekly_analytics` template — Monday-morning usage digest. * Range is caller-formatted via `start_date` / `end_date`. */ type WeeklyAnalyticsProps = AnalyticsBaseProps & DigestProps & BrandContext & { /** Period start, pre-formatted (e.g. "May 4"). */ start_date: string; /** Period end, pre-formatted (e.g. "May 10"). */ end_date: string; }; /** Props for the `monthly_analytics` template — same shape as weekly, * monthly cadence; uses a single `period_label` instead of a range. */ type MonthlyAnalyticsProps = AnalyticsBaseProps & DigestProps & BrandContext & { /** Pre-formatted period label, e.g. "May 2026". */ period_label: string; }; /** Return value of `renderEmail`. SendGrid-ready: pass straight to `mail.send`. */ type RenderedEmail = { subject: string; html: string; /** Plain-text body, rendered from the same React tree with `plainText: true`. */ text: string; }; /** * Maps each template `id` to its props shape. Used by `renderEmail` for * type-safe dispatch — passing wrong props for an `id` is a compile error. * * To add a template: drop the .tsx file in `templates/`, register a * `Props` type in `i18n/types.ts`, then add a row here + in `RENDERERS`. */ type TemplateRegistry = { welcome: WelcomeProps; invitation: InvitationProps; conversation_assigned: ConversationAssignedProps; request_access: RequestAccessProps; alerts: AlertsProps; weekly_analytics: WeeklyAnalyticsProps; monthly_analytics: MonthlyAnalyticsProps; }; /** Union of every template identifier the renderer accepts. */ type TemplateId = keyof TemplateRegistry; /** Props shape required for a given `TemplateId`. Enables typed dispatch: * `renderEmail<'welcome'>(...)` infers `PropsByTemplate<'welcome'>`. */ type PropsByTemplate = TemplateRegistry[Id]; /** * All template IDs the renderer accepts, as a runtime array. Mirrors the * `SUPPORTED_LOCALES` / `Locale` pattern so consumers can iterate, validate, * or build a registry without hardcoding the list. Order is not semantic. */ declare const TEMPLATE_IDS: readonly ["welcome", "invitation", "conversation_assigned", "request_access", "alerts", "weekly_analytics", "monthly_analytics"]; /** Type guard for narrowing an unknown string to `TemplateId`. */ declare const isTemplateId: (value: unknown) => value is TemplateId; /** * Render a template into a SendGrid-ready (subject, html, text) triple. * * @example * const { html, text, subject } = await renderEmail( * 'welcome', * { first_name: 'Sam', sender_email: 'support@moveo.ai' }, * 'en' * ); */ declare const renderEmail: (id: Id, props: PropsByTemplate, locale: Locale) => Promise; /** * Shape of the configuration values a sender service (e.g. the orchestrator) * typically derives from env / per-region config and feeds into `renderEmail`. * * The package itself reads zero env vars — `EmailDefaults` is purely a typing * convenience for consumers so they can: * 1. Type their own config loader against a single source of truth. * 2. See, at compile time, which subset of defaults each template actually * consumes (via `DefaultsFor`). * * The orchestrator's `SENDGRID_DEFAULTS` JSON blob and region env vars * (`TOOLING_URL`, `API_PUBLIC_URL`) populate these fields. Every field is * optional here — required fields live on the individual template prop types, * the consumer is responsible for guaranteeing the right slice is present * when calling `renderEmail`. */ type EmailDefaults = { /** Default `From:` address — `SENDGRID_DEFAULTS.sender_email` in orchestrator. */ sender_email?: string; /** Region console URL — `TOOLING_URL` in orchestrator. Used to build per-call CTA URLs. */ console_url?: string; /** Region public API URL — `API_PUBLIC_URL` in orchestrator. Reserved; no template consumes it today. */ api_url?: string; /** Onboarding-call URL shown on the Welcome template — `SENDGRID_DEFAULTS.meeting_url`. */ meeting_url?: string; }; /** * Per-template subset of `EmailDefaults` that the renderer actually reads. * * Use this to slice your config-loading code so each call site only sees the * keys it cares about. Example: * * const welcomeDefaults: DefaultsFor<'welcome'> = pickDefaults(env); * await renderEmail('welcome', { ...welcomeDefaults, first_name }, 'en'); * * Fields here are still optional — required-vs-optional is enforced by the * template's own prop type (e.g. `WelcomeProps.sender_email` is required). */ type TemplateDefaults = { welcome: Pick; invitation: Pick; conversation_assigned: Pick; request_access: Pick; alerts: Pick; weekly_analytics: Pick; monthly_analytics: Pick; }; type DefaultsFor = TemplateDefaults[Id]; export { type AlertItem, type AlertPriority, type AlertsProps, type AnalyticsBaseProps, type BrandContext, type ConversationAssignedProps, type ConversationAssignedUser, DEFAULT_LOCALE, type DefaultsFor, type EmailDefaults, type EmailRegionContext, type InvitationProps, type Locale, type MonthlyAnalyticsProps, type PropsByTemplate, type RenderedEmail, type RequestAccessProps, SUPPORTED_LOCALES, type Statistic, TEMPLATE_IDS, type TemplateDefaults, type TemplateId, type WeeklyAnalyticsProps, type WelcomeProps, isLocale, isTemplateId, renderEmail };