NeonRyan neondaniel commited on
Commit
268a18f
·
unverified ·
1 Parent(s): 82a0eb1

Add Onboarding tour (#48)

Browse files

* Upgraded onboarding experience by adding OnboardingTour component and related configuration.Using Reactour each indivual component is highlighted. Updated package.json to include @reactour/tour for onboarding functionality. Introduced TESTING_ONBOARDING flag in App.js to control tour visibility during development this allows for quick and easy changes.

* Refactor onboarding logic to utilize environment variable for TESTING_ONBOARDING flag in App.js. Enhanced OnboardingTour component by integrating dynamic feature and advisor data from AppConfigContext, improving the onboarding experience with customizable content based on backend configuration.

* Add onboarding and canvas configurations to YAML files and backend models

- Introduced onboarding features for both PhD and undergraduate configurations, including AI advisor advice, conversation saving, document uploads, and progress tracking.
- Added canvas configurations with tour titles and descriptions for PhD and undergraduate journeys.
- Updated backend models to include new onboarding and canvas configurations, ensuring they are integrated into the application settings.

* Pass `REACT_APP_TESTING_ONBOARDING` envvar to frontend container

* Rebase Complete

* Update App.js

* Refactor onboarding configuration by removing canvas settings

- Removed the CanvasConfig from the onboarding configuration in both the YAML and backend model files.
- Updated the OnboardingTour component to reference the onboarding configuration for tour titles and bodies instead of the removed canvas settings, streamlining the onboarding experience.

---------

Co-authored-by: Daniel McKnight <daniel@neon.ai>

docker-compose.yml CHANGED
@@ -32,6 +32,7 @@ services:
32
  - backend
33
  environment:
34
  REACT_APP_API_URL: ${REACT_APP_API_URL:-http://localhost:8000}
 
35
  database:
36
  image: mongo:8.0
37
  volumes:
 
32
  - backend
33
  environment:
34
  REACT_APP_API_URL: ${REACT_APP_API_URL:-http://localhost:8000}
35
+ REACT_APP_TESTING_ONBOARDING: ${REACT_APP_TESTING_ONBOARDING:-false}
36
  database:
37
  image: mongo:8.0
38
  volumes:
multi_llm_chatbot_backend/app/config.py CHANGED
@@ -88,6 +88,12 @@ class ChatPageConfig(BaseModel):
88
  examples: List[ExampleCategory] = []
89
 
90
 
 
 
 
 
 
 
91
  class PersonaItemConfig(_IconValidatorMixin):
92
  id: str
93
  name: str
@@ -305,6 +311,7 @@ class AppSettings(BaseModel):
305
  homepage: HomepageConfig = HomepageConfig()
306
  login: LoginConfig = LoginConfig()
307
  chat_page: ChatPageConfig = ChatPageConfig()
 
308
  personas: PersonasConfig = PersonasConfig()
309
  orchestrator: OrchestratorConfig = OrchestratorConfig()
310
  auth: AuthConfig = AuthConfig()
@@ -326,6 +333,7 @@ class AppSettings(BaseModel):
326
  "homepage": self.homepage.dict(),
327
  "login": self.login.dict(),
328
  "chat_page": self.chat_page.dict(),
 
329
  "personas": {
330
  "items": [p.to_frontend_config() for p in self.personas.items],
331
  },
 
88
  examples: List[ExampleCategory] = []
89
 
90
 
91
+ class OnboardingConfig(BaseModel):
92
+ features: List[FeatureConfig] = []
93
+ tour_title: str = ""
94
+ tour_body: str = ""
95
+
96
+
97
  class PersonaItemConfig(_IconValidatorMixin):
98
  id: str
99
  name: str
 
311
  homepage: HomepageConfig = HomepageConfig()
312
  login: LoginConfig = LoginConfig()
313
  chat_page: ChatPageConfig = ChatPageConfig()
314
+ onboarding: OnboardingConfig = OnboardingConfig()
315
  personas: PersonasConfig = PersonasConfig()
316
  orchestrator: OrchestratorConfig = OrchestratorConfig()
317
  auth: AuthConfig = AuthConfig()
 
333
  "homepage": self.homepage.dict(),
334
  "login": self.login.dict(),
335
  "chat_page": self.chat_page.dict(),
336
+ "onboarding": self.onboarding.dict(),
337
  "personas": {
338
  "items": [p.to_frontend_config() for p in self.personas.items],
339
  },
phd-advisor-frontend/package-lock.json CHANGED
@@ -8,6 +8,7 @@
8
  "name": "phd-advisor-frontend",
9
  "version": "0.1.0",
10
  "dependencies": {
 
11
  "@testing-library/dom": "^10.4.0",
12
  "@testing-library/jest-dom": "^6.6.3",
13
  "@testing-library/react": "^16.3.0",
@@ -3090,6 +3091,57 @@
3090
  }
3091
  }
3092
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3093
  "node_modules/@rollup/plugin-babel": {
3094
  "version": "5.3.1",
3095
  "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@@ -3169,6 +3221,15 @@
3169
  "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
3170
  "license": "MIT"
3171
  },
 
 
 
 
 
 
 
 
 
3172
  "node_modules/@rtsao/scc": {
3173
  "version": "1.1.0",
3174
  "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -3866,16 +3927,6 @@
3866
  "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
3867
  "license": "MIT"
3868
  },
3869
- "node_modules/@types/react": {
3870
- "version": "19.1.9",
3871
- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.9.tgz",
3872
- "integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==",
3873
- "license": "MIT",
3874
- "peer": true,
3875
- "dependencies": {
3876
- "csstype": "^3.0.2"
3877
- }
3878
- },
3879
  "node_modules/@types/resolve": {
3880
  "version": "1.17.1",
3881
  "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@@ -6486,13 +6537,6 @@
6486
  "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
6487
  "license": "MIT"
6488
  },
6489
- "node_modules/csstype": {
6490
- "version": "3.1.3",
6491
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
6492
- "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
6493
- "license": "MIT",
6494
- "peer": true
6495
- },
6496
  "node_modules/damerau-levenshtein": {
6497
  "version": "1.0.8",
6498
  "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -15563,6 +15607,12 @@
15563
  "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
15564
  "license": "MIT"
15565
  },
 
 
 
 
 
 
15566
  "node_modules/resolve": {
15567
  "version": "1.22.10",
15568
  "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -17704,20 +17754,6 @@
17704
  "is-typedarray": "^1.0.0"
17705
  }
17706
  },
17707
- "node_modules/typescript": {
17708
- "version": "4.9.5",
17709
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
17710
- "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
17711
- "license": "Apache-2.0",
17712
- "peer": true,
17713
- "bin": {
17714
- "tsc": "bin/tsc",
17715
- "tsserver": "bin/tsserver"
17716
- },
17717
- "engines": {
17718
- "node": ">=4.2.0"
17719
- }
17720
- },
17721
  "node_modules/unbox-primitive": {
17722
  "version": "1.1.0",
17723
  "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
 
8
  "name": "phd-advisor-frontend",
9
  "version": "0.1.0",
10
  "dependencies": {
11
+ "@reactour/tour": "^3.8.0",
12
  "@testing-library/dom": "^10.4.0",
13
  "@testing-library/jest-dom": "^6.6.3",
14
  "@testing-library/react": "^16.3.0",
 
3091
  }
3092
  }
3093
  },
