Kennedy Johnson Cursor commited on
Commit
82ccb3f
·
1 Parent(s): 99ae678

Add guest try-without-account flow for MuscleGrowthAI.

Browse files

Port LaunchPad-style disposable guest sessions so visitors can explore advisors without signup, using EmailStr-safe @guests .musclegrowth.ai addresses.

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

multi_llm_chatbot_backend/app/api/routes/auth.py CHANGED
@@ -15,6 +15,8 @@ from app.core.auth import (
15
  )
16
  from app.core.database import get_database
17
  import logging
 
 
18
 
19
  logger = logging.getLogger(__name__)
20
 
@@ -173,6 +175,55 @@ async def login(user_credentials: UserLogin):
173
  detail="Login failed"
174
  )
175
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  @router.get("/me", response_model=UserResponse)
177
  async def get_current_user_profile(current_user: User = Depends(get_current_active_user)):
178
  """
 
15
  )
16
  from app.core.database import get_database
17
  import logging
18
+ import secrets
19
+ import uuid
20
 
21
  logger = logging.getLogger(__name__)
22
 
 
175
  detail="Login failed"
176
  )
177
 
178
+
179
+ @router.post("/guest", response_model=Token)
180
+ async def guest_login():
181
+ """Create a disposable guest session so visitors can try the advisors without signing up."""
182
+ try:
183
+ db = get_database()
184
+ guest_id = uuid.uuid4().hex[:12]
185
+ # Must be a syntactically valid email: EmailStr / email-validator
186
+ # rejects reserved TLDs like `.local`, which previously made guest
187
+ # sign-in always 500.
188
+ email = f"guest-{guest_id}@guests.musclegrowth.ai"
189
+ user = User(
190
+ firstName="Guest",
191
+ lastName="Athlete",
192
+ email=email,
193
+ hashed_password=get_password_hash(secrets.token_urlsafe(32)),
194
+ created_at=datetime.utcnow(),
195
+ last_login=datetime.utcnow(),
196
+ is_active=True,
197
+ is_guest=True,
198
+ )
199
+ result = await db.users.insert_one(user.dict(by_alias=True))
200
+ user.id = result.inserted_id
201
+ await db.user_profiles.update_one(
202
+ {"user_id": user.id},
203
+ {
204
+ "$set": {
205
+ "user_id": user.id,
206
+ "updated_at": datetime.utcnow(),
207
+ }
208
+ },
209
+ upsert=True,
210
+ )
211
+ access_token = create_access_token(
212
+ data={"sub": str(user.id), "guest": True},
213
+ expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES),
214
+ )
215
+ return Token(
216
+ access_token=access_token,
217
+ token_type="bearer",
218
+ user=create_user_response(user),
219
+ )
220
+ except Exception as e:
221
+ logger.error(f"Error during guest login: {e}")
222
+ raise HTTPException(
223
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
224
+ detail="Could not start guest session",
225
+ )
226
+
227
  @router.get("/me", response_model=UserResponse)
228
  async def get_current_user_profile(current_user: User = Depends(get_current_active_user)):
229
  """
multi_llm_chatbot_backend/app/core/auth.py CHANGED
@@ -115,5 +115,6 @@ def create_user_response(user: User) -> UserResponse:
115
  researchArea=user.researchArea,
116
  avatarId=user.avatarId,
117
  created_at=user.created_at,
118
- last_login=user.last_login
 
119
  )
 
115
  researchArea=user.researchArea,
116
  avatarId=user.avatarId,
117
  created_at=user.created_at,
118
+ last_login=user.last_login,
119
+ is_guest=bool(getattr(user, "is_guest", False)),
120
  )
multi_llm_chatbot_backend/app/models/user.py CHANGED
@@ -51,6 +51,7 @@ class User(BaseModel):
51
  created_at: datetime = Field(default_factory=datetime.utcnow)
52
  last_login: Optional[datetime] = None
53
  is_active: bool = True
 
54
 
55
  class UserUpdate(BaseModel):
56
  avatarId: Optional[str] = None
@@ -71,6 +72,7 @@ class UserResponse(BaseModel):
71
  avatarId: Optional[str] = None
