Spaces:
Sleeping
Sleeping
Kennedy Johnson Cursor commited on
Commit ·
371ef22
1
Parent(s): 4ae79ba
Remove guest Foods/Supplements/Exercise/Healthcare track popup.
Browse filesHeidi’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
|
| 2 |
-
import {
|
| 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:
|
| 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 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
'more fruits & vegetables',
|
| 47 |
-
'more fruits and vegetables',
|
| 48 |
-
]);
|
| 49 |
|
| 50 |
/**
|
| 51 |
-
*
|
| 52 |
-
* Shown once until a known focus is saved (profile or localStorage).
|
| 53 |
*/
|
| 54 |
-
|
| 55 |
-
|
| 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
|
| 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 |
-
|
| 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
|
| 1124 |
currentChatSessionId={currentSessionId}
|
| 1125 |
authToken={authToken}
|
| 1126 |
synthesizedMode={synthesizedMode}
|
| 1127 |
onToggleSynthesized={() => setSynthesizedMode((prev) => !prev)}
|
| 1128 |
placeholder={
|
| 1129 |
-
|
| 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}
|