3094
+ "node_modules/@reactour/mask": {
3095
+ "version": "1.2.0",
3096
+ "resolved": "https://registry.npmjs.org/@reactour/mask/-/mask-1.2.0.tgz",
3097
+ "integrity": "sha512-XLgBLWfKJybtZjNTSO5lt/SIvRlCZBadB6JfE/hO1ErqURRjYhnv+edC0Ki1haUCqMGFppWk3lwcPCjmK0xNog==",
3098
+ "license": "MIT",
3099
+ "dependencies": {
3100
+ "@reactour/utils": "*"
3101
+ },
3102
+ "peerDependencies": {
3103
+ "react": "16.x || 17.x || 18.x || 19.x"
3104
+ }
3105
+ },
3106
+ "node_modules/@reactour/popover": {
3107
+ "version": "1.3.0",
3108
+ "resolved": "https://registry.npmjs.org/@reactour/popover/-/popover-1.3.0.tgz",
3109
+ "integrity": "sha512-YdyjSmHPvEeQEcJM4gcGFa5pI/Yf4nZGqwG4JnT+rK1SyUJBIPnm4Gkl/h7/+1g0KCFMkwNwagS3ZiXvZB7ThA==",
3110
+ "license": "MIT",
3111
+ "dependencies": {
3112
+ "@reactour/utils": "*"
3113
+ },
3114
+ "peerDependencies": {
3115
+ "react": "16.x || 17.x || 18.x || 19.x"
3116
+ }
3117
+ },
3118
+ "node_modules/@reactour/tour": {
3119
+ "version": "3.8.0",
3120
+ "resolved": "https://registry.npmjs.org/@reactour/tour/-/tour-3.8.0.tgz",
3121
+ "integrity": "sha512-KZTFi1pAvoTVKKRdBN5+XCYxXBp4k4Ql/acZcXyPvec8VU24fkMSEeV+v8krfYQpoVcewxIu3gM6xWZZLjxi7w==",
3122
+ "license": "MIT",
3123
+ "dependencies": {
3124
+ "@reactour/mask": "*",
3125
+ "@reactour/popover": "*",
3126
+ "@reactour/utils": "*"
3127
+ },
3128
+ "peerDependencies": {
3129
+ "react": "16.x || 17.x || 18.x || 19.x"
3130
+ }
3131
+ },
3132
+ "node_modules/@reactour/utils": {
3133
+ "version": "0.6.0",
3134
+ "resolved": "https://registry.npmjs.org/@reactour/utils/-/utils-0.6.0.tgz",
3135
+ "integrity": "sha512-GqaLjQi7MJsgtAKjdiw2Eak1toFkADoLRnm1+HZpaD+yl+DkaHpC1N7JAl+kVOO5I17bWInPA+OFbXjO9Co8Qg==",
3136
+ "license": "MIT",
3137
+ "dependencies": {
3138
+ "@rooks/use-mutation-observer": "^4.11.2",
3139
+ "resize-observer-polyfill": "^1.5.1"
3140
+ },
3141
+ "peerDependencies": {
3142
+ "react": "16.x || 17.x || 18.x || 19.x"
3143
+ }
3144
+ },
3145
  "node_modules/@rollup/plugin-babel": {
3146
  "version": "5.3.1",
3147
  "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
 
3221
  "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
3222
  "license": "MIT"
3223
  },
