| 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<string, unknown>; |
| }; |
|
|
| 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<string, CustomerMemoryState>(); |
|
|
| 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 { |
| |
| } |
| } |
|
|
| 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>): 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<ResumeWorkCard, "updatedAt"> & { 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<string, unknown>, 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<string, unknown>, 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(); |
| } |
|
|