File size: 4,146 Bytes
ad7840f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
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 {};
  }
}