import type { MessageDescriptor } from "@lingui/core"; import { msg, t } from "@lingui/core/macro"; import type { MessageBlock, OnboardingStep, ThreadMessage } from "@rakazo/contracts"; import { i18n } from "./i18n"; /** * The onboarding cards in the user's language. The server stores each card with its step and * option ids plus English text; the web says what it knows from the ids and keeps the server * text for anything new (mobile and agent history always use the server text). */ type ChoiceBlock = Extract; type IntakeNote = Extract; const QUESTIONS: Record = { focus: msg`What do you want me on first?`, goal: msg`What’s one thing you want to get done in the next few weeks?`, rhythm: msg`When should I check in with you?`, quiet_hours: msg`When should I stay quiet?`, notifications: msg`Can I send you notifications?`, }; const SKIP = msg`Skip`; const NOT_NOW = msg`Not now`; // Quiet-hour ranges are left out: the server already writes them in the user's locale. const OPTIONS: Record> = { focus: { day: msg`Day-to-day work`, inbox: msg`Inbox & email`, research: msg`Research & writing`, crew: msg`Specialist crew`, skip: SKIP, }, goal: { skip: SKIP }, rhythm: { morning: msg`Every morning`, morning_evening: msg`Morning and evening`, weekly: msg`Once a week`, none: NOT_NOW, }, quiet_hours: { skip: SKIP }, notifications: { on: msg`Turn on`, later: NOT_NOW }, }; const PLACEHOLDERS: Partial> = { goal: msg`Your goal`, }; /** Another bot's focus card carries no step; its option ids give it away. */ function cardStep(card: ChoiceBlock): OnboardingStep | undefined { if (card.step) return card.step; const focus = OPTIONS.focus; return card.options.length > 0 && card.options.every((option) => focus[option.id]) ? "focus" : undefined; } /** The card's question, options and placeholder in the user's language. */ export function localizedChoice(card: ChoiceBlock): ChoiceBlock { const step = cardStep(card); if (!step) return card; const options = OPTIONS[step]; const placeholder = PLACEHOLDERS[step]; return { ...card, question: i18n._(QUESTIONS[step]), options: card.options.map((option) => { const label = options[option.id]; return label ? { ...option, label: i18n._(label) } : option; }), ...(card.textInput && placeholder ? { textInput: { placeholder: i18n._(placeholder) } } : {}), }; } function sameDay(left: Date, right: Date): boolean { return left.toDateString() === right.toDateString(); } /** The closing note in the user's language, with the first check-in in their time zone. */ export function intakeNoteText(note: IntakeNote, now = new Date()): string { if (note.note !== "done") return note.text; if (!note.nextCheckInAt) return t`All set. I’m here whenever you need me.`; const at = new Date(note.nextCheckInAt); if (!Number.isFinite(at.getTime())) return note.text; const time = new Intl.DateTimeFormat(i18n.locale || "en", { hour: "numeric", minute: "2-digit", }).format(at); const tomorrow = new Date(now); tomorrow.setDate(now.getDate() + 1); if (sameDay(at, now)) return t`All set. I’ll check in today at ${time}.`; if (sameDay(at, tomorrow)) return t`All set. I’ll check in tomorrow at ${time}.`; const weekday = new Intl.DateTimeFormat(i18n.locale || "en", { weekday: "long" }).format(at); return t`All set. I’ll check in on ${weekday} at ${time}.`; } /** The message with onboarding notes as plain text, so bubbles, copy and speech read the same. */ export function withIntakeNoteText(message: ThreadMessage): ThreadMessage { if (!message.blocks.some((block) => block.kind === "intake_note")) return message; return { ...message, blocks: message.blocks.map((block) => block.kind === "intake_note" ? { kind: "text", text: intakeNoteText(block) } : block, ), }; }