SarahXia0405 commited on
Commit
236f183
·
verified ·
1 Parent(s): 3e0f21f

Update web/src/App.tsx

Browse files
Files changed (1) hide show
  1. web/src/App.tsx +116 -187
web/src/App.tsx CHANGED
@@ -21,8 +21,8 @@ export interface Message {
21
  content: string;
22
  timestamp: Date;
23
  references?: string[];
24
- sender?: GroupMember; // For group chat
25
- showNextButton?: boolean; // For quiz mode
26
  questionData?: {
27
  type: "multiple-choice" | "fill-in-blank" | "open-ended";
28
  question: string;
@@ -51,14 +51,8 @@ export type SpaceType = "individual" | "group";
51
  export interface CourseInfo {
52
  id: string;
53
  name: string;
54
- instructor: {
55
- name: string;
56
- email: string;
57
- };
58
- teachingAssistant: {
59
- name: string;
60
- email: string;
61
- };
62
  }
63
 
64
  export interface Workspace {
@@ -70,7 +64,7 @@ export interface Workspace {
70
  category?: "course" | "personal";
71
  courseName?: string;
72
  courseInfo?: CourseInfo;
73
- isEditable?: boolean; // For personal interest workspaces
74
  }
75
 
76
  export type FileType = "syllabus" | "lecture-slides" | "literature-review" | "other";
@@ -124,10 +118,8 @@ function App() {
124
 
125
  const [user, setUser] = useState<User | null>(null);
126
 
127
- // Global current course selection (My Space only)
128
  const [currentCourseId, setCurrentCourseId] = useState<string>(() => localStorage.getItem("myspace_selected_course") || "course1");
129
 
130
- // Available courses with instructor/TA info
131
  const availableCourses: CourseInfo[] = [
132
  {
133
  id: "course1",
@@ -155,7 +147,6 @@ function App() {
155
  },
156
  ];
157
 
158
- // Separate messages for each chat mode
159
  const [askMessages, setAskMessages] = useState<Message[]>([
160
  {
161
  id: "1",
@@ -192,7 +183,6 @@ function App() {
192
 
193
  const prevChatModeRef = useRef<ChatMode>(chatMode);
194
 
195
- // Ensure welcome message exists when switching modes or when messages are empty
196
  useEffect(() => {
197
  let currentMessages: Message[];
198
  let setCurrentMessages: (messages: Message[]) => void;
@@ -267,21 +257,14 @@ function App() {
267
  const [showProfileEditor, setShowProfileEditor] = useState(false);
268
  const [showOnboarding, setShowOnboarding] = useState(false);
269
 
270
- // Review banner state
271
- const [showReviewBanner, setShowReviewBanner] = useState(() => {
272
- return true; // force show for testing (as you had)
273
- });
274
-
275
  const [showClearDialog, setShowClearDialog] = useState(false);
276
 
277
- // Saved conversations/summaries
278
  const [savedItems, setSavedItems] = useState<SavedItem[]>([]);
279
  const [recentlySavedId, setRecentlySavedId] = useState<string | null>(null);
280
 
281
- // Saved chats
282
  const [savedChats, setSavedChats] = useState<SavedChat[]>([]);
283
 
284
- // Mock group members
285
  const [groupMembers] = useState<GroupMember[]>([
286
  { id: "clare", name: "Clare AI", email: "clare@ai.assistant", isAI: true },
287
  { id: "1", name: "Sarah Johnson", email: "sarah.j@university.edu" },
@@ -289,27 +272,19 @@ function App() {
289
  { id: "3", name: "Emma Williams", email: "emma.w@university.edu" },
290
  ]);
291
 
292
- // Workspaces
293
  const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
294
  const [currentWorkspaceId, setCurrentWorkspaceId] = useState<string>("individual");
295
 
296
- // Avoid double upload for same file
297
  const uploadedFingerprintsRef = useRef<Set<string>>(new Set());
298
 
299
- // Initialize workspaces when user logs in
300
  useEffect(() => {
301
  if (user) {
302
  const userAvatar = `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}`;
303
  const course1Info = availableCourses.find((c) => c.id === "course1");
304
- const course2Info = availableCourses.find((c) => c.name === "AI Ethics"); // may be undefined
305
 
306
  setWorkspaces([
307
- {
308
- id: "individual",
309
- name: "My Space",
310
- type: "individual",
311
- avatar: userAvatar,
312
- },
313
  {
314
  id: "group-1",
315
  name: "CS 101 Study Group",
@@ -334,16 +309,13 @@ function App() {
334
  }
335
  }, [user, groupMembers, availableCourses]);
336
 
337
- // Get current workspace
338
  const currentWorkspace = workspaces.find((w) => w.id === currentWorkspaceId) || workspaces[0];
339
  const spaceType: SpaceType = currentWorkspace?.type || "individual";
340
 
341
- // Keep current course in sync with workspace type
342
  useEffect(() => {
343
  if (!currentWorkspace) return;
344
 
345
  if (currentWorkspace.type === "group" && currentWorkspace.category === "course") {
346
- // ✅ FIX: keep courseId as id (not name)
347
  const cid = currentWorkspace.courseInfo?.id;
348
  if (cid) setCurrentCourseId(cid);
349
  return;
@@ -355,7 +327,6 @@ function App() {
355
  }
356
  }, [currentWorkspaceId, currentWorkspace]);
357
 
358
- // Persist selection for My Space
359
  useEffect(() => {
360
  if (currentWorkspace?.type === "individual") {
361
  localStorage.setItem("myspace_selected_course", currentCourseId);
@@ -367,7 +338,15 @@ function App() {
367
  localStorage.setItem("theme", isDarkMode ? "dark" : "light");
368
  }, [isDarkMode]);
369
 
370
- // Pull memoryline progress (optional, but makes the UI real)
 
 
 
 
 
 
 
 
371
  useEffect(() => {
372
  if (!user) return;
373
 
@@ -377,7 +356,7 @@ function App() {
377
  const pct = Math.round((r.progress_pct ?? 0) * 100);
378
  setMemoryProgress(pct);
379
  } catch {
380
- // silent; do not block UI
381
  }
382
  })();
383
  }, [user]);
@@ -421,9 +400,6 @@ function App() {
421
  return questions[randomIndex];
422
  };
423
 
424
- // Pick doc_type for chat:
425
- // - if any uploaded files: use the most recent file's selected type
426
- // - else: default to Syllabus
427
  const getCurrentDocTypeForChat = (): string => {
428
  if (uploadedFiles.length > 0) {
429
  const last = uploadedFiles[uploadedFiles.length - 1];
@@ -450,15 +426,10 @@ function App() {
450
  sender,
451
  };
452
 
453
- if (chatMode === "ask") {
454
- setAskMessages((prev) => [...prev, userMessage]);
455
- } else if (chatMode === "review") {
456
- setReviewMessages((prev) => [...prev, userMessage]);
457
- } else {
458
- setQuizMessages((prev) => [...prev, userMessage]);
459
- }
460
 
461
- // Quiz mode stays mock for now (your current behavior)
462
  if (chatMode === "quiz") {
463
  if (quizState.waitingForAnswer) {
464
  const isCorrect = Math.random() > 0.3;
@@ -488,7 +459,6 @@ function App() {
488
  return;
489
  }
490
 
491
- // Ask / Review: real backend call
492
  setIsTyping(true);
493
  try {
494
  const docType = getCurrentDocTypeForChat();
@@ -522,14 +492,10 @@ function App() {
522
  setIsTyping(false);
523
 
524
  setTimeout(() => {
525
- if (chatMode === "ask") {
526
- setAskMessages((prev) => [...prev, assistantMessage]);
527
- } else if (chatMode === "review") {
528
- setReviewMessages((prev) => [...prev, assistantMessage]);
529
- }
530
  }, 50);
531
 
532
- // refresh memory progress (best-effort)
533
  try {
534
  const ml = await apiMemoryline(user.email);
535
  setMemoryProgress(Math.round((ml.progress_pct ?? 0) * 100));
@@ -588,15 +554,10 @@ function App() {
588
  }, 2000);
589
  };
590
 
591
- const handleStartQuiz = () => {
592
- handleNextQuestion();
593
- };
594
 
595
  const handleFileUpload = (files: File[]) => {
596
- const newFiles: UploadedFile[] = files.map((file) => ({
597
- file,
598
- type: "other" as FileType, // default; user will confirm type in ChatArea dialog
599
- }));
600
  setUploadedFiles((prev) => [...prev, ...newFiles]);
601
  };
602
 
@@ -605,10 +566,8 @@ function App() {
605
  };
606
 
607
  const handleFileTypeChange = async (index: number, type: FileType) => {
608
- // update FE state first
609
  setUploadedFiles((prev) => prev.map((file, i) => (i === index ? { ...file, type } : file)));
610
 
611
- // upload to backend once (best effort)
612
  if (!user) return;
613
 
614
  const target = uploadedFiles[index];
@@ -632,7 +591,6 @@ function App() {
632
  }
633
  };
634
 
635
- // Helper function to check if current chat is already saved
636
  const isCurrentChatSaved = (): SavedChat | null => {
637
  if (messages.length <= 1) return null;
638
 
@@ -679,17 +637,11 @@ function App() {
679
  const handleLoadChat = (savedChat: SavedChat) => {
680
  setChatMode(savedChat.chatMode);
681
 
682
- if (savedChat.chatMode === "ask") {
683
- setAskMessages(savedChat.messages);
684
- } else if (savedChat.chatMode === "review") {
685
- setReviewMessages(savedChat.messages);
686
- } else {
687
  setQuizMessages(savedChat.messages);
688
- setQuizState({
689
- currentQuestion: 0,
690
- waitingForAnswer: false,
691
- showNextButton: false,
692
- });
693
  }
694
 
695
  toast.success("Chat loaded!");
@@ -706,9 +658,7 @@ function App() {
706
  };
707
 
708
  const handleClearConversation = (shouldSave: boolean = false) => {
709
- if (shouldSave) {
710
- handleSaveChat();
711
- }
712
 
713
  const initialMessages: Record<ChatMode, Message[]> = {
714
  ask: [
@@ -738,17 +688,11 @@ function App() {
738
  ],
739
  };
740
 
741
- if (chatMode === "ask") {
742
- setAskMessages(initialMessages.ask);
743
- } else if (chatMode === "review") {
744
- setReviewMessages(initialMessages.review);
745
- } else {
746
  setQuizMessages(initialMessages.quiz);
747
- setQuizState({
748
- currentQuestion: 0,
749
- waitingForAnswer: false,
750
- showNextButton: false,
751
- });
752
  }
753
  };
754
 
@@ -770,12 +714,7 @@ function App() {
770
  "👋 Hi! I'm Clare, your AI teaching assistant. I'm here to help you learn through personalized tutoring. Feel free to ask me anything about the course materials, or upload your documents to get started!",
771
  timestamp: new Date(),
772
  },
773
- {
774
- id: Date.now().toString(),
775
- role: "assistant",
776
- content,
777
- timestamp: new Date(),
778
- },
779
  ];
780
 
781
  const title = type === "export" ? "Exported Conversation" : "Micro-Quiz";
@@ -815,10 +754,7 @@ function App() {
815
  setRecentlySavedId(newItem.id);
816
  setLeftPanelVisible(true);
817
 
818
- setTimeout(() => {
819
- setRecentlySavedId(null);
820
- }, 2000);
821
-
822
  toast.success("Saved for later!");
823
  };
824
 
@@ -827,7 +763,6 @@ function App() {
827
  toast.success("Removed from saved items");
828
  };
829
 
830
- // Create a new group workspace
831
  const handleCreateWorkspace = (payload: { name: string; category: "course" | "personal"; courseId?: string; invites: string[] }) => {
832
  const id = `group-${Date.now()}`;
833
  const avatar = `https://api.dicebear.com/7.x/shapes/svg?seed=${encodeURIComponent(payload.name)}`;
@@ -907,48 +842,46 @@ function App() {
907
  setShowOnboarding(false);
908
  };
909
 
910
- const handleOnboardingSkip = () => {
911
- setShowOnboarding(false);
912
- };
913
-
914
- if (!user) {
915
- return <LoginScreen onLogin={handleLogin} />;
916
- }
917
 
918
- if (showOnboarding && user) {
919
- return <Onboarding user={user} onComplete={handleOnboardingComplete} onSkip={handleOnboardingSkip} />;
920
- }
921
 
922
  return (
923
- <div className="min-h-screen bg-background flex flex-col">
924
  <Toaster />
925
 
926
- <Header
927
- user={user}
928
- onMenuClick={() => setLeftSidebarOpen(!leftSidebarOpen)}
929
- onUserClick={() => {}}
930
- isDarkMode={isDarkMode}
931
- onToggleDarkMode={() => setIsDarkMode(!isDarkMode)}
932
- language={language}
933
- onLanguageChange={setLanguage}
934
- workspaces={workspaces}
935
- currentWorkspace={currentWorkspace}
936
- onWorkspaceChange={setCurrentWorkspaceId}
937
- onCreateWorkspace={handleCreateWorkspace}
938
- onLogout={() => setUser(null)}
939
- availableCourses={availableCourses}
940
- onUserUpdate={setUser}
941
- />
 
 
 
942
 
943
  {showProfileEditor && user && <ProfileEditor user={user} onSave={setUser} onClose={() => setShowProfileEditor(false)} />}
944
 
 
945
  {showReviewBanner && (
946
- <div className="w-full bg-background border-b border-border flex-shrink-0 relative z-50">
947
  <ReviewBanner onReview={handleReviewClick} onDismiss={handleDismissReviewBanner} />
948
  </div>
949
  )}
950
 
951
- <div className="flex-1 flex overflow-hidden h-[calc(100vh-4rem)] relative" style={{ overscrollBehavior: "none" }}>
 
952
  {!leftPanelVisible && (
953
  <Button
954
  variant="secondary"
@@ -964,11 +897,9 @@ function App() {
964
 
965
  {leftSidebarOpen && <div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setLeftSidebarOpen(false)} />}
966
 
 
967
  {leftPanelVisible ? (
968
- <aside
969
- className="hidden lg:flex w-80 bg-card border-r border-border flex-col h-full min-h-0 relative"
970
- style={{ borderRight: "1px solid var(--border)", height: "calc(100vh - 4rem)" }}
971
- >
972
  <Button
973
  variant="secondary"
974
  size="icon"
@@ -980,58 +911,7 @@ function App() {
980
  <ChevronLeft className="h-3 w-3" />
981
  </Button>
982
 
983
- {/* ✅ KEY: isolate scroll container for LeftSidebar */}
984
- <div className="flex-1 min-h-0 overflow-y-auto">
985
- <LeftSidebar
986
- learningMode={learningMode}
987
- language={language}
988
- onLearningModeChange={setLearningMode}
989
- onLanguageChange={setLanguage}
990
- spaceType={spaceType}
991
- groupMembers={groupMembers}
992
- user={user}
993
- onLogin={setUser}
994
- onLogout={() => setUser(null)}
995
- isLoggedIn={!!user}
996
- onEditProfile={() => setShowProfileEditor(true)}
997
- savedItems={savedItems}
998
- recentlySavedId={recentlySavedId}
999
- onUnsave={handleUnsave}
1000
- onSave={handleSave}
1001
- savedChats={savedChats}
1002
- onLoadChat={handleLoadChat}
1003
- onDeleteSavedChat={handleDeleteSavedChat}
1004
- onRenameSavedChat={handleRenameSavedChat}
1005
- currentWorkspaceId={currentWorkspaceId}
1006
- workspaces={workspaces}
1007
- selectedCourse={currentCourseId}
1008
- availableCourses={availableCourses}
1009
- />
1010
- </div>
1011
- </aside>
1012
- ) : null}
1013
-
1014
- <aside
1015
- className={`
1016
- fixed lg:hidden inset-y-0 left-0 z-50
1017
- w-80 bg-card border-r border-border
1018
- transform transition-transform duration-300 ease-in-out
1019
- ${leftSidebarOpen ? "translate-x-0" : "-translate-x-full"}
1020
- flex flex-col
1021
- mt-16
1022
- h-[calc(100vh-4rem)]
1023
- min-h-0
1024
- `}
1025
- >
1026
- <div className="p-4 border-b border-border flex justify-between items-center">
1027
- <h3>Settings & Guide</h3>
1028
- <Button variant="ghost" size="icon" onClick={() => setLeftSidebarOpen(false)}>
1029
- <X className="h-5 w-5" />
1030
- </Button>
1031
- </div>
1032
-
1033
- {/* ✅ KEY: isolate scroll container for mobile LeftSidebar */}
1034
- <div className="flex-1 min-h-0 overflow-y-auto">
1035
  <LeftSidebar
1036
  learningMode={learningMode}
1037
  language={language}
@@ -1051,15 +931,64 @@ function App() {
1051
  savedChats={savedChats}
1052
  onLoadChat={handleLoadChat}
1053
  onDeleteSavedChat={handleDeleteSavedChat}
 
1054
  currentWorkspaceId={currentWorkspaceId}
1055
  workspaces={workspaces}
1056
  selectedCourse={currentCourseId}
1057
  availableCourses={availableCourses}
1058
  />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1059
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1060
  </aside>
1061
 
1062
- <main className="flex-1 flex flex-col min-w-0 min-h-0 h-full">
 
1063
  <ChatArea
1064
  messages={messages}
1065
  onSendMessage={handleSendMessage}
 
21
  content: string;
22
  timestamp: Date;
23
  references?: string[];
24
+ sender?: GroupMember;
25
+ showNextButton?: boolean;
26
  questionData?: {
27
  type: "multiple-choice" | "fill-in-blank" | "open-ended";
28
  question: string;
 
51
  export interface CourseInfo {
52
  id: string;
53
  name: string;
54
+ instructor: { name: string; email: string };
55
+ teachingAssistant: { name: string; email: string };
 
 
 
 
 
 
56
  }
57
 
58
  export interface Workspace {
 
64
  category?: "course" | "personal";
65
  courseName?: string;
66
  courseInfo?: CourseInfo;
67
+ isEditable?: boolean;
68
  }
69
 
70
  export type FileType = "syllabus" | "lecture-slides" | "literature-review" | "other";
 
118
 
119
  const [user, setUser] = useState<User | null>(null);
120
 
 
121
  const [currentCourseId, setCurrentCourseId] = useState<string>(() => localStorage.getItem("myspace_selected_course") || "course1");
122
 
 
123
  const availableCourses: CourseInfo[] = [
124
  {
125
  id: "course1",
 
147
  },
148
  ];
149
 
 
150
  const [askMessages, setAskMessages] = useState<Message[]>([
151
  {
152
  id: "1",
 
183
 
184
  const prevChatModeRef = useRef<ChatMode>(chatMode);
185
 
 
186
  useEffect(() => {
187
  let currentMessages: Message[];
188
  let setCurrentMessages: (messages: Message[]) => void;
 
257
  const [showProfileEditor, setShowProfileEditor] = useState(false);
258
  const [showOnboarding, setShowOnboarding] = useState(false);
259
 
260
+ const [showReviewBanner, setShowReviewBanner] = useState(() => true);
 
 
 
 
261
  const [showClearDialog, setShowClearDialog] = useState(false);
262
 
 
263
  const [savedItems, setSavedItems] = useState<SavedItem[]>([]);
264
  const [recentlySavedId, setRecentlySavedId] = useState<string | null>(null);
265
 
 
266
  const [savedChats, setSavedChats] = useState<SavedChat[]>([]);
267
 
 
268
  const [groupMembers] = useState<GroupMember[]>([
269
  { id: "clare", name: "Clare AI", email: "clare@ai.assistant", isAI: true },
270
  { id: "1", name: "Sarah Johnson", email: "sarah.j@university.edu" },
 
272
  { id: "3", name: "Emma Williams", email: "emma.w@university.edu" },
273
  ]);
274
 
 
275
  const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
276
  const [currentWorkspaceId, setCurrentWorkspaceId] = useState<string>("individual");
277
 
 
278
  const uploadedFingerprintsRef = useRef<Set<string>>(new Set());
279
 
 
280
  useEffect(() => {
281
  if (user) {
282
  const userAvatar = `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}`;
283
  const course1Info = availableCourses.find((c) => c.id === "course1");
284
+ const course2Info = availableCourses.find((c) => c.name === "AI Ethics");
285
 
286
  setWorkspaces([
287
+ { id: "individual", name: "My Space", type: "individual", avatar: userAvatar },
 
 
 
 
 
288
  {
289
  id: "group-1",
290
  name: "CS 101 Study Group",
 
309
  }
310
  }, [user, groupMembers, availableCourses]);
311
 
 
312
  const currentWorkspace = workspaces.find((w) => w.id === currentWorkspaceId) || workspaces[0];
313
  const spaceType: SpaceType = currentWorkspace?.type || "individual";
314
 
 
315
  useEffect(() => {
316
  if (!currentWorkspace) return;
317
 
318
  if (currentWorkspace.type === "group" && currentWorkspace.category === "course") {
 
319
  const cid = currentWorkspace.courseInfo?.id;
320
  if (cid) setCurrentCourseId(cid);
321
  return;
 
327
  }
328
  }, [currentWorkspaceId, currentWorkspace]);
329
 
 
330
  useEffect(() => {
331
  if (currentWorkspace?.type === "individual") {
332
  localStorage.setItem("myspace_selected_course", currentCourseId);
 
338
  localStorage.setItem("theme", isDarkMode ? "dark" : "light");
339
  }, [isDarkMode]);
340
 
341
+ // 额外保险:锁 body 滚动(避免某些情况下仍出现页面滚动条)
342
+ useEffect(() => {
343
+ const prev = document.body.style.overflow;
344
+ document.body.style.overflow = "hidden";
345
+ return () => {
346
+ document.body.style.overflow = prev;
347
+ };
348
+ }, []);
349
+
350
  useEffect(() => {
351
  if (!user) return;
352
 
 
356
  const pct = Math.round((r.progress_pct ?? 0) * 100);
357
  setMemoryProgress(pct);
358
  } catch {
359
+ // silent
360
  }
361
  })();
362
  }, [user]);
 
400
  return questions[randomIndex];
401
  };
402
 
 
 
 
403
  const getCurrentDocTypeForChat = (): string => {
404
  if (uploadedFiles.length > 0) {
405
  const last = uploadedFiles[uploadedFiles.length - 1];
 
426
  sender,
427
  };
428
 
429
+ if (chatMode === "ask") setAskMessages((prev) => [...prev, userMessage]);
430
+ else if (chatMode === "review") setReviewMessages((prev) => [...prev, userMessage]);
431
+ else setQuizMessages((prev) => [...prev, userMessage]);
 
 
 
 
432
 
 
433
  if (chatMode === "quiz") {
434
  if (quizState.waitingForAnswer) {
435
  const isCorrect = Math.random() > 0.3;
 
459
  return;
460
  }
461
 
 
462
  setIsTyping(true);
463
  try {
464
  const docType = getCurrentDocTypeForChat();
 
492
  setIsTyping(false);
493
 
494
  setTimeout(() => {
495
+ if (chatMode === "ask") setAskMessages((prev) => [...prev, assistantMessage]);
496
+ else if (chatMode === "review") setReviewMessages((prev) => [...prev, assistantMessage]);
 
 
 
497
  }, 50);
498
 
 
499
  try {
500
  const ml = await apiMemoryline(user.email);
501
  setMemoryProgress(Math.round((ml.progress_pct ?? 0) * 100));
 
554
  }, 2000);
555
  };
556
 
557
+ const handleStartQuiz = () => handleNextQuestion();
 
 
558
 
559
  const handleFileUpload = (files: File[]) => {
560
+ const newFiles: UploadedFile[] = files.map((file) => ({ file, type: "other" as FileType }));
 
 
 
561
  setUploadedFiles((prev) => [...prev, ...newFiles]);
562
  };
563
 
 
566
  };
567
 
568
  const handleFileTypeChange = async (index: number, type: FileType) => {
 
569
  setUploadedFiles((prev) => prev.map((file, i) => (i === index ? { ...file, type } : file)));
570
 
 
571
  if (!user) return;
572
 
573
  const target = uploadedFiles[index];
 
591
  }
592
  };
593
 
 
594
  const isCurrentChatSaved = (): SavedChat | null => {
595
  if (messages.length <= 1) return null;
596
 
 
637
  const handleLoadChat = (savedChat: SavedChat) => {
638
  setChatMode(savedChat.chatMode);
639
 
640
+ if (savedChat.chatMode === "ask") setAskMessages(savedChat.messages);
641
+ else if (savedChat.chatMode === "review") setReviewMessages(savedChat.messages);
642
+ else {
 
 
643
  setQuizMessages(savedChat.messages);
644
+ setQuizState({ currentQuestion: 0, waitingForAnswer: false, showNextButton: false });
 
 
 
 
645
  }
646
 
647
  toast.success("Chat loaded!");
 
658
  };
659
 
660
  const handleClearConversation = (shouldSave: boolean = false) => {
661
+ if (shouldSave) handleSaveChat();
 
 
662
 
663
  const initialMessages: Record<ChatMode, Message[]> = {
664
  ask: [
 
688
  ],
689
  };
690
 
691
+ if (chatMode === "ask") setAskMessages(initialMessages.ask);
692
+ else if (chatMode === "review") setReviewMessages(initialMessages.review);
693
+ else {
 
 
694
  setQuizMessages(initialMessages.quiz);
695
+ setQuizState({ currentQuestion: 0, waitingForAnswer: false, showNextButton: false });
 
 
 
 
696
  }
697
  };
698
 
 
714
  "👋 Hi! I'm Clare, your AI teaching assistant. I'm here to help you learn through personalized tutoring. Feel free to ask me anything about the course materials, or upload your documents to get started!",
715
  timestamp: new Date(),
716
  },
717
+ { id: Date.now().toString(), role: "assistant", content, timestamp: new Date() },
 
 
 
 
 
718
  ];
719
 
720
  const title = type === "export" ? "Exported Conversation" : "Micro-Quiz";
 
754
  setRecentlySavedId(newItem.id);
755
  setLeftPanelVisible(true);
756
 
757
+ setTimeout(() => setRecentlySavedId(null), 2000);
 
 
 
758
  toast.success("Saved for later!");
759
  };
760
 
 
763
  toast.success("Removed from saved items");
764
  };
765
 
 
766
  const handleCreateWorkspace = (payload: { name: string; category: "course" | "personal"; courseId?: string; invites: string[] }) => {
767
  const id = `group-${Date.now()}`;
768
  const avatar = `https://api.dicebear.com/7.x/shapes/svg?seed=${encodeURIComponent(payload.name)}`;
 
842
  setShowOnboarding(false);
843
  };
844
 
845
+ const handleOnboardingSkip = () => setShowOnboarding(false);
 
 
 
 
 
 
846
 
847
+ if (!user) return <LoginScreen onLogin={handleLogin} />;
848
+ if (showOnboarding && user) return <Onboarding user={user} onComplete={handleOnboardingComplete} onSkip={handleOnboardingSkip} />;
 
849
 
850
  return (
851
+ <div className="h-screen overflow-hidden bg-background flex flex-col">
852
  <Toaster />
853
 
854
+ {/* Header fixed */}
855
+ <div className="flex-shrink-0">
856
+ <Header
857
+ user={user}
858
+ onMenuClick={() => setLeftSidebarOpen(!leftSidebarOpen)}
859
+ onUserClick={() => {}}
860
+ isDarkMode={isDarkMode}
861
+ onToggleDarkMode={() => setIsDarkMode(!isDarkMode)}
862
+ language={language}
863
+ onLanguageChange={setLanguage}
864
+ workspaces={workspaces}
865
+ currentWorkspace={currentWorkspace}
866
+ onWorkspaceChange={setCurrentWorkspaceId}
867
+ onCreateWorkspace={handleCreateWorkspace}
868
+ onLogout={() => setUser(null)}
869
+ availableCourses={availableCourses}
870
+ onUserUpdate={setUser}
871
+ />
872
+ </div>
873
 
874
  {showProfileEditor && user && <ProfileEditor user={user} onSave={setUser} onClose={() => setShowProfileEditor(false)} />}
875
 
876
+ {/* Review banner fixed */}
877
  {showReviewBanner && (
878
+ <div className="flex-shrink-0 w-full bg-background border-b border-border relative z-50">
879
  <ReviewBanner onReview={handleReviewClick} onDismiss={handleDismissReviewBanner} />
880
  </div>
881
  )}
882
 
883
+ {/* Main area: NO page scroll, only inner panes scroll */}
884
+ <div className="flex-1 min-h-0 flex overflow-hidden relative" style={{ overscrollBehavior: "none" }}>
885
  {!leftPanelVisible && (
886
  <Button
887
  variant="secondary"
 
897
 
898
  {leftSidebarOpen && <div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setLeftSidebarOpen(false)} />}
899
 
900
+ {/* Desktop left panel */}
901
  {leftPanelVisible ? (
902
+ <aside className="hidden lg:flex w-80 bg-card border-r border-border flex-col min-h-0 overflow-hidden relative">
 
 
 
903
  <Button
904
  variant="secondary"
905
  size="icon"
 
911
  <ChevronLeft className="h-3 w-3" />
912
  </Button>
913
 
914
+ {/* IMPORTANT: do NOT wrap LeftSidebar with another overflow container */}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
915
  <LeftSidebar
916
  learningMode={learningMode}
917
  language={language}
 
931
  savedChats={savedChats}
932
  onLoadChat={handleLoadChat}
933
  onDeleteSavedChat={handleDeleteSavedChat}
934
+ onRenameSavedChat={handleRenameSavedChat}
935
  currentWorkspaceId={currentWorkspaceId}
936
  workspaces={workspaces}
937
  selectedCourse={currentCourseId}
938
  availableCourses={availableCourses}
939
  />
940
+ </aside>
941
+ ) : null}
942
+
943
+ {/* Mobile left drawer */}
944
+ <aside
945
+ className={`
946
+ fixed lg:hidden inset-y-0 left-0 z-50
947
+ w-80 bg-card border-r border-border
948
+ transform transition-transform duration-300 ease-in-out
949
+ ${leftSidebarOpen ? "translate-x-0" : "-translate-x-full"}
950
+ flex flex-col
951
+ mt-16
952
+ h-[calc(100vh-4rem)]
953
+ min-h-0
954
+ overflow-hidden
955
+ `}
956
+ >
957
+ <div className="p-4 border-b border-border flex justify-between items-center flex-shrink-0">
958
+ <h3>Settings & Guide</h3>
959
+ <Button variant="ghost" size="icon" onClick={() => setLeftSidebarOpen(false)}>
960
+ <X className="h-5 w-5" />
961
+ </Button>
962
  </div>
963
+
964
+ <LeftSidebar
965
+ learningMode={learningMode}
966
+ language={language}
967
+ onLearningModeChange={setLearningMode}
968
+ onLanguageChange={setLanguage}
969
+ spaceType={spaceType}
970
+ groupMembers={groupMembers}
971
+ user={user}
972
+ onLogin={setUser}
973
+ onLogout={() => setUser(null)}
974
+ isLoggedIn={!!user}
975
+ onEditProfile={() => setShowProfileEditor(true)}
976
+ savedItems={savedItems}
977
+ recentlySavedId={recentlySavedId}
978
+ onUnsave={handleUnsave}
979
+ onSave={handleSave}
980
+ savedChats={savedChats}
981
+ onLoadChat={handleLoadChat}
982
+ onDeleteSavedChat={handleDeleteSavedChat}
983
+ currentWorkspaceId={currentWorkspaceId}
984
+ workspaces={workspaces}
985
+ selectedCourse={currentCourseId}
986
+ availableCourses={availableCourses}
987
+ />
988
  </aside>
989
 
990
+ {/* Chat column: overflow hidden, ChatArea handles its own inner scroll */}
991
+ <main className="flex-1 min-w-0 min-h-0 flex flex-col overflow-hidden">
992
  <ChatArea
993
  messages={messages}
994
  onSendMessage={handleSendMessage}