Spaces:
Running
Running
| import { PATH_NODES, getPathNode } from '../data/advisorPaths'; | |
| const STORAGE_PREFIX = 'advisor-path-progress::'; | |
| export function pathProgressKey(userId) { | |
| const uid = userId || 'anon'; | |
| return `${STORAGE_PREFIX}${uid}`; | |
| } | |
| /** @returns {{ completed: string[], activeId: string|null, updatedAt: number }} */ | |
| export function loadPathProgress(userId) { | |
| try { | |
| const raw = localStorage.getItem(pathProgressKey(userId)); | |
| if (!raw) return { completed: [], activeId: null, updatedAt: 0 }; | |
| const parsed = JSON.parse(raw); | |
| const completed = Array.isArray(parsed.completed) | |
| ? parsed.completed.filter((id) => getPathNode(id)) | |
| : []; | |
| const activeId = parsed.activeId && getPathNode(parsed.activeId) ? parsed.activeId : null; | |
| return { | |
| completed, | |
| activeId, | |
| updatedAt: typeof parsed.updatedAt === 'number' ? parsed.updatedAt : 0, | |
| }; | |
| } catch { | |
| return { completed: [], activeId: null, updatedAt: 0 }; | |
| } | |
| } | |
| export function savePathProgress(userId, state) { | |
| try { | |
| localStorage.setItem( | |
| pathProgressKey(userId), | |
| JSON.stringify({ | |
| completed: state.completed || [], | |
| activeId: state.activeId || null, | |
| updatedAt: Date.now(), | |
| }), | |
| ); | |
| } catch { | |
| /* quota / private mode */ | |
| } | |
| } | |
| function prerequisitesMet(node, completedSet) { | |
| if (node.requires?.length) { | |
| if (!node.requires.every((id) => completedSet.has(id))) return false; | |
| } | |
| if (node.requiresAny?.length) { | |
| if (!node.requiresAny.some((id) => completedSet.has(id))) return false; | |
| } | |
| return true; | |
| } | |
| export function isNodeVisible(node, { completedSet, isGuest }) { | |
| if (node.guestOnly && !isGuest) return false; | |
| if (node.signedInOnly && isGuest) return false; | |
| if (completedSet.has(node.id)) return false; | |
| return prerequisitesMet(node, completedSet); | |
| } | |
| /** | |
| * Next steps to show (max `limit`), preferring lower tier then definition order. | |
| */ | |
| export function getAvailableSteps({ completed = [], isGuest = false, limit = 4 } = {}) { | |
| const completedSet = new Set(completed); | |
| return PATH_NODES.filter((node) => isNodeVisible(node, { completedSet, isGuest })) | |
| .sort((a, b) => a.tier - b.tier || PATH_NODES.indexOf(a) - PATH_NODES.indexOf(b)) | |
| .slice(0, limit); | |
| } | |
| /** Highest tier among completed nodes, or 0 if none. */ | |
| export function currentTier(completed = []) { | |
| let max = 0; | |
| completed.forEach((id) => { | |
| const node = getPathNode(id); | |
| if (node && node.tier > max) max = node.tier; | |
| }); | |
| return max; | |
| } | |
| /** Most recently completed node id for the “current focus” strip. */ | |
| export function latestCompleted(completed = []) { | |
| if (!completed.length) return null; | |
| return completed[completed.length - 1]; | |
| } | |
| export function markStepComplete(completed, stepId) { | |
| if (!getPathNode(stepId) || completed.includes(stepId)) return completed; | |
| return [...completed, stepId]; | |
| } | |
| /** Auto-detect completions from app state (does not remove manual completions). */ | |
| export function mergeAutoCompletions(completed, signals = {}) { | |
| const next = new Set(completed); | |
| const { | |
| hasChats, | |
| hasProfileFacts, | |
| hasStatedGoal, | |
| visitedJourney, | |
| visitedWorkspace, | |
| } = signals; | |
| if (hasChats) next.add('first-question'); | |
| if (hasProfileFacts) next.add('your-profile'); | |
| if (hasStatedGoal) next.add('stated-goal'); | |
| if (visitedJourney) next.add('your-journey'); | |
| if (visitedWorkspace) next.add('your-workspace'); | |
| return PATH_NODES.map((n) => n.id).filter((id) => next.has(id)); | |
| } | |
| export function recordVisit(userId, area) { | |
| const key = `${STORAGE_PREFIX}visits::${userId || 'anon'}`; | |
| try { | |
| const raw = localStorage.getItem(key); | |
| const visits = raw ? JSON.parse(raw) : {}; | |
| visits[area] = true; | |
| localStorage.setItem(key, JSON.stringify(visits)); | |
| } catch { | |
| /* ignore */ | |
| } | |
| } | |
| export function loadVisits(userId) { | |
| try { | |
| const raw = localStorage.getItem(`${STORAGE_PREFIX}visits::${userId || 'anon'}`); | |
| return raw ? JSON.parse(raw) : {}; | |
| } catch { | |
| return {}; | |
| } | |
| } | |