72
  created_at: datetime
73
  last_login: Optional[datetime] = None
 
74
 
75
  class ChatSession(BaseModel):
76
  model_config = ConfigDict(
 
51
  created_at: datetime = Field(default_factory=datetime.utcnow)
52
  last_login: Optional[datetime] = None
53
  is_active: bool = True
54
+ is_guest: bool = False
55
 
56
  class UserUpdate(BaseModel):
57
  avatarId: Optional[str] = None
 
72
  avatarId: Optional[str] = None
73
  created_at: datetime
74
  last_login: Optional[datetime] = None
75
+ is_guest: bool = False
76
 
77
  class ChatSession(BaseModel):
78
  model_config = ConfigDict(
multi_llm_chatbot_backend/app/tests/unit/test_account_management.py CHANGED
@@ -16,6 +16,7 @@ from app.api.routes.auth import ( # noqa: E402
16
  UpdateProfileRequest,
17
  change_password,
18
  delete_account,
 
19
  update_profile,
20
  )
21
  from app.models.user import User # noqa: E402
@@ -205,6 +206,48 @@ class TestUpdateProfile(unittest.TestCase):
205
  self.assertIn("at least one field", str(ctx.exception).lower())
206
 
207
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
  # ------------------------------------------------------------------
209
  # DELETE /auth/me
210
  # ------------------------------------------------------------------
 
16
  UpdateProfileRequest,
17
  change_password,
18
  delete_account,
19
+ guest_login,
20
  update_profile,
21
  )
22
  from app.models.user import User # noqa: E402
 
206
  self.assertIn("at least one field", str(ctx.exception).lower())
207
 
208
 
209
+
210
+ # ------------------------------------------------------------------
211
+ # POST /auth/guest
212
+ # ------------------------------------------------------------------
213
+
214
+
215
+ @patch("app.api.routes.auth.create_access_token", return_value="guest-token")
216
+ @patch("app.api.routes.auth.get_password_hash", return_value="hashed")
217
+ @patch("app.api.routes.auth.get_database")
218
+ class TestGuestLogin(unittest.TestCase):
219
+
220
+ def test_creates_guest_user_with_valid_email(self, mock_get_db, _hash, _token):
221
+ db = _mock_db()
222
+ db.users.insert_one = AsyncMock(return_value=MagicMock(inserted_id=FAKE_USER_ID))
223
+ db.user_profiles = MagicMock()
224
+ db.user_profiles.update_one = AsyncMock()
225
+ mock_get_db.return_value = db
226
+
227
+ result = asyncio.run(guest_login())
228
+
229
+ self.assertEqual(result.access_token, "guest-token")
230
+ self.assertEqual(result.token_type, "bearer")
231
+ self.assertTrue(result.user.is_guest)
232
+ self.assertTrue(result.user.email.endswith("@guests.musclegrowth.ai"))
233
+ self.assertFalse(result.user.email.endswith(".local"))
234
+ db.users.insert_one.assert_called_once()
235
+ inserted = db.users.insert_one.call_args.args[0]
236
+ self.assertTrue(inserted.get("is_guest"))
237
+ self.assertTrue(str(inserted.get("email", "")).endswith("@guests.musclegrowth.ai"))
238
+
239
+ def test_legacy_local_guest_email_is_rejected_by_model(self, *_mocks):
240
+ """Regression: EmailStr must not accept reserved .local guest addresses."""
241
+ with self.assertRaises(ValidationError):
242
+ User(
243
+ firstName="Guest",
244
+ lastName="Athlete",
245
+ email="guest-abc@guest.musclegrowth.local",
246
+ hashed_password="x",
247
+ is_guest=True,
248
+ )
249
+
250
+
251
  # ------------------------------------------------------------------
252
  # DELETE /auth/me
253
  # ------------------------------------------------------------------
phd-advisor-frontend/src/App.js CHANGED
@@ -115,6 +115,26 @@ function App() {
115
  setCurrentView('chat');
116
  };
117
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  const handleSignOut = () => {
119
  clearAuthState();
120
  };
@@ -133,6 +153,7 @@ function App() {
133
  onNavigateToHome={navigateToHome}
134
  onNavigateToChat={sessionReady ? navigateToChat : navigateToAuth}
135
  onNavigateToCanvas={sessionReady ? navigateToCanvas : navigateToAuth}
 
136
  isAuthenticated={sessionReady}
137
  />
138
  )}
 
