Kennedy Johnson Cursor commited on
Commit
371ef22
·
1 Parent(s): 4ae79ba

Remove guest Foods/Supplements/Exercise/Healthcare track popup.

Browse files

Heidi’s panel is food-only, so first-run guests go straight to chat.

Co-authored-by: Cursor <cursoragent@cursor.com>

phd-advisor-frontend/src/components/SearchPathGate.css DELETED
@@ -1,129 +0,0 @@
1
- .search-path-backdrop {
2
- position: fixed;
3
- inset: 0;
4
- z-index: 1200;
5
- background: rgba(15, 23, 42, 0.55);
6
- display: flex;
7
- align-items: center;
8
- justify-content: center;
9
- padding: 24px 16px;
10
- backdrop-filter: blur(4px);
11
- }
12
-
13
- .search-path-card {
14
- width: 100%;
15
- max-width: 560px;
16
- background: var(--bg-primary, #fff);
17
- border: 1px solid var(--border-primary, #e2e8f0);
18
- border-radius: 20px;
19
- padding: 28px 24px 22px;
20
- box-shadow: var(--shadow-xl, 0 20px 40px rgba(0, 0, 0, 0.18));
21
- }
22
-
23
- .search-path-card h2 {
24
- margin: 0 0 8px;
25
- font-size: 1.35rem;
26
- color: var(--text-primary, #0f172a);
27
- }
28
-
29
- .search-path-sub {
30
- margin: 0 0 20px;
31
- color: var(--text-secondary, #475569);
32
- line-height: 1.5;
33
- font-size: 0.95rem;
34
- }
35
-
36
- .search-path-options {
37
- display: grid;
38
- grid-template-columns: 1fr 1fr;
39
- gap: 12px;
40
- margin-bottom: 16px;
41
- }
42
-
43
- @media (max-width: 560px) {
44
- .search-path-options {
45
- grid-template-columns: 1fr;
46
- }
47
- }
48
-
49
- .search-path-option {
50
- text-align: left;
51
- display: flex;
52
- flex-direction: column;
53
- gap: 6px;
54
- padding: 16px;
55
- border-radius: 14px;
56
- border: 2px solid var(--border-primary, #e2e8f0);
57
- background: var(--bg-secondary, #f8fafc);
58
- cursor: pointer;
59
- transition: border-color 0.15s, background 0.15s, transform 0.15s;
60
- }
61
-
62
- .search-path-option:hover {
63
- border-color: var(--accent-primary, #0f766e);
64
- transform: translateY(-1px);
65
- }
66
-
67
- .search-path-option.active {
68
- border-color: var(--accent-primary, #0f766e);
69
- background: var(--accent-soft, #ccfbf1);
70
- }
71
-
72
- .search-path-option-icon {
73
- color: var(--accent-primary, #0f766e);
74
- }
75
-
76
- .search-path-option-title {
77
- font-weight: 700;
78
- font-size: 1.05rem;
79
- color: var(--text-primary, #0f172a);
80
- }
81
-
82
- .search-path-option-desc {
83
- font-size: 0.85rem;
84
- line-height: 1.4;
85
- color: var(--text-secondary, #475569);
86
- }
87
-
88
- .search-path-error {
89
- color: #b91c1c;
90
- font-size: 0.875rem;
91
- margin-bottom: 10px;
92
- }
93
-
94
- .search-path-continue {
95
- width: 100%;
96
- display: inline-flex;
97
- align-items: center;
98
- justify-content: center;
99
- gap: 8px;
100
- border: none;
101
- border-radius: 12px;
102
- padding: 12px 16px;
103
- background: var(--accent-primary, #0f766e);
104
- color: #fff;
105
- font-weight: 600;
106
- cursor: pointer;
107
- }
108
-
109
- .search-path-continue:disabled {
110
- opacity: 0.55;
111
- cursor: not-allowed;
112
- }
113
-
114
- .search-path-skip {
115
- width: 100%;
116
- margin-top: 10px;
117
- border: none;
118
- background: transparent;
119
- color: var(--text-secondary, #64748b);
120
- font-size: 0.9rem;
121
- font-weight: 500;
122
- cursor: pointer;
123
- padding: 8px;
124
- }
125
-
126
- .search-path-skip:hover {
127
- color: var(--text-primary, #0f172a);
128
- text-decoration: underline;
129
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
phd-advisor-frontend/src/components/SearchPathGate.js CHANGED
@@ -1,156 +1,32 @@
1
- import React, { useState } from 'react';
2
- import { Leaf, Pill, Dumbbell, HeartPulse, ArrowRight } from 'lucide-react';
3
- import { writeScopedItem, readScopedItem, SEARCH_PATH_KEY } from '../utils/canvasStorage';
4
- import './SearchPathGate.css';
5
 
6
  export const SEARCH_PATH_STORAGE_KEY = SEARCH_PATH_KEY;
 
7
 
 
8
  export const SEARCH_PATH_OPTIONS = [
9
  {
10
- value: 'Foods',
11
  title: 'Foods',
12
  description: 'Clean eating, produce, recipes, and everyday real-food habits.',
13
  icon: Leaf,
14
  color: '#059669',
15
  bg: '#ECFDF5',
16
  },
17
- {
18
- value: 'Supplements',
19
- title: 'Supplements',
20
- description: 'Superfoods, enzymes, and concentrated nutrition — with careful guidance.',
21
- icon: Pill,
22
- color: '#D97706',
23
- bg: '#FFFBEB',
24
- },
25
- {
26
- value: 'Exercise',
27
- title: 'Exercise',
28
- description: 'Fueling movement with whole foods, snacks, and recovery-friendly meals.',
29
- icon: Dumbbell,
30
- color: '#2563EB',
31
- bg: '#EFF6FF',
32
- },
33
- {
34
- value: 'Healthcare',
35
- title: 'Healthcare',
36
- description: 'Food-first wellness questions to discuss alongside your own clinicians.',
37
- icon: HeartPulse,
38
- color: '#E11D48',
39
- bg: '#FFF1F2',
40
- },
41
  ];
42
 
43
- const KNOWN_PATHS = new Set([
44
- ...SEARCH_PATH_OPTIONS.map((o) => o.value.toLowerCase()),
45
- 'clean eating basics',
46
- 'more fruits & vegetables',
47
- 'more fruits and vegetables',
48
- ]);
49
 
50
  /**
51
- * First-run gate: healthy-eating focus steers advisor trajectory.
52
- * Shown once until a known focus is saved (profile or localStorage).
53
  */
54
- const SearchPathGate = ({ authToken, userId, onComplete }) => {
55
- const [selected, setSelected] = useState(null);
56
- const [saving, setSaving] = useState(false);
57
- const [error, setError] = useState('');
58
-
59
- const handleContinue = async () => {
60
- if (!selected) {
61
- setError('Pick a focus to continue.');
62
- return;
63
- }
64
- setSaving(true);
65
- setError('');
66
- try {
67
- const resp = await fetch(`${process.env.REACT_APP_API_URL}/api/users/me/profile`, {
68
- method: 'PUT',
69
- headers: {
70
- Authorization: `Bearer ${authToken}`,
71
- 'Content-Type': 'application/json',
72
- },
73
- body: JSON.stringify({ cyber_role: selected }),
74
- });
75
- if (!resp.ok) {
76
- const data = await resp.json().catch(() => ({}));
77
- throw new Error(data.detail || 'Could not save your choice');
78
- }
79
- const profile = await resp.json();
80
- writeScopedItem(SEARCH_PATH_KEY, userId, selected);
81
- onComplete?.(profile, selected);
82
- } catch (e) {
83
- setError(e.message || 'Could not save your choice');
84
- } finally {
85
- setSaving(false);
86
- }
87
- };
88
-
89
- return (
90
- <div className="search-path-backdrop" role="dialog" aria-modal="true" aria-labelledby="search-path-title">
91
- <div className="search-path-card">
92
- <h2 id="search-path-title">What would you like to focus on right now?</h2>
93
- <p className="search-path-sub">
94
- Pick one track to start. You can always ask about anything in chat — this just helps the advisors greet you in the right lane.
95
- </p>
96
- <div className="search-path-options">
97
- {SEARCH_PATH_OPTIONS.map((opt) => {
98
- const Icon = opt.icon;
99
- const active = selected === opt.value;
100
- return (
101
- <button
102
- key={opt.value}
103
- type="button"
104
- className={`search-path-option ${active ? 'active' : ''}`}
105
- onClick={() => { setSelected(opt.value); setError(''); }}
106
- disabled={saving}
107
- style={active ? { borderColor: opt.color, background: opt.bg } : undefined}
108
- >
109
- <Icon className="search-path-option-icon" size={22} style={{ color: opt.color }} />
110
- <span className="search-path-option-title">{opt.title}</span>
111
- <span className="search-path-option-desc">{opt.description}</span>
112
- </button>
113
- );
114
- })}
115
- </div>
116
- {error && <div className="search-path-error">{error}</div>}
117
- <button
118
- type="button"
119
- className="search-path-continue"
120
- onClick={handleContinue}
121
- disabled={saving || !selected}
122
- >
123
- {saving ? 'Saving…' : 'Continue to advisors'}
124
- {!saving && <ArrowRight size={16} />}
125
- </button>
126
- <button
127
- type="button"
128
- className="search-path-skip"
129
- onClick={() => {
130
- writeScopedItem(SEARCH_PATH_KEY, userId, 'Foods');
131
- onComplete?.(null, 'Foods');
132
- }}
133
- disabled={saving}
134
- >
135
- Skip for now
136
- </button>
137
- </div>
138
- </div>
139
- );
140
- };
141
-
142
- export function needsSearchPath(profile, userId) {
143
- try {
144
- const stored = readScopedItem(SEARCH_PATH_KEY, userId);
145
- if (stored && KNOWN_PATHS.has(String(stored).toLowerCase())) {
146
- return false;
147
- }
148
- } catch {
149
- /* ignore */
150
- }
151
- const role = profile?.cyber_role;
152
- if (!role) return true;
153
- return !KNOWN_PATHS.has(String(role).toLowerCase());
154
  }
155
 
 
 
156
  export default SearchPathGate;
 
1
+ import { Leaf } from 'lucide-react';
2
+ import { writeScopedItem, SEARCH_PATH_KEY } from '../utils/canvasStorage';
 
 
3
 
4
  export const SEARCH_PATH_STORAGE_KEY = SEARCH_PATH_KEY;
5
+ export const FOOD_TRACK = 'Foods';
6
 
7
+ /** Food is the only track. Non-food options (Supplements, Exercise, Healthcare) are gone. */
8
  export const SEARCH_PATH_OPTIONS = [
9
  {
10
+ value: FOOD_TRACK,
11
  title: 'Foods',
12
  description: 'Clean eating, produce, recipes, and everyday real-food habits.',
13
  icon: Leaf,
14
  color: '#059669',
15
  bg: '#ECFDF5',
16
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  ];
18
 
19
+ export function persistFoodTrack(userId) {
20
+ if (userId) writeScopedItem(SEARCH_PATH_KEY, userId, FOOD_TRACK);
21
+ }
 
 
 
22
 
23
  /**
24
+ * Never show the first-run track picker. Heidi’s panel is food-only.
 
25
  */
26
+ export function needsSearchPath(_profile, _userId) {
27
+ return false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  }
29
 
30
+ const SearchPathGate = () => null;
31
+
32
  export default SearchPathGate;
phd-advisor-frontend/src/components/SearchPathGate.test.js ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { needsSearchPath, persistFoodTrack, FOOD_TRACK, SEARCH_PATH_OPTIONS } from './SearchPathGate';
2
+ import { SEARCH_PATH_KEY, readScopedItem } from '../utils/canvasStorage';
3
+
4
+ beforeEach(() => {
5
+ localStorage.clear();
6
+ });
7
+
8
+ test('food is the only track option', () => {
9
+ expect(SEARCH_PATH_OPTIONS.map((o) => o.value)).toEqual([FOOD_TRACK]);
10
+ });
11
+
12
+ test('the guest track popup never shows', () => {
13
+ expect(needsSearchPath(null, 'guest-1')).toBe(false);
14
+ expect(needsSearchPath({ cyber_role: 'Supplements' }, 'guest-1')).toBe(false);
15
+ expect(needsSearchPath({ cyber_role: 'Healthcare' }, 'user-a')).toBe(false);
16
+ });
17
+
18
+ test('persistFoodTrack stores Foods for the user', () => {
19
+ persistFoodTrack('guest-1');
20
+ expect(readScopedItem(SEARCH_PATH_KEY, 'guest-1')).toBe(FOOD_TRACK);
21
+ });
phd-advisor-frontend/src/pages/ChatPage.js CHANGED
@@ -15,7 +15,7 @@ import '../styles/EnhancedChatInput.css';
15
  import AdvisorCarousel from '../components/AdvisorCarousel';
16
  import OnboardingChat from '../components/OnboardingChat';
17
  import ProfileWalkthrough from '../components/ProfileWalkthrough';
18
- import SearchPathGate, { needsSearchPath } from '../components/SearchPathGate';
19
  import ClearDataModal from '../components/ClearDataModal';
20
  import AccountModal from '../components/AccountModal';
21
  import {
@@ -75,11 +75,10 @@ const ChatPage = ({ user, authToken, onNavigateToHome, onNavigateToCanvas, onSig
75
  const [showClearData, setShowClearData] = useState(false);
76
  const [showAccount, setShowAccount] = useState(false);
77
  const [userProfile, setUserProfile] = useState(null);
78
- const [profileLoaded, setProfileLoaded] = useState(false);
79
- const [showSearchPathGate, setShowSearchPathGate] = useState(false);
80
  const [synthesizedMode, setSynthesizedMode] = useState(false);
81
 
82
  const loadProfile = async () => {
 
83
  try {
84
  const resp = await fetch(`${process.env.REACT_APP_API_URL}/api/users/me/profile`, {
85
  headers: { Authorization: `Bearer ${authToken}` },
@@ -87,21 +86,14 @@ const ChatPage = ({ user, authToken, onNavigateToHome, onNavigateToCanvas, onSig
87
  if (resp.ok) {
88
  const profile = await resp.json();
89
  setUserProfile(profile);
90
- // Only show the focus gate once — needsSearchPath also honors localStorage
91
- setShowSearchPathGate(needsSearchPath(profile, userId));
92
- } else {
93
- setShowSearchPathGate(needsSearchPath(null, userId));
94
  }
95
  } catch (e) {
96
- setShowSearchPathGate(needsSearchPath(null, userId));
97
- } finally {
98
- setProfileLoaded(true);
99
  }
100
  };
101
 
102
  useEffect(() => {
103
  if (authToken) {
104
- setProfileLoaded(false);
105
  loadProfile();
106
  }
107
  }, [authToken]);
@@ -889,7 +881,6 @@ const handleNewChat = async (sessionId = null) => {
889
  }, [messages]);
890
 
891
  const handleInputSubmit = async (inputMessage) => {
892
- if (showSearchPathGate) return;
893
  if (replyingTo) {
894
  // This is a reply to a specific message
895
  await handleReplyToAdvisor(inputMessage, replyingTo);
@@ -1120,15 +1111,13 @@ const handleNewChat = async (sessionId = null) => {
1120
  onSendMessage={handleInputSubmit}
1121
  onFileUploaded={handleFileUploaded}
1122
  uploadedDocuments={uploadedDocuments}
1123
- isLoading={isLoading || showSearchPathGate}
1124
  currentChatSessionId={currentSessionId}
1125
  authToken={authToken}
1126
  synthesizedMode={synthesizedMode}
1127
  onToggleSynthesized={() => setSynthesizedMode((prev) => !prev)}
1128
  placeholder={
1129
- showSearchPathGate
1130
- ? 'Choose a focus above to start…'
1131
- : replyingTo
1132
  ? `Reply to ${replyingTo.advisorName}...`
1133
  : chatPlaceholder
1134
  }
@@ -1145,17 +1134,6 @@ const handleNewChat = async (sessionId = null) => {
1145
  />
1146
  )}
1147
 
1148
- {profileLoaded && showSearchPathGate && (
1149
- <SearchPathGate
1150
- authToken={authToken}
1151
- userId={userId}
1152
- onComplete={(profile) => {
1153
- if (profile) setUserProfile(profile);
1154
- setShowSearchPathGate(false);
1155
- }}
1156
- />
1157
- )}
1158
-
1159
  {showAccount && (
1160
  <AccountModal
1161
  user={user}
 
15
  import AdvisorCarousel from '../components/AdvisorCarousel';
16
  import OnboardingChat from '../components/OnboardingChat';
17
  import ProfileWalkthrough from '../components/ProfileWalkthrough';
18
+ import { persistFoodTrack } from '../components/SearchPathGate';
19
  import ClearDataModal from '../components/ClearDataModal';
20
  import AccountModal from '../components/AccountModal';
21
  import {
 
75
  const [showClearData, setShowClearData] = useState(false);
76
  const [showAccount, setShowAccount] = useState(false);
77
  const [userProfile, setUserProfile] = useState(null);
 
 
78
  const [synthesizedMode, setSynthesizedMode] = useState(false);
79
 
80
  const loadProfile = async () => {
81
+ persistFoodTrack(userId);
82
  try {
83
  const resp = await fetch(`${process.env.REACT_APP_API_URL}/api/users/me/profile`, {
84
  headers: { Authorization: `Bearer ${authToken}` },
 
86
  if (resp.ok) {
87
  const profile = await resp.json();
88
  setUserProfile(profile);
 
 
 
 
89
  }
90
  } catch (e) {
91
+ /* profile is optional for guests */
 
 
92
  }
93
  };
94
 
95
  useEffect(() => {
96
  if (authToken) {
 
97
  loadProfile();
98
  }
99
  }, [authToken]);
 
881
  }, [messages]);
882
 
883
  const handleInputSubmit = async (inputMessage) => {
 
884
  if (replyingTo) {
885
  // This is a reply to a specific message
886
  await handleReplyToAdvisor(inputMessage, replyingTo);
 
1111
  onSendMessage={handleInputSubmit}
1112
  onFileUploaded={handleFileUploaded}
1113
  uploadedDocuments={uploadedDocuments}
1114
+ isLoading={isLoading}
1115
  currentChatSessionId={currentSessionId}
1116
  authToken={authToken}
1117
  synthesizedMode={synthesizedMode}
1118
  onToggleSynthesized={() => setSynthesizedMode((prev) => !prev)}
1119
  placeholder={
1120
+ replyingTo
 
 
1121
  ? `Reply to ${replyingTo.advisorName}...`
1122
  : chatPlaceholder
1123
  }
 
1134
  />
1135
  )}
1136
 
 
 
 
 
 
 
 
 
 
 
 
1137
  {showAccount && (
1138
  <AccountModal
1139
  user={user}