learning_ai_common_plat/packages/gentle-notifications/src/client.ts
saravanakumardb1 be03efa111 feat(shared-packages): add 9 @bytelyst/* client packages with 100% API coverage
Packages added:
- @bytelyst/referral-client — referral API client + share helpers
- @bytelyst/subscription-client — subscription/plan API client + cache
- @bytelyst/celebrations — milestone triggers, confetti, positive messages
- @bytelyst/gentle-notifications — ND-friendly messaging, forbidden phrases
- @bytelyst/accessibility — VoiceOver/TalkBack label generators
- @bytelyst/quick-actions — progressive disclosure, smart defaults
- @bytelyst/time-references — familiar duration references
- @bytelyst/org-client — org/workspace/membership/license API client
- @bytelyst/marketplace-client — listing/review/install API client

All packages: pure TS, ESM, globalThis.fetch, no Node.js deps.
99 Vitest tests across 9 packages, 79/79 public methods covered.

Review fixes applied:
- time-references: fix module-level mutable state leak + add clearCustomReferences()
- accessibility: fix parameter reassignment in formatDurationForA11y/numberToWords
- subscription-client: fix flaky daysRemaining test (ms boundary race)
2026-03-19 13:10:09 -07:00

158 lines
4.3 KiB
TypeScript

/**
* Neurodivergent-friendly notification messaging system.
*
* Encouraging tone, adaptive frequency, forbidden phrases.
* Pure client-side TS — no backend dependency.
*/
import type { GentleMessage, GentleNotificationConfig, GentleNotificationEngine } from './types.js';
export const FORBIDDEN_PHRASES: readonly string[] = [
"You haven't",
'You forgot',
"Don't forget",
'You should have',
"Why didn't you",
'You missed',
'You failed',
'You need to',
] as const;
const DEFAULT_MESSAGES: Record<string, GentleMessage[]> = {
reminder: [
{
title: 'Gentle Reminder',
body: 'Whenever you are ready, there is something waiting for you.',
tone: 'encouraging',
},
{
title: 'Quick Note',
body: 'No rush — just a friendly nudge when the time feels right.',
tone: 'encouraging',
},
{
title: 'Hey there',
body: 'Take your time. We will be here when you are ready.',
tone: 'neutral',
},
],
progress: [
{
title: 'Nice Progress',
body: 'Look at what you have accomplished — every step matters!',
tone: 'encouraging',
},
{
title: 'Moving Forward',
body: 'You are making progress at your own pace. That is perfect.',
tone: 'encouraging',
},
],
check_in: [
{
title: 'Check In',
body: 'How are you feeling? Remember, there is no wrong answer.',
tone: 'encouraging',
},
{ title: 'Quick Check', body: 'Just checking in — hope you are doing well!', tone: 'neutral' },
],
streak: [
{
title: 'Streak Update',
body: 'Your consistency is impressive — keep it going if it feels right!',
tone: 'encouraging',
},
],
idle: [
{
title: 'Welcome Back',
body: 'Great to see you again — no judgment, just glad you are here!',
tone: 'encouraging',
},
{
title: 'Hi Again',
body: 'Whenever you are ready to jump back in, we are here.',
tone: 'neutral',
},
],
};
export function createGentleNotificationEngine(
initialConfig?: Partial<GentleNotificationConfig>
): GentleNotificationEngine {
const messagePools: Record<string, GentleMessage[]> = { ...DEFAULT_MESSAGES };
function getDefaultConfig(): GentleNotificationConfig {
return {
maxPerHour: 3,
tone: 'encouraging',
adaptiveFrequency: true,
dismissCount: 0,
suppressThreshold: 5,
...initialConfig,
};
}
function getMessage(type: string, config?: GentleNotificationConfig): GentleMessage {
const tone = config?.tone ?? 'encouraging';
const pool = messagePools[type];
if (!pool || pool.length === 0) {
return {
title: 'Hey',
body: 'Hope you are having a good day!',
tone,
};
}
// Filter by tone if possible, fallback to any
const toneFiltered = pool.filter(m => m.tone === tone);
const candidates = toneFiltered.length > 0 ? toneFiltered : pool;
const index = Math.floor(Math.random() * candidates.length);
return candidates[index];
}
function shouldSuppress(config: GentleNotificationConfig): boolean {
if (config.adaptiveFrequency && config.dismissCount >= config.suppressThreshold) {
return true;
}
return false;
}
function recordDismissal(config: GentleNotificationConfig): GentleNotificationConfig {
const newConfig = { ...config, dismissCount: config.dismissCount + 1 };
if (newConfig.adaptiveFrequency && newConfig.dismissCount >= newConfig.suppressThreshold) {
newConfig.maxPerHour = Math.max(1, Math.floor(newConfig.maxPerHour / 2));
}
return newConfig;
}
function resetDismissals(config: GentleNotificationConfig): GentleNotificationConfig {
return { ...config, dismissCount: 0 };
}
function registerMessages(type: string, messages: GentleMessage[]): void {
messagePools[type] = [...(messagePools[type] ?? []), ...messages];
}
function getForbiddenPhrases(): readonly string[] {
return FORBIDDEN_PHRASES;
}
function containsForbiddenPhrase(text: string): boolean {
const lower = text.toLowerCase();
return FORBIDDEN_PHRASES.some(phrase => lower.includes(phrase.toLowerCase()));
}
return {
getDefaultConfig,
getMessage,
shouldSuppress,
recordDismissal,
resetDismissals,
registerMessages,
getForbiddenPhrases,
containsForbiddenPhrase,
};
}