115
  setCurrentView('chat');
116
  };
117
 
118
+ const handleGuestStart = async () => {
119
+ if (authBootstrapping) {
120
+ return;
121
+ }
122
+ try {
123
+ const response = await fetch(`${getApiBaseUrl()}/auth/guest`, {
124
+ method: 'POST',
125
+ headers: { 'Content-Type': 'application/json' },
126
+ });
127
+ const data = await response.json();
128
+ if (response.ok) {
129
+ handleAuthSuccess(data.user, data.access_token);
130
+ } else {
131
+ setCurrentView('auth');
132
+ }
133
+ } catch {
134
+ setCurrentView('auth');
135
+ }
136
+ };
137
+
138
  const handleSignOut = () => {
139
  clearAuthState();
140
  };
 
153
  onNavigateToHome={navigateToHome}
154
  onNavigateToChat={sessionReady ? navigateToChat : navigateToAuth}
155
  onNavigateToCanvas={sessionReady ? navigateToCanvas : navigateToAuth}
156
+ onTryAsGuest={sessionReady ? navigateToChat : handleGuestStart}
157
  isAuthenticated={sessionReady}
158
  />
159
  )}
phd-advisor-frontend/src/components/Login.js CHANGED
@@ -1,7 +1,7 @@
1
  import React, { useState } from 'react';
2
  import { Eye, EyeOff, Mail, Lock, ArrowRight } from 'lucide-react';
3
  import { useAppConfig } from '../contexts/AppConfigContext';
4
- import { persistAuth } from '../utils/authStorage';
5
  import CopyrightNotice from './CopyrightNotice';
6
  import '../styles/Login.css';
7
 
@@ -14,6 +14,7 @@ const Login = ({ onNavigateToSignup, onNavigateToHome }) => {
14
  password: ''
15
  });
16
  const [isLoading, setIsLoading] = useState(false);
 
17
  const [errors, setErrors] = useState({});
18
 
19
  const handleInputChange = (e) => {
@@ -22,7 +23,6 @@ const Login = ({ onNavigateToSignup, onNavigateToHome }) => {
22
  ...prev,
23
  [name]: value
24
  }));
