export type SmartCustomerProfile = { companyName?: string; contactName?: string; email?: string; phone?: string; street?: string; suite?: string; city?: string; state?: string; zip?: string; updatedAt?: string; }; export type ResumeWorkCard = { id: string; title: string; subtitle?: string; source: "rapid_router" | "pots_estimator" | "knowledgebase"; actionCommand: string; updatedAt: string; }; type CarryoverPayload = { summary?: string; updatedAt: string; payload: Record; }; type CustomerMemoryState = { profile?: SmartCustomerProfile; cards?: ResumeWorkCard[]; carryover?: { potsEstimator?: CarryoverPayload; rapidRouterDraft?: CarryoverPayload; }; }; const CUSTOMER_MEMORY_NAMESPACE = "masters_toolkit_customer_memory_v2"; const CUSTOMER_MEMORY_SCOPE_FALLBACK = "anonymous"; const MAX_CARD_COUNT = 8; const MAX_TEXT_LEN = 320; let activeCustomerMemoryScope = CUSTOMER_MEMORY_SCOPE_FALLBACK; const volatileStateByScope = new Map(); function cleanText(value: unknown): string { return String(value || "") .replace(/\s+/g, " ") .trim() .slice(0, MAX_TEXT_LEN); } function cleanStateCode(value: unknown): string { return cleanText(value).toUpperCase().slice(0, 2); } function cleanZip(value: unknown): string { return String(value || "").replace(/[^0-9]/g, "").slice(0, 5); } function nowIso(): string { return new Date().toISOString(); } function normalizeScope(value: unknown): string { const raw = String(value || "").trim(); if (!raw) return CUSTOMER_MEMORY_SCOPE_FALLBACK; return raw.includes("@") ? raw.toLowerCase() : raw; } function getScopedStorageKey(): string { return `${CUSTOMER_MEMORY_NAMESPACE}:${activeCustomerMemoryScope}`; } export function setCustomerMemoryScope(scope: unknown): string { activeCustomerMemoryScope = normalizeScope(scope); return activeCustomerMemoryScope; } export function getCustomerMemoryScope(): string { return activeCustomerMemoryScope; } function getScopedVolatileState(): CustomerMemoryState { return volatileStateByScope.get(activeCustomerMemoryScope) || {}; } function setScopedVolatileState(next: CustomerMemoryState): void { volatileStateByScope.set(activeCustomerMemoryScope, next); } function readScopedState(): CustomerMemoryState { const persistent = readPersistentRaw(); const volatile = getScopedVolatileState(); return { ...persistent, ...volatile, profile: volatile.profile || persistent.profile, carryover: { ...(persistent.carryover || {}), ...(volatile.carryover || {}), }, cards: Array.isArray(volatile.cards) ? volatile.cards : persistent.cards, }; } function readPersistentRaw(): CustomerMemoryState { if (typeof window === "undefined") return {}; try { const raw = window.localStorage.getItem(getScopedStorageKey()); if (!raw) return {}; const parsed = JSON.parse(raw) as CustomerMemoryState; return parsed && typeof parsed === "object" ? parsed : {}; } catch { return {}; } } function writePersistentRaw(next: CustomerMemoryState): void { if (typeof window === "undefined") return; try { window.localStorage.setItem(getScopedStorageKey(), JSON.stringify(next)); } catch { // ignore storage failures } } function writeScopedState(next: CustomerMemoryState): void { setScopedVolatileState(next); writePersistentRaw(next); } export function getSmartProfile(): SmartCustomerProfile | null { const raw = readScopedState().profile; if (!raw || typeof raw !== "object") return null; const normalized: SmartCustomerProfile = { companyName: cleanText(raw.companyName), contactName: cleanText(raw.contactName), email: cleanText(raw.email), phone: cleanText(raw.phone), street: cleanText(raw.street), suite: cleanText(raw.suite), city: cleanText(raw.city), state: cleanStateCode(raw.state), zip: cleanZip(raw.zip), updatedAt: cleanText(raw.updatedAt || ""), }; const hasAny = Object.entries(normalized).some(([k, v]) => k !== "updatedAt" && Boolean(v)); return hasAny ? normalized : null; } export function mergeSmartProfile(patch: Partial): SmartCustomerProfile { const existing = getSmartProfile() || {}; const merged: SmartCustomerProfile = { companyName: cleanText(patch.companyName || existing.companyName), contactName: cleanText(patch.contactName || existing.contactName), email: cleanText(patch.email || existing.email), phone: cleanText(patch.phone || existing.phone), street: cleanText(patch.street || existing.street), suite: cleanText(patch.suite || existing.suite), city: cleanText(patch.city || existing.city), state: cleanStateCode(patch.state || existing.state), zip: cleanZip(patch.zip || existing.zip), updatedAt: nowIso(), }; const state = readScopedState(); writeScopedState({ ...state, profile: merged }); return merged; } export function listResumeWorkCards(): ResumeWorkCard[] { const cards = readScopedState().cards; if (!Array.isArray(cards)) return []; return cards .filter((card) => card && typeof card === "object") .map((card) => ({ id: cleanText(card.id), title: cleanText(card.title), subtitle: cleanText(card.subtitle || ""), source: card.source === "rapid_router" || card.source === "pots_estimator" || card.source === "knowledgebase" ? card.source : "knowledgebase", actionCommand: cleanText(card.actionCommand), updatedAt: cleanText(card.updatedAt || ""), })) .filter((card) => card.id && card.title && card.actionCommand) .sort((a, b) => String(b.updatedAt || "").localeCompare(String(a.updatedAt || ""))); } export function upsertResumeWorkCard( card: Omit & { updatedAt?: string } ): ResumeWorkCard[] { const safeCard: ResumeWorkCard = { id: cleanText(card.id), title: cleanText(card.title), subtitle: cleanText(card.subtitle || ""), source: card.source, actionCommand: cleanText(card.actionCommand), updatedAt: cleanText(card.updatedAt || nowIso()), }; if (!safeCard.id || !safeCard.title || !safeCard.actionCommand) { return listResumeWorkCards(); } const current = listResumeWorkCards().filter((item) => item.id !== safeCard.id); const next = [safeCard, ...current].slice(0, MAX_CARD_COUNT); const state = readScopedState(); writeScopedState({ ...state, cards: next }); return next; } export function setPotsEstimatorCarryover(payload: Record, summary = ""): void { const state = readScopedState(); writeScopedState({ ...state, carryover: { ...(state.carryover || {}), potsEstimator: { payload, summary: cleanText(summary), updatedAt: nowIso(), }, }, }); } export function getPotsEstimatorCarryover(): CarryoverPayload | null { const carry = readScopedState().carryover?.potsEstimator; if (!carry || typeof carry !== "object" || !carry.payload || typeof carry.payload !== "object") return null; return { payload: carry.payload, summary: cleanText(carry.summary || ""), updatedAt: cleanText(carry.updatedAt || ""), }; } export function setRapidRouterDraftCarryover(payload: Record, summary = ""): void { const state = readScopedState(); writeScopedState({ ...state, carryover: { ...(state.carryover || {}), rapidRouterDraft: { payload, summary: cleanText(summary), updatedAt: nowIso(), }, }, }); } export function getRapidRouterDraftCarryover(): CarryoverPayload | null { const carry = readScopedState().carryover?.rapidRouterDraft; if (!carry || typeof carry !== "object" || !carry.payload || typeof carry.payload !== "object") return null; return { payload: carry.payload, summary: cleanText(carry.summary || ""), updatedAt: cleanText(carry.updatedAt || ""), }; } export function __resetCustomerMemoryForTests(): void { volatileStateByScope.clear(); }