3224
+ "node_modules/@rooks/use-mutation-observer": {
3225
+ "version": "4.11.2",
3226
+ "resolved": "https://registry.npmjs.org/@rooks/use-mutation-observer/-/use-mutation-observer-4.11.2.tgz",
3227
+ "integrity": "sha512-vpsdrZdr6TkB1zZJcHx+fR1YC/pHs2BaqcuYiEGjBVbwY5xcC49+h0hAUtQKHth3oJqXfIX/Ng8S7s5HFHdM/A==",
3228
+ "license": "MIT",
3229
+ "peerDependencies": {
3230
+ "react": ">=16.8.0"
3231
+ }
3232
+ },
3233
  "node_modules/@rtsao/scc": {
3234
  "version": "1.1.0",
3235
  "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
 
3927
  "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
3928
  "license": "MIT"
3929
  },
 
 
 
 
 
 
 
 
 
 
3930
  "node_modules/@types/resolve": {
3931
  "version": "1.17.1",
3932
  "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
 
6537
  "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==",
6538
  "license": "MIT"
6539
  },
 
 
 
 
 
 
 
6540
  "node_modules/damerau-levenshtein": {
6541
  "version": "1.0.8",
6542
  "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
 
15607
  "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
15608
  "license": "MIT"
15609
  },
15610
+ "node_modules/resize-observer-polyfill": {
15611
+ "version": "1.5.1",
15612
+ "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
15613
+ "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
15614
+ "license": "MIT"
15615
+ },
15616
  "node_modules/resolve": {
15617
  "version": "1.22.10",
15618
  "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
 
17754
  "is-typedarray": "^1.0.0"
17755
  }