25
- // Clear error when user starts typing
26
  if (errors[name]) {
27
  setErrors(prev => ({
28
  ...prev,
@@ -33,32 +33,55 @@ const Login = ({ onNavigateToSignup, onNavigateToHome }) => {
33
 
34
  const validateForm = () => {
35
  const newErrors = {};
36
-
37
  if (!formData.email) {
38
  newErrors.email = 'Email is required';
39
  } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
40
  newErrors.email = 'Please enter a valid email address';
41
  }
42
-
43
  if (!formData.password) {
44
  newErrors.password = 'Password is required';
45
  } else if (formData.password.length < 6) {
46
  newErrors.password = 'Password must be at least 6 characters';
47
  }
48
-
49
  setErrors(newErrors);
50
  return Object.keys(newErrors).length === 0;
51
  };
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  const handleSubmit = async (e) => {
54
  e.preventDefault();
55
-
56
  if (!validateForm()) return;
57
-
58
  setIsLoading(true);
59
-
60
  try {
61
- const response = await fetch(`${process.env.REACT_APP_API_URL}/auth/login`, {
62
  method: 'POST',
63
  headers: {
64
  'Content-Type': 'application/json',
@@ -77,7 +100,7 @@ const Login = ({ onNavigateToSignup, onNavigateToHome }) => {
77
  } else {
78
  setErrors({ submit: data.detail || 'Login failed. Please try again.' });
79
  }
80
-
81
  } catch (error) {
82
  console.error('Login error:', error);
83
  setErrors({ submit: 'Login failed. Please try again.' });
@@ -86,11 +109,12 @@ const Login = ({ onNavigateToSignup, onNavigateToHome }) => {
86
  }
87
  };
88
 
 
 
89
  return (
90
  <div className="login-page">
91
  <div className="login-content">
92
  <div className="login-container">
93
- {/* Header */}
94
  <div className="login-header">
95
  <div className="logo-container">
96
  <LogoIcon className="logo-icon" />
@@ -101,11 +125,23 @@ const Login = ({ onNavigateToSignup, onNavigateToHome }) => {
101
  </p>
102
  </div>
103
 
104
- {/* Main Login Form */}
105
  <div className="login-form-container">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
  <form onSubmit={handleSubmit} className="login-form">
107
-
108
- {/* Email Field */}
109
  <div className="form-group">
110
  <label htmlFor="email" className="form-label">
111
  Email Address
@@ -120,7 +156,7 @@ const Login = ({ onNavigateToSignup, onNavigateToHome }) => {
120
  onChange={handleInputChange}
121
  className={`form-input ${errors.email ? 'error' : ''}`}
122
  placeholder="Enter your email"
123
- disabled={isLoading}
124
  />
125
  </div>
126
  {errors.email && (
@@ -128,7 +164,6 @@ const Login = ({ onNavigateToSignup, onNavigateToHome }) => {
128
  )}
129
  </div>
130
 
131
- {/* Password Field */}
132
  <div className="form-group">
133
  <label htmlFor="password" className="form-label">
134
  Password
@@ -143,13 +178,13 @@ const Login = ({ onNavigateToSignup, onNavigateToHome }) => {
143
  onChange={handleInputChange}
144
  className={`form-input ${errors.password ? 'error' : ''}`}
145
  placeholder="Enter your password"
146
- disabled={isLoading}
147
  />
148
  <button
149
  type="button"
150
  onClick={() => setShowPassword(!showPassword)}
151
  className="password-toggle"
152
- disabled={isLoading}
153
  >
154
  {showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
155
  </button>
@@ -159,25 +194,22 @@ const Login = ({ onNavigateToSignup, onNavigateToHome }) => {
159
  )}
160
  </div>
161
 
162
- {/* Forgot Password */}
163
  <div className="form-actions">
164
  <button type="button" className="forgot-password">
165
  Forgot your password?
166
  </button>
167
  </div>
168
 
169
- {/* Submit Error */}
170
  {errors.submit && (
171
  <div className="submit-error">
172
  {errors.submit}
173
  </div>
174
  )}
175
 
176
- {/* Submit Button */}
177
- <button
178
- type="submit"
179
  className={`submit-btn ${isLoading ? 'loading' : ''}`}
180
- disabled={isLoading}
181
  >
182
  {isLoading ? (
183
  <>
@@ -194,12 +226,11 @@ const Login = ({ onNavigateToSignup, onNavigateToHome }) => {
194
  </form>
195
  </div>
196
 
197
- {/* Footer */}
198
  <div className="login-footer">
199
  <p>
200
  Don't have an account?{' '}
201
- <button
202
- type="button"
203
  className="link-btn"
204
  onClick={onNavigateToSignup}
205
  >
 
1
  import React, { useState } from 'react';
2
  import { Eye, EyeOff, Mail, Lock, ArrowRight } from 'lucide-react';
3
  import { useAppConfig } from '../contexts/AppConfigContext';
4
+ import { persistAuth, getApiBaseUrl } from '../utils/authStorage';
5
  import CopyrightNotice from './CopyrightNotice';
6
  import '../styles/Login.css';
7
 
 
14
  password: ''
15
  });
16
  const [isLoading, setIsLoading] = useState(false);
17
+ const [isGuestLoading, setIsGuestLoading] = useState(false);
18
  const [errors, setErrors] = useState({});
19
 
20
  const handleInputChange = (e) => {
 
23
  ...prev,
24
  [name]: value
25
  }));
 
26
  if (errors[name]) {
27
  setErrors(prev => ({
28
  ...prev,
 
33
 
34
  const validateForm = () => {
35
  const newErrors = {};
36
+
37
  if (!formData.email) {
38
  newErrors.email = 'Email is required';
39
  } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
40
  newErrors.email = 'Please enter a valid email address';
41
  }
42
+
43
  if (!formData.password) {
44
  newErrors.password = 'Password is required';
45
  } else if (formData.password.length < 6) {
46
  newErrors.password = 'Password must be at least 6 characters';
47
  }
48
+
49
  setErrors(newErrors);
50
  return Object.keys(newErrors).length === 0;
51
  };
52
 
53
+ const handleGuestContinue = async () => {
54
+ setIsGuestLoading(true);
55
+ setErrors({});
56
+ try {
57
+ const response = await fetch(`${getApiBaseUrl()}/auth/guest`, {
58
+ method: 'POST',
59
+ headers: { 'Content-Type': 'application/json' },
60
+ });
61
+ const data = await response.json();
62
+ if (response.ok) {
63
+ persistAuth(data.user, data.access_token);
64
+ onNavigateToHome?.(data.user, data.access_token);
65
+ } else {
66
+ setErrors({ submit: data.detail || 'Could not start a guest session.' });
67
+ }
68
+ } catch (error) {
69
+ console.error('Guest login error:', error);
70
+ setErrors({ submit: 'Could not start a guest session. Please try again.' });
71
+ } finally {
72
+ setIsGuestLoading(false);
73
+ }
74
+ };
75
+
76
  const handleSubmit = async (e) => {
77
  e.preventDefault();
78
+
79
  if (!validateForm()) return;
80
+
81
  setIsLoading(true);
82
+
83
  try {
84
+ const response = await fetch(`${getApiBaseUrl()}/auth/login`, {
85
  method: 'POST',
86
  headers: {
87
  'Content-Type': 'application/json',
 
100
  } else {
101
  setErrors({ submit: data.detail || 'Login failed. Please try again.' });
102
  }
103
+
104
  } catch (error) {
105
  console.error('Login error:', error);
106
  setErrors({ submit: 'Login failed. Please try again.' });
 
109
  }
110
  };
111
 
112
+ const busy = isLoading || isGuestLoading;
113
+
114
  return (
115
  <div className="login-page">
116
  <div className="login-content">
117
  <div className="login-container">
 
118
  <div className="login-header">
119
  <div className="logo-container">
120
  <LogoIcon className="logo-icon" />
 
125
  </p>
126
  </div>
127
 
 
128
  <div className="login-form-container">
129
+ <button
130
+ type="button"
131
+ className={`guest-continue-btn ${isGuestLoading ? 'loading' : ''}`}
132
+ onClick={handleGuestContinue}
133
+ disabled={busy}
134
+ >
135
+ {isGuestLoading ? 'Starting guest mode…' : 'Try without an account'}
136
+ </button>
137
+ <p className="guest-continue-hint">
138
+ Explore the advisors first. Create an account later if you want to save your training chats.
139
+ </p>
140
+ <div className="login-divider" aria-hidden="true">
141
+ <span>or sign in</span>
142
+ </div>
143
+
144
  <form onSubmit={handleSubmit} className="login-form">
 
 
145
  <div className="form-group">
146
  <label htmlFor="email" className="form-label">
147
  Email Address
 
156
  onChange={handleInputChange}
157
  className={`form-input ${errors.email ? 'error' : ''}`}
158
  placeholder="Enter your email"
159
+ disabled={busy}
160
  />
161
  </div>
162
  {errors.email && (
 
164
  )}
165
  </div>
166
 
 
167
  <div className="form-group">
168
  <label htmlFor="password" className="form-label">
169
  Password
 
178
  onChange={handleInputChange}
179
  className={`form-input ${errors.password ? 'error' : ''}`}
180
  placeholder="Enter your password"
181
+ disabled={busy}
182
  />
183
  <button
184
  type="button"
185
  onClick={() => setShowPassword(!showPassword)}
186
  className="password-toggle"
187
+ disabled={busy}
188
  >
189
  {showPassword ? <EyeOff size={16} /> : <Eye size={16} />}
190
  </button>
 
194
  )}
195
  </div>
196
 
 
197
  <div className="form-actions">
198
  <button type="button" className="forgot-password">
199
  Forgot your password?
200
  </button>
201
  </div>
202
 
 
203
  {errors.submit && (
204
  <div className="submit-error">
205
  {errors.submit}
206
  </div>
207
  )}
208
 
209
+ <button
210
+ type="submit"
 
211
  className={`submit-btn ${isLoading ? 'loading' : ''}`}
212
+ disabled={busy}
213
  >
214
  {isLoading ? (
215
  <>
 
226
  </form>
227
  </div>
228
 
 
229
  <div className="login-footer">
230
  <p>
231
  Don't have an account?{' '}
232
+ <button
233
+ type="button"
234
  className="link-btn"
235
  onClick={onNavigateToSignup}
236
  >
phd-advisor-frontend/src/components/Signup.js CHANGED
@@ -1,7 +1,7 @@
1
  import React, { useState } from 'react';
2
  import { Eye, EyeOff, Mail, Lock, User, ArrowRight, Shield, Globe } from 'lucide-react';
3
  import { useAppConfig } from '../contexts/AppConfigContext';
4
- import { persistAuth } from '../utils/authStorage';
5
  import '../styles/Signup.css';
6
 
7
  const Signup = ({ onNavigateToLogin, onNavigateToHome }) => {
@@ -100,7 +100,7 @@ const Signup = ({ onNavigateToLogin, onNavigateToHome }) => {
100
  setIsLoading(true);
101
 
102
  try {
103
- const response = await fetch(`${process.env.REACT_APP_API_URL}/auth/signup`, {
104
  method: 'POST',
105
  headers: {
106
  'Content-Type': 'application/json',
@@ -383,6 +383,28 @@ const Signup = ({ onNavigateToLogin, onNavigateToHome }) => {
383
  >
384
  Sign in here
385
  </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
386
  </p>
387
  </div>
388
  </div>
 
1
  import React, { useState } from 'react';
2
  import { Eye, EyeOff, Mail, Lock, User, ArrowRight, Shield, Globe } from 'lucide-react';
3
  import { useAppConfig } from '../contexts/AppConfigContext';
4
+ import { persistAuth, getApiBaseUrl } from '../utils/authStorage';
5
  import '../styles/Signup.css';
6
 
7
  const Signup = ({ onNavigateToLogin, onNavigateToHome }) => {
 
100
  setIsLoading(true);
101
 
102
  try {
103
+ const response = await fetch(`${getApiBaseUrl()}/auth/signup`, {
104
  method: 'POST',
105
  headers: {
106
  'Content-Type': 'application/json',
 
383
  >
384
  Sign in here
385
  </button>
386
+ {' · '}
387
+ <button
388
+ type="button"
389
+ className="link-btn"
390
+ onClick={async () => {
391
+ try {
392
+ const response = await fetch(`${getApiBaseUrl()}/auth/guest`, {
393
+ method: 'POST',
394
+ headers: { 'Content-Type': 'application/json' },
395
+ });
396
+ const data = await response.json();
397
+ if (response.ok) {
398
+ persistAuth(data.user, data.access_token);
399
+ onNavigateToHome?.(data.user, data.access_token);
400
+ }
401
+ } catch (e) {
402
+ console.error(e);
403
+ }
404
+ }}
405
+ >
406
+ Try without an account
407
+ </button>
408
  </p>
409
  </div>
410
  </div>
phd-advisor-frontend/src/pages/ChatPage.js CHANGED
@@ -951,6 +951,11 @@ const handleNewChat = async (sessionId = null) => {
951
  <HelpCircle size={18} />
952
  </button>
953
  </AppHeader>
 
 
 
 
 
954
 
955
  {/* Main Content */}
956
  <div className="chat-content">
 
951
  <HelpCircle size={18} />
952
  </button>
953
  </AppHeader>
954
+ {user?.is_guest && (
955
+ <div className="guest-mode-banner" role="status">
956
+ You&apos;re exploring as a guest. Sign out from the menu when you&apos;re ready to create an account and keep your training progress.
957
+ </div>
958
+ )}
959
 
960
  {/* Main Content */}
961
  <div className="chat-content">
phd-advisor-frontend/src/pages/HomePage.js CHANGED
@@ -5,7 +5,7 @@ import AppHeader from '../components/AppHeader';
5
  import CopyrightNotice from '../components/CopyrightNotice';
6
  import { useAppConfig } from '../contexts/AppConfigContext';
7
 
8
- const HomePage = ({ onNavigateToChat, isAuthenticated, onNavigateToHome, onNavigateToCanvas }) => {
9
  const { config, advisors, resolveIcon } = useAppConfig();
10
 
11
  return (
@@ -27,14 +27,25 @@ const HomePage = ({ onNavigateToChat, isAuthenticated, onNavigateToHome, onNavig
27
  <p className="hero-subtitle">
28
  {config.homepage.description}
29
  </p>
30
- <button
31
- onClick={onNavigateToChat}
32
- className="cta-button"
33
- >
34
- <MessageCircle className="cta-icon" />
35
- <span>{isAuthenticated ? 'Continue Conversation' : 'Start Conversation'}</span>
36
- <ArrowRight className="cta-arrow" />
37
- </button>
 
 
 
 
 
 
 
 
 
 
 
38
  </div>
39
 
40
  {/* Advisors Grid */}
 
5
  import CopyrightNotice from '../components/CopyrightNotice';
6
  import { useAppConfig } from '../contexts/AppConfigContext';
7
 
8
+ const HomePage = ({ onNavigateToChat, onTryAsGuest, isAuthenticated, onNavigateToHome, onNavigateToCanvas }) => {
9
  const { config, advisors, resolveIcon } = useAppConfig();
10
 
11
  return (
 
27
  <p className="hero-subtitle">
28
  {config.homepage.description}
29
  </p>
30
+ <div className="cta-group">
31
+ <button
32
+ onClick={onNavigateToChat}
33
+ className="cta-button"
34
+ >
35
+ <MessageCircle className="cta-icon" />
36
+ <span>{isAuthenticated ? 'Continue Conversation' : 'Start Conversation'}</span>
37
+ <ArrowRight className="cta-arrow" />
38
+ </button>
39
+ {!isAuthenticated && (
40
+ <button
41
+ type="button"
42
+ onClick={onTryAsGuest}
43
+ className="cta-button cta-button-secondary"
44
+ >
45
+ <span>Try without an account</span>
46
+ </button>
47
+ )}
48
+ </div>
49
  </div>
50
 
51
  {/* Advisors Grid */}
phd-advisor-frontend/src/styles/ChatPage.css CHANGED
@@ -13,6 +13,32 @@
13
  transform: translateZ(0);
14
  }
15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  /* Floating Header */
17
  .floating-header {
18
  position: sticky;
 
13
  transform: translateZ(0);
14
  }
15
 
16
+ .guest-mode-banner {
17
+ flex-shrink: 0;
18
+ padding: 8px 16px;
19
+ background: var(--accent-soft, #F3E8FF);
20
+ color: var(--text-accent, #5B21B6);
21
+ font-size: 0.875rem;
22
+ text-align: center;
23
+ border-bottom: 1px solid var(--accent-muted, #DDD6FE);
24
+ }
25
+
26
+ .guest-mode-banner button {
27
+ background: none;
28
+ border: none;
29
+ color: var(--accent-primary, #7C3AED);
30
+ font-weight: 700;
31
+ text-decoration: underline;
32
+ cursor: pointer;
33
+ padding: 0;
34
+ }
35
+
36
+ [data-theme="dark"] .guest-mode-banner {
37
+ background: var(--accent-soft, rgba(124, 58, 237, 0.2));
38
+ color: var(--text-accent, #E9D5FF);
39
+ border-bottom-color: var(--border-primary);
40
+ }
41
+
42
  /* Floating Header */
43
  .floating-header {
44
  position: sticky;
phd-advisor-frontend/src/styles/Login.css CHANGED
@@ -92,6 +92,60 @@
92
  line-height: 1.5;
93
  }
94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  /* Form Container */
96
  .login-form-container {
97
  padding: 32px;
 
92
  line-height: 1.5;
93
  }
94
 
95
+
96
+ /* Guest continue */
97
+ .guest-continue-btn {
98
+ width: 100%;
99
+ display: inline-flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ gap: 8px;
103
+ border: 2px solid var(--accent-primary, #7C3AED);
104
+ border-radius: 12px;
105
+ padding: 12px 16px;
106
+ background: var(--accent-soft, #F3E8FF);
107
+ color: var(--accent-primary, #7C3AED);
108
+ font-weight: 700;
109
+ cursor: pointer;
110
+ margin-bottom: 8px;
111
+ }
112
+
113
+ .guest-continue-btn:hover:not(:disabled) {
114
+ background: var(--accent-muted, #DDD6FE);
115
+ }
116
+
117
+ .guest-continue-btn:disabled {
118
+ opacity: 0.6;
119
+ cursor: not-allowed;
120
+ }
121
+
122
+ .guest-continue-hint {
123
+ margin: 0 0 16px;
124
+ text-align: center;
125
+ font-size: 0.85rem;
126
+ color: var(--text-secondary, #6B7280);
127
+ line-height: 1.4;
128
+ }
129
+
130
+ .login-divider {
131
+ display: flex;
132
+ align-items: center;
133
+ gap: 12px;
134
+ margin: 0 0 18px;
135
+ color: var(--text-tertiary, #9CA3AF);
136
+ font-size: 0.8rem;
137
+ text-transform: uppercase;
138
+ letter-spacing: 0.04em;
139
+ }
140
+
141
+ .login-divider::before,
142
+ .login-divider::after {
143
+ content: "";
144
+ flex: 1;
145
+ height: 1px;
146
+ background: var(--border-primary, #E5E7EB);
147
+ }
148
+
149
  /* Form Container */
150
  .login-form-container {
151
  padding: 32px;
phd-advisor-frontend/src/styles/components.css CHANGED
@@ -21,6 +21,8 @@
21
  --accent-primary: #6366F1;
22
  --accent-secondary: #8B5CF6;
23
  --accent-gradient: linear-gradient(135deg, #6366F1, #8B5CF6);
 
 
24
 
25
  /* Shadow Colors */
26
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
@@ -69,6 +71,8 @@
69
 
70
  /* Accent Colors */
71
  --accent-primary: #818CF8;
 
 
72
  --accent-secondary: #A78BFA;
73
  --accent-gradient: linear-gradient(135deg, #818CF8, #A78BFA);
74
 
@@ -245,6 +249,37 @@
245
  transform: scale(1.05);
246
  }
247
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
248
  .cta-icon {
249
  width: 24px;
250
  height: 24px;
 
21
  --accent-primary: #6366F1;
22
  --accent-secondary: #8B5CF6;
23
  --accent-gradient: linear-gradient(135deg, #6366F1, #8B5CF6);
24
+ --accent-soft: #F3E8FF;
25
+ --accent-muted: #DDD6FE;
26
 
27
  /* Shadow Colors */
28
  --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
 
71
 
72
  /* Accent Colors */
73
  --accent-primary: #818CF8;
74
+ --accent-soft: rgba(124, 58, 237, 0.25);
75
+ --accent-muted: rgba(167, 139, 250, 0.35);
76
  --accent-secondary: #A78BFA;
77
  --accent-gradient: linear-gradient(135deg, #818CF8, #A78BFA);
78
 
 
249
  transform: scale(1.05);
250
  }
251
 
252
+ .cta-group {
253
+ display: flex;
254
+ flex-wrap: wrap;
255
+ gap: 12px;
256
+ align-items: center;
257
+ justify-content: center;
258
+ }
259
+
260
+ .cta-button-secondary {
261
+ background: var(--accent-soft, #F3E8FF);
262
+ color: var(--accent-primary, #7C3AED);
263
+ border: 2px solid var(--accent-primary, #7C3AED);
264
+ box-shadow: none;
265
+ }
266
+
267
+ .cta-button-secondary:hover {
268
+ background: var(--accent-muted, #DDD6FE);
269
+ }
270
+
271
+ .auth-bootstrap-overlay {
272
+ position: fixed;
273
+ inset: 0;
274
+ z-index: 9999;
275
+ display: flex;
276
+ align-items: center;
277
+ justify-content: center;
278
+ background: var(--bg-primary, #fff);
279
+ color: var(--text-secondary, #6B7280);
280
+ font-size: 1rem;
281
+ }
282
+
283
  .cta-icon {
284
  width: 24px;
285
  height: 24px;