17756
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17757
  "node_modules/unbox-primitive": {
17758
  "version": "1.1.0",
17759
  "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
phd-advisor-frontend/package.json CHANGED
@@ -10,6 +10,7 @@
10
  "lucide-react": "^0.544.0",
11
  "react": "^19.1.0",
12
  "react-dom": "^19.1.0",
 
13
  "react-markdown": "^10.1.0",
14
  "react-scripts": "5.0.1",
15
  "remark-gfm": "^4.0.1",
 
10
  "lucide-react": "^0.544.0",
11
  "react": "^19.1.0",
12
  "react-dom": "^19.1.0",
13
+ "@reactour/tour": "^3.8.0",
14
  "react-markdown": "^10.1.0",
15
  "react-scripts": "5.0.1",
16
  "remark-gfm": "^4.0.1",
phd-advisor-frontend/src/App.js CHANGED
@@ -8,6 +8,11 @@ import CanvasPage from './pages/CanvasPage';
8
  import UserGuide from './components/UserGuide';
9
  import './styles/components.css';
10
 
 
 
 
 
 
11
  function App() {
12
  const [currentView, setCurrentView] = useState('home');
13
  const [isAuthenticated, setIsAuthenticated] = useState(false);
 
8
  import UserGuide from './components/UserGuide';
9
  import './styles/components.css';
10
 
11
+ // Set REACT_APP_TESTING_ONBOARDING=true in your .env to force the onboarding
12
+ // tour to run on every page load. Leave unset in production — tour will only
13
+ // show once per user (localStorage).
14
+ export const TESTING_ONBOARDING = process.env.REACT_APP_TESTING_ONBOARDING === 'true';
15
+
16
  function App() {
17
  const [currentView, setCurrentView] = useState('home');
18
  const [isAuthenticated, setIsAuthenticated] = useState(false);
phd-advisor-frontend/src/components/OnboardingTour.js ADDED
@@ -0,0 +1,320 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useMemo } from 'react';
2
+ import { TourProvider, useTour } from '@reactour/tour';
3
+ import { Hand, GraduationCap, Plus, MessageCircle, Paperclip, BarChart3 } from 'lucide-react';
4
+ import { useTheme } from '../contexts/ThemeContext';
5
+ import { useAppConfig } from '../contexts/AppConfigContext';
6
+ import { TESTING_ONBOARDING } from '../App';
7
+ import '../styles/OnboardingTour.css';
8
+
9
+ const STORAGE_KEY = 'hasSeenOnboardingTour';
10
+
11
+ // Fallbacks used when config.onboarding.* fields aren't provided by the backend.
12
+ const DEFAULT_FEATURES = [
13
+ { Icon: GraduationCap, label: 'Get advice from specialized AI advisors' },
14
+ { Icon: MessageCircle, label: 'Save and revisit every conversation' },
15
+ { Icon: Paperclip, label: 'Upload PDFs and documents for context-aware answers' },
16
+ { Icon: BarChart3, label: 'Track your progress on a structured canvas' },
17
+ ];
18
+ const DEFAULT_CANVAS_STEP = {
19
+ title: 'PhD Progress Canvas',
20
+ body: 'A dashboard view of your PhD journey — research progress, methodology, next steps, all in one place.',
21
+ };
22
+
23
+ const buildAdvisorBody = (advisors) => {
24
+ const names = Object.values(advisors || {}).map((a) => a.name).filter(Boolean);
25
+ if (names.length === 0) {
26
+ return "AI personas are ready to help. Click here anytime to see who's available.";
27
+ }
28
+ const firstThree = names.slice(0, 3).join(', ');
29
+ return `${names.length} AI personas are ready to help — ${firstThree}, and more. Click here anytime to see who's available.`;
30
+ };
31
+
32
+ const buildFeatures = (config, resolveIcon) => {
33
+ const fromConfig = config?.onboarding?.features;
34
+ if (!Array.isArray(fromConfig) || fromConfig.length === 0) return DEFAULT_FEATURES;
35
+ return fromConfig.map((f) => ({
36
+ Icon: f.icon ? resolveIcon(f.icon) : GraduationCap,
37
+ label: f.label || f.description || f.title || '',
38
+ }));
39
+ };
40
+
41
+ // Theme-resolved colors that match the rest of the app exactly
42
+ const palette = (isDark) => ({
43
+ bg: isDark ? '#1F2937' : '#FFFFFF',
44
+ bgSubtle: isDark ? '#111827' : '#F9FAFB',
45
+ text: isDark ? '#F9FAFB' : '#111827',
46
+ textMuted: isDark ? '#D1D5DB' : '#6B7280',
47
+ textDim: isDark ? '#9CA3AF' : '#9CA3AF',
48
+ border: isDark ? '#374151' : '#E5E7EB',
49
+ accent: '#2663EB',
50
+ accentGrad: '#2663EB',
51
+ accentShadow: 'rgba(38, 99, 235, 0.45)',
52
+ });
53
+
54
+ // --- Step content -------------------------------------------------------
55
+ const titleStyle = (c) => ({
56
+ margin: 0, fontSize: 22, fontWeight: 700, lineHeight: 1.2,
57
+ color: c.text, WebkitTextFillColor: c.text, letterSpacing: '-0.02em',
58
+ });
59
+ const bodyStyle = (c) => ({
60
+ margin: 0, fontSize: 15.5, lineHeight: 1.55,
61
+ color: c.textMuted, WebkitTextFillColor: c.textMuted,
62
+ });
63
+
64
+ const StepBody = ({ title, body, Icon, c }) => (
65
+ <div style={{ fontFamily: 'system-ui, -apple-system, sans-serif' }}>
66
+ <div style={{ display: 'flex', alignItems: 'center', gap: 14, marginBottom: 14 }}>
67
+ <div style={{
68
+ width: 48, height: 48, borderRadius: 14,
69
+ background: c.accent,
70
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
71
+ flexShrink: 0,
72
+ boxShadow: `0 6px 16px -4px ${c.accentShadow}`,
73
+ }}>
74
+ <Icon size={24} color="#fff" strokeWidth={2.2} />
75
+ </div>
76
+ <div style={titleStyle(c)}>{title}</div>
77
+ </div>
78
+ <div style={bodyStyle(c)}>{body}</div>
79
+ </div>
80
+ );
81
+
82
+ const WelcomeBody = ({ c, title, subtitle, features }) => {
83
+ return (
84
+ <div style={{ fontFamily: 'system-ui, -apple-system, sans-serif', textAlign: 'center', padding: '4px 8px' }}>
85
+ <div style={{
86
+ width: 72, height: 72, borderRadius: 20, margin: '0 auto 20px',
87
+ background: c.accent,
88
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
89
+ boxShadow: `0 12px 32px -8px ${c.accentShadow}`,
90
+ }}>
91
+ <Hand size={36} color="#fff" strokeWidth={2.2} />
92
+ </div>
93
+ <div style={{
94
+ margin: '0 0 12px', fontSize: 30, fontWeight: 800, lineHeight: 1.15,
95
+ color: c.text, WebkitTextFillColor: c.text, letterSpacing: '-0.025em',
96
+ }}>
97
+ Welcome to your<br />{title}
98
+ </div>
99
+ <div style={{
100
+ margin: '0 0 22px', fontSize: 16, lineHeight: 1.55,
101
+ color: c.textMuted, WebkitTextFillColor: c.textMuted,
102
+ }}>
103
+ {subtitle}
104
+ </div>
105
+ <div style={{
106
+ display: 'grid', gap: 14, textAlign: 'left',
107
+ background: c.bgSubtle,
108
+ border: `1px solid ${c.border}`,
109
+ borderRadius: 14, padding: '18px 20px',
110
+ }}>
111
+ {features.map(({ Icon, label }) => (
112
+ <div key={label} style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
113
+ <div style={{
114
+ width: 32, height: 32, borderRadius: 8,
115
+ background: c.accent,
116
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
117
+ flexShrink: 0,
118
+ }}>
119
+ <Icon size={18} color="#fff" strokeWidth={2.2} />
120
+ </div>
121
+ <span style={{ fontSize: 15, color: c.text, lineHeight: 1.4 }}>
122
+ {label}
123
+ </span>
124
+ </div>
125
+ ))}
126
+ </div>
127
+ </div>
128
+ );
129
+ };
130
+
131
+ const buildSteps = (c, data) => [
132
+ {
133
+ selector: 'body',
134
+ position: 'center',
135
+ content: <WelcomeBody c={c} title={data.title} subtitle={data.subtitle} features={data.features} />,
136
+ styles: {
137
+ maskArea: (base) => ({ ...base, x: -10000, y: -10000, width: 0, height: 0 }),
138
+ popover: (base) => ({
139
+ ...base,
140
+ maxWidth: 540,
141
+ minWidth: 460,
142
+ padding: '32px 30px 24px',
143
+ borderRadius: 22,
144
+ background: c.bg,
145
+ backgroundColor: c.bg,
146
+ color: c.text,
147
+ border: `1px solid ${c.border}`,
148
+ boxShadow: '0 30px 80px -10px rgba(0,0,0,0.5)',
149
+ }),
150
+ },
151
+ },
152
+ {
153
+ selector: '.advisor-status-button',
154
+ content: <StepBody c={c} Icon={GraduationCap} title="Meet your advisors" body={data.advisorBody} />,
155
+ },
156
+ {
157
+ selector: '.new-chat-button',
158
+ content: <StepBody c={c} Icon={Plus} title="Start a new chat" body="Begin a fresh conversation. Each chat is saved automatically so you can return to it later." />,
159
+ },
160
+ {
161
+ selector: '.sessions-list',
162
+ content: <StepBody c={c} Icon={MessageCircle} title="Your past chats" body="All your previous conversations live here. Click any session to pick up where you left off." />,
163
+ },
164
+ {
165
+ selector: '.enhanced-chat-input-container',
166
+ content: <StepBody c={c} Icon={Paperclip} title="Ask anything" body="Type a question, attach a PDF or document for context, and your advisors will respond with diverse perspectives." />,
167
+ },
168
+ {
169
+ selector: '.sidebar-canvas-btn',
170
+ content: <StepBody c={c} Icon={BarChart3} title={data.canvas.title} body={data.canvas.body} />,
171
+ },
172
+ ];
173
+
174
+ // --- Custom Buttons -----------------------------------------------------
175
+ const makeButtons = (c) => {
176
+ const ghostBtn = {
177
+ background: 'transparent', color: c.textMuted,
178
+ border: `1px solid ${c.border}`, padding: '10px 18px',
179
+ borderRadius: 10, fontSize: 14, fontWeight: 600, cursor: 'pointer',
180
+ };
181
+ const primaryBtn = {
182
+ background: c.accent, color: '#fff',
183
+ border: 'none', padding: '10px 22px',
184
+ borderRadius: 10, fontSize: 14, fontWeight: 600, cursor: 'pointer',
185
+ boxShadow: `0 4px 14px -4px ${c.accentShadow}`,
186
+ };
187
+
188
+ const PrevButton = ({ currentStep, setCurrentStep }) => {
189
+ if (currentStep === 0) return <span />;
190
+ return (
191
+ <button style={ghostBtn} onClick={() => setCurrentStep(currentStep - 1)}>Back</button>
192
+ );
193
+ };
194
+
195
+ const NextButton = ({ currentStep, stepsLength, setCurrentStep, setIsOpen }) => {
196
+ const isFirst = currentStep === 0;
197
+ const isLast = currentStep === stepsLength - 1;
198
+ const label = isFirst ? 'Begin tour' : isLast ? 'Get started' : 'Next';
199
+ const padded = isFirst ? { ...primaryBtn, padding: '12px 28px', fontSize: 15 } : primaryBtn;
200
+ return (
201
+ <button
202
+ style={padded}
203
+ onClick={() => (isLast ? setIsOpen(false) : setCurrentStep(currentStep + 1))}
204
+ >
205
+ {label}
206
+ </button>
207
+ );
208
+ };
209
+
210
+ const SkipButton = ({ onClick }) => (
211
+ <button
212
+ onClick={onClick}
213
+ style={{
214
+ position: 'absolute', top: 14, right: 16,
215
+ background: 'transparent', border: 'none',
216
+ color: c.textDim, fontSize: 13, fontWeight: 500,
217
+ cursor: 'pointer', padding: '4px 8px', borderRadius: 6,
218
+ }}
219
+ >
220
+ Skip tour
221
+ </button>
222
+ );
223
+
224
+ return { PrevButton, NextButton, SkipButton };
225
+ };
226
+
227
+ // Auto-opens the tour, writes flag on close.
228
+ const TourLauncher = () => {
229
+ const { setIsOpen, isOpen } = useTour();
230
+
231
+ useEffect(() => {
232
+ const seen = localStorage.getItem(STORAGE_KEY) === 'true';
233
+ if (TESTING_ONBOARDING || !seen) {
234
+ const t = setTimeout(() => setIsOpen(true), 400);
235
+ return () => clearTimeout(t);
236
+ }
237
+ }, [setIsOpen]);
238
+
239
+ useEffect(() => {
240
+ if (!isOpen) {
241
+ if (sessionStorage.getItem('__tourStarted__')) {
242
+ localStorage.setItem(STORAGE_KEY, 'true');
243
+ }
244
+ } else {
245
+ sessionStorage.setItem('__tourStarted__', '1');
246
+ }
247
+ }, [isOpen]);
248
+
249
+ return null;
250
+ };
251
+
252
+ const OnboardingTour = ({ children }) => {
253
+ const { isDark } = useTheme();
254
+ const { config, advisors, resolveIcon } = useAppConfig();
255
+ const c = useMemo(() => palette(isDark), [isDark]);
256
+
257
+ const stepData = useMemo(() => ({
258
+ title: config?.app?.title || 'PhD Advisory Panel',
259
+ subtitle: config?.app?.subtitle || 'AI-Powered Guidance',
260
+ features: buildFeatures(config, resolveIcon),
261
+ advisorBody: buildAdvisorBody(advisors),
262
+ canvas: {
263
+ title: config?.onboarding?.tour_title || DEFAULT_CANVAS_STEP.title,
264
+ body: config?.onboarding?.tour_body || DEFAULT_CANVAS_STEP.body,
265
+ },
266
+ }), [config, advisors, resolveIcon]);
267
+
268
+ const steps = useMemo(() => buildSteps(c, stepData), [c, stepData]);
269
+ const { PrevButton, NextButton, SkipButton } = useMemo(() => makeButtons(c), [c]);
270
+
271
+ return (
272
+ <TourProvider
273
+ steps={steps}
274
+ showBadge={false}
275
+ disableInteraction
276
+ prevButton={PrevButton}
277
+ nextButton={NextButton}
278
+ components={{ Close: SkipButton }}
279
+ padding={{ mask: 8, popover: [16, 12] }}
280
+ styles={{
281
+ popover: (base) => ({
282
+ ...base,
283
+ borderRadius: 18,
284
+ padding: '28px 26px 22px',
285
+ maxWidth: 440,
286
+ minWidth: 360,
287
+ background: c.bg,
288
+ backgroundColor: c.bg,
289
+ color: c.text,
290
+ boxShadow: '0 30px 80px -10px rgba(0,0,0,0.5)',
291
+ border: `1px solid ${c.border}`,
292
+ }),
293
+ maskWrapper: (base) => ({ ...base, color: 'rgba(0, 0, 0, 0.82)' }),
294
+ maskArea: (base) => ({ ...base, rx: 14 }),
295
+ controls: (base) => ({
296
+ ...base,
297
+ marginTop: 22,
298
+ paddingTop: 18,
299
+ borderTop: `1px solid ${c.border}`,
300
+ alignItems: 'center',
301
+ justifyContent: 'space-between',
302
+ }),
303
+ dot: (base, { current }) => ({
304
+ ...base,
305
+ width: current ? 24 : 8,
306
+ height: 8,
307
+ borderRadius: 999,
308
+ background: current ? c.accent : c.border,
309
+ transition: 'all 0.25s ease',
310
+ }),
311
+ navigation: (base) => ({ ...base, gap: 7 }),
312
+ }}
313
+ >
314
+ <TourLauncher />
315
+ {children}
316
+ </TourProvider>
317
+ );
318
+ };
319
+
320
+ export default OnboardingTour;
phd-advisor-frontend/src/pages/ChatPage.js CHANGED
@@ -16,6 +16,7 @@ import '../styles/ChatPage.css';
16
  import '../styles/EnhancedChatInput.css';
17
  import AdvisorStatusDropdown from '../components/AdvisorStatusDropdown';
18
  import AdvisorCarousel from '../components/AdvisorCarousel';
 
19
 
20
  const ChatPage = ({ user, authToken, onNavigateToHome, onNavigateToCanvas, onSignOut, onUserUpdate }) => {
21
  const { config, advisors, getAdvisorColors } = useAppConfig();
@@ -752,6 +753,7 @@ const handleNewChat = async (sessionId = null) => {
752
  const chatPlaceholder = config?.chat_page?.placeholder || "Ask your advisors anything...";
753
 
754
  return (
 
755
  <div className="chat-page-with-sidebar">
756
  {/* Sidebar Component */}
757
  <Sidebar
@@ -998,6 +1000,7 @@ const handleNewChat = async (sessionId = null) => {
998
  </div>
999
  </div>
1000
  </div>
 
1001
  );
1002
  };
1003
 
 
16
  import '../styles/EnhancedChatInput.css';
17
  import AdvisorStatusDropdown from '../components/AdvisorStatusDropdown';
18
  import AdvisorCarousel from '../components/AdvisorCarousel';
19
+ import OnboardingTour from '../components/OnboardingTour';
20
 
21
  const ChatPage = ({ user, authToken, onNavigateToHome, onNavigateToCanvas, onSignOut, onUserUpdate }) => {
22
  const { config, advisors, getAdvisorColors } = useAppConfig();
 
753
  const chatPlaceholder = config?.chat_page?.placeholder || "Ask your advisors anything...";
754
 
755
  return (
756
+ <OnboardingTour>
757
  <div className="chat-page-with-sidebar">
758
  {/* Sidebar Component */}
759
  <Sidebar
 
1000
  </div>
1001
  </div>
1002
  </div>
1003
+ </OnboardingTour>
1004
  );
1005
  };
1006
 
phd-advisor-frontend/src/styles/OnboardingTour.css ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Force popover to match app theme via CSS vars set on :root[data-theme] */
2
+ [data-tour-elem="popover"] {
3
+ background: var(--bg-primary) !important;
4
+ background-color: var(--bg-primary) !important;
5
+ color: var(--text-primary) !important;
6
+ border: 1px solid var(--border-primary) !important;
7
+ }
8
+
9
+ /* All text inside should respect theme */
10
+ [data-tour-elem="popover"] * {
11
+ -webkit-text-fill-color: inherit;
12
+ }
phd_config.yaml CHANGED
@@ -84,6 +84,19 @@ chat_page:
84
  - "Should I speak up about unclear expectations or just try to figure it out quietly?"
85
  - "What are the unspoken expectations no one tells you about?"
86
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  # ── Personas ───────────────────────────────────────────────────────────────
88
 
89
  personas:
 
84
  - "Should I speak up about unclear expectations or just try to figure it out quietly?"
85
  - "What are the unspoken expectations no one tells you about?"
86
 
87
+ onboarding:
88
+ features:
89
+ - title: "Get advice from specialized AI advisors"
90
+ icon: "GraduationCap"
91
+ - title: "Save and revisit every conversation"
92
+ icon: "MessageCircle"
93
+ - title: "Upload PDFs for context-aware answers"
94
+ icon: "Paperclip"
95
+ - title: "Track your PhD progress on a structured canvas"
96
+ icon: "BarChart3"
97
+ tour_title: "PhD Progress Canvas"
98
+ tour_body: "A dashboard view of your PhD journey — research progress, methodology, next steps, all in one place."
99
+
100
  # ── Personas ───────────────────────────────────────────────────────────────
101
 
102
  personas:
undergrad_config.yaml CHANGED
@@ -86,6 +86,21 @@ chat_page:
86
  - "How do I balance self-care with a heavy course load?"
87
  - "What campus resources exist for mental health, tutoring, or financial aid?"
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  # -- Personas ----------------------------------------------------------------
90
 
91
  personas:
 
86
  - "How do I balance self-care with a heavy course load?"
87
  - "What campus resources exist for mental health, tutoring, or financial aid?"
88
 
89
+ onboarding:
90
+ features:
91
+ - title: "Get advice from specialized AI advisors"
92
+ icon: "GraduationCap"
93
+ - title: "Save and revisit every conversation"
94
+ icon: "MessageCircle"
95
+ - title: "Upload course syllabi and documents for context-aware answers"
96
+ icon: "Paperclip"
97
+ - title: "Track your progress on a structured canvas"
98
+ icon: "BarChart3"
99
+
100
+ canvas:
101
+ tour_title: "Academic Progress Canvas"
102
+ tour_body: "A dashboard view of your college journey -- courses, milestones, next steps, all in one place."
103
+
104
  # -- Personas ----------------------------------------------------------------
105
 
106
  personas: