SarahXia0405 commited on
Commit
9bb5dde
·
verified ·
1 Parent(s): 52c5789

Update web/src/App.tsx

Browse files
Files changed (1) hide show
  1. web/src/App.tsx +212 -123
web/src/App.tsx CHANGED
@@ -6,7 +6,7 @@ import { LoginScreen } from './components/LoginScreen';
6
  import { ProfileEditor } from './components/ProfileEditor';
7
  import { ReviewBanner } from './components/ReviewBanner';
8
  import { Onboarding } from './components/Onboarding';
9
- import { Menu, X, User, ChevronLeft, ChevronRight } from 'lucide-react';
10
  import { Button } from './components/ui/button';
11
  import { Toaster } from './components/ui/sonner';
12
  import { toast } from 'sonner';
@@ -99,12 +99,25 @@ export interface SavedChat {
99
  timestamp: Date;
100
  }
101
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  function App() {
103
  const [isDarkMode, setIsDarkMode] = useState(() => {
104
  const saved = localStorage.getItem('theme');
105
  return saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches);
106
  });
107
  const [user, setUser] = useState<User | null>(null);
 
108
  // Global current course selection
109
  const [currentCourseId, setCurrentCourseId] = useState<string>(() => localStorage.getItem('myspace_selected_course') || 'course1');
110
 
@@ -135,7 +148,7 @@ function App() {
135
  teachingAssistant: { name: 'James Brown', email: 'james.brown@university.edu' }
136
  },
137
  ];
138
-
139
  // Separate messages for each chat mode
140
  const [askMessages, setAskMessages] = useState<Message[]>([
141
  {
@@ -161,23 +174,23 @@ function App() {
161
  timestamp: new Date(),
162
  }
163
  ]);
164
-
165
  const [learningMode, setLearningMode] = useState<LearningMode>('concept');
166
  const [language, setLanguage] = useState<Language>('auto');
167
  const [chatMode, setChatMode] = useState<ChatMode>('ask');
168
-
169
  // Get current messages based on chat mode
170
  const messages = chatMode === 'ask' ? askMessages : chatMode === 'review' ? reviewMessages : quizMessages;
171
-
172
  // Track previous chat mode to detect mode changes
173
  const prevChatModeRef = useRef<ChatMode>(chatMode);
174
-
175
  // Ensure welcome message exists when switching modes or when messages are empty
176
  useEffect(() => {
177
  // Check the actual state arrays, not the computed messages
178
  let currentMessages: Message[];
179
  let setCurrentMessages: (messages: Message[]) => void;
180
-
181
  if (chatMode === 'ask') {
182
  currentMessages = askMessages;
183
  setCurrentMessages = setAskMessages;
@@ -188,12 +201,12 @@ function App() {
188
  currentMessages = quizMessages;
189
  setCurrentMessages = setQuizMessages;
190
  }
191
-
192
  const hasUserMessages = currentMessages.some(msg => msg.role === 'user');
193
  const expectedWelcomeId = chatMode === 'ask' ? '1' : chatMode === 'review' ? 'review-1' : 'quiz-1';
194
  const hasWelcomeMessage = currentMessages.some(msg => msg.id === expectedWelcomeId && msg.role === 'assistant');
195
  const modeChanged = prevChatModeRef.current !== chatMode;
196
-
197
  // If mode changed or messages are empty or missing welcome message, restore welcome message
198
  if ((modeChanged || currentMessages.length === 0 || !hasWelcomeMessage) && !hasUserMessages) {
199
  const initialMessages: Record<ChatMode, Message[]> = {
@@ -216,12 +229,13 @@ function App() {
216
  timestamp: new Date(),
217
  }],
218
  };
219
-
220
  setCurrentMessages(initialMessages[chatMode]);
221
  }
222
-
223
  prevChatModeRef.current = chatMode;
224
  }, [chatMode, askMessages.length, reviewMessages.length, quizMessages.length]); // Only depend on lengths to avoid infinite loops
 
225
  const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
226
  const [memoryProgress, setMemoryProgress] = useState(36);
227
  const [quizState, setQuizState] = useState<{
@@ -239,6 +253,7 @@ function App() {
239
  const [showProfileEditor, setShowProfileEditor] = useState(false);
240
  const [showOnboarding, setShowOnboarding] = useState(false);
241
  const [exportResult, setExportResult] = useState('');
 
242
  // Review banner state
243
  const [showReviewBanner, setShowReviewBanner] = useState(() => {
244
  // Temporarily force show for testing - remove this after confirming it works
@@ -248,14 +263,14 @@ function App() {
248
  });
249
  const [resultType, setResultType] = useState<'export' | 'quiz' | 'summary' | null>(null);
250
  const [showClearDialog, setShowClearDialog] = useState(false);
251
-
252
  // Saved conversations/summaries
253
  const [savedItems, setSavedItems] = useState<SavedItem[]>([]);
254
  const [recentlySavedId, setRecentlySavedId] = useState<string | null>(null);
255
-
256
  // Saved chats
257
  const [savedChats, setSavedChats] = useState<SavedChat[]>([]);
258
-
259
  // Mock group members
260
  const [groupMembers] = useState<GroupMember[]>([
261
  { id: 'clare', name: 'Clare AI', email: 'clare@ai.assistant', isAI: true },
@@ -268,13 +283,90 @@ function App() {
268
  const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
269
  const [currentWorkspaceId, setCurrentWorkspaceId] = useState<string>('individual');
270
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
271
  // Initialize workspaces when user logs in
272
  useEffect(() => {
273
  if (user) {
274
  const userAvatar = `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}`;
275
  const course1Info = availableCourses.find(c => c.id === 'course1');
276
  const course2Info = availableCourses.find(c => c.name === 'AI Ethics');
277
-
278
  setWorkspaces([
279
  {
280
  id: 'individual',
@@ -363,12 +455,12 @@ function App() {
363
  explanation: "Transparency is crucial because it helps users understand AI decision-making processes, enables debugging, and ensures accountability."
364
  }
365
  ];
366
-
367
  const randomIndex = Math.floor(Math.random() * questions.length);
368
  return questions[randomIndex];
369
  };
370
 
371
- const handleSendMessage = (content: string) => {
372
  if (!content.trim() || !user) return;
373
 
374
  // Attach sender info for all user messages
@@ -396,20 +488,22 @@ function App() {
396
  setQuizMessages(prev => [...prev, userMessage]);
397
  }
398
 
399
- // Handle Quiz mode differently
 
 
400
  if (chatMode === 'quiz') {
401
  if (quizState.waitingForAnswer) {
402
  // User is answering a question
403
  const isCorrect = Math.random() > 0.3; // Simulate answer checking
404
-
405
  setIsTyping(true);
406
  setTimeout(() => {
407
- const feedback = isCorrect
408
  ? "✅ Correct! Great job!"
409
  : "❌ Not quite right, but good effort!";
410
-
411
  const explanation = "Here's the explanation: The correct answer demonstrates understanding of the key concepts. Let me break it down for you...";
412
-
413
  const assistantMessage: Message = {
414
  id: (Date.now() + 1).toString(),
415
  role: 'assistant',
@@ -421,7 +515,7 @@ function App() {
421
 
422
  // Close typing indicator first
423
  setIsTyping(false);
424
-
425
  // Wait a bit to ensure typing indicator disappears before adding message
426
  setTimeout(() => {
427
  setQuizMessages(prev => [...prev, assistantMessage]);
@@ -432,70 +526,65 @@ function App() {
432
  return;
433
  }
434
 
435
- // Handle Ask and Review modes
436
- // Respond in all workspaces to keep conversations continuous
 
437
  const shouldAIRespond = true;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
438
 
439
- if (shouldAIRespond) {
440
- setIsTyping(true);
 
 
 
 
 
 
 
 
 
 
441
  setTimeout(() => {
442
- let response = '';
443
-
444
  if (chatMode === 'ask') {
445
- const responses: Record<LearningMode, string> = {
446
- general: "I'd be happy to help! To provide you with the most accurate and relevant answer, could you please provide some context about what you're asking? For example, what subject or topic is this related to?",
447
- concept: "Great question! Let me break this concept down for you. In Responsible AI, this relates to ensuring our AI systems are fair, transparent, and accountable. Would you like me to explain any specific aspect in more detail?",
448
- socratic: "That's an interesting point! Let me ask you this: What do you think are the key ethical considerations when deploying AI systems? Take a moment to think about it.",
449
- exam: "Let me test your understanding with a quick question: Which of the following is NOT a principle of Responsible AI? A) Fairness B) Transparency C) Profit Maximization D) Accountability",
450
- assignment: "I can help you with that assignment! Let's break it down into manageable steps. First, what specific aspect are you working on?",
451
- summary: "Here's a quick summary: Responsible AI focuses on developing and deploying AI systems that are ethical, fair, transparent, and accountable to society.",
452
- };
453
- response = responses[learningMode];
454
  } else if (chatMode === 'review') {
455
- // Check if this is a review request by checking window storage
456
- const reviewData = (window as any).__lastReviewData;
457
- if (reviewData) {
458
- if (reviewData.startsWith('REVIEW_TOPIC:')) {
459
- // Parse review topic data
460
- const data = reviewData.replace('REVIEW_TOPIC:', '').split('|');
461
- const [title, previousQuestion, memoryRetention, schedule, status, weight, lastReviewed] = data;
462
-
463
- response = `Let's review **${title}** together!\n\n**Your Previous Question:**\n"${previousQuestion}"\n\n**Review Details:**\n- **Memory Retention:** ${memoryRetention}%\n- **Schedule:** ${schedule}\n- **Status:** ${status.toUpperCase()}\n- **Weight:** ${weight}%\n- **Last Reviewed:** ${lastReviewed}\n\nLet's go through this topic step by step. What would you like to focus on first?`;
464
- // Clear the stored data
465
- delete (window as any).__lastReviewData;
466
- } else if (reviewData === 'REVIEW_ALL') {
467
- response = `I'll help you review all the topics that need your attention. Based on your learning history, here are the topics we should focus on:\n\n1. **Main Concept of Lab 3** (Urgent - Memory Retention: 25%)\n2. **Effective Prompt Engineering** (Review - Memory Retention: 60%)\n3. **Objective LLM Evaluation** (Stable - Memory Retention: 90%)\n\nLet's start with the most urgent ones first. Which topic would you like to begin with?`;
468
- // Clear the stored data
469
- delete (window as any).__lastReviewData;
470
- } else {
471
- response = "Let's review what you've learned! Based on your previous conversations, here are the key concepts we covered: [Review content would go here]";
472
- }
473
- } else {
474
- response = "Let's review what you've learned! Based on your previous conversations, here are the key concepts we covered: [Review content would go here]";
475
- }
476
  }
 
 
 
 
477
 
478
- const assistantMessage: Message = {
479
- id: (Date.now() + 1).toString(),
480
- role: 'assistant',
481
- content: response,
482
- timestamp: new Date(),
483
- references: chatMode === 'ask' ? ['Module 10, Section 2.3', 'Lecture Notes - Week 5'] : undefined,
484
- sender: spaceType === 'group' ? groupMembers.find(m => m.isAI) : undefined,
485
- };
486
-
487
- // Close typing indicator first
488
- setIsTyping(false);
489
-
490
- // Wait a bit to ensure typing indicator disappears before adding message
491
- setTimeout(() => {
492
- if (chatMode === 'ask') {
493
- setAskMessages(prev => [...prev, assistantMessage]);
494
- } else if (chatMode === 'review') {
495
- setReviewMessages(prev => [...prev, assistantMessage]);
496
- }
497
- }, 50);
498
- }, 2000);
499
  }
500
  };
501
 
@@ -503,11 +592,11 @@ function App() {
503
  setIsTyping(true);
504
  const question = generateQuizQuestion();
505
  let questionText = question.question;
506
-
507
  if (question.type === 'multiple-choice') {
508
- questionText += '\n\n' + question.options.join('\n');
509
  }
510
-
511
  setTimeout(() => {
512
  const assistantMessage: Message = {
513
  id: Date.now().toString(),
@@ -520,15 +609,15 @@ function App() {
520
 
521
  // Close typing indicator first
522
  setIsTyping(false);
523
-
524
  // Wait a bit to ensure typing indicator disappears before adding message
525
  setTimeout(() => {
526
  setQuizMessages(prev => [...prev, assistantMessage]);
527
- setQuizState(prev => ({
528
- ...prev,
529
  currentQuestion: prev.currentQuestion + 1,
530
  waitingForAnswer: true,
531
- showNextButton: false
532
  }));
533
  }, 50);
534
  }, 2000);
@@ -551,7 +640,7 @@ function App() {
551
  };
552
 
553
  const handleFileTypeChange = (index: number, type: FileType) => {
554
- setUploadedFiles(prev => prev.map((file, i) =>
555
  i === index ? { ...file, type } : file
556
  ));
557
  };
@@ -559,12 +648,12 @@ function App() {
559
  // Helper function to check if current chat is already saved
560
  const isCurrentChatSaved = (): SavedChat | null => {
561
  if (messages.length <= 1) return null;
562
-
563
  // Find a saved chat that matches the current messages and chatMode
564
  return savedChats.find(chat => {
565
  if (chat.chatMode !== chatMode) return false;
566
  if (chat.messages.length !== messages.length) return false;
567
-
568
  // Check if all messages match
569
  return chat.messages.every((savedMsg, index) => {
570
  const currentMsg = messages[index];
@@ -582,7 +671,7 @@ function App() {
582
  toast.info('No conversation to save');
583
  return;
584
  }
585
-
586
  // Check if already saved
587
  const existingChat = isCurrentChatSaved();
588
  if (existingChat) {
@@ -591,7 +680,7 @@ function App() {
591
  toast.success('Chat unsaved');
592
  return;
593
  }
594
-
595
  // Save: add new chat
596
  const title = `Chat - ${chatMode === 'ask' ? 'Ask' : chatMode === 'review' ? 'Review' : 'Quiz'} - ${new Date().toLocaleDateString()}`;
597
  const newChat: SavedChat = {
@@ -601,7 +690,7 @@ function App() {
601
  chatMode,
602
  timestamp: new Date(),
603
  };
604
-
605
  setSavedChats(prev => [newChat, ...prev]);
606
  setLeftPanelVisible(true);
607
  toast.success('Chat saved!');
@@ -610,7 +699,7 @@ function App() {
610
  const handleLoadChat = (savedChat: SavedChat) => {
611
  // Set the chat mode first
612
  setChatMode(savedChat.chatMode);
613
-
614
  // Then set the messages for that mode
615
  if (savedChat.chatMode === 'ask') {
616
  setAskMessages(savedChat.messages);
@@ -625,7 +714,7 @@ function App() {
625
  showNextButton: false,
626
  });
627
  }
628
-
629
  toast.success('Chat loaded!');
630
  };
631
 
@@ -635,7 +724,7 @@ function App() {
635
  };
636
 
637
  const handleRenameSavedChat = (id: string, newTitle: string) => {
638
- setSavedChats(prev => prev.map(chat =>
639
  chat.id === id ? { ...chat, title: newTitle } : chat
640
  ));
641
  toast.success('Chat renamed');
@@ -645,7 +734,7 @@ function App() {
645
  if (shouldSave) {
646
  handleSaveChat();
647
  }
648
-
649
  const initialMessages: Record<ChatMode, Message[]> = {
650
  ask: [{
651
  id: '1',
@@ -666,7 +755,7 @@ function App() {
666
  timestamp: new Date(),
667
  }],
668
  };
669
-
670
  // Clear only the current mode's conversation
671
  if (chatMode === 'ask') {
672
  setAskMessages(initialMessages.ask);
@@ -696,7 +785,7 @@ This conversation covered key concepts in Module 10 – Responsible AI, includin
696
  3. Best practices for ethical AI development
697
 
698
  Exported successfully! ✓`;
699
-
700
  setExportResult(result);
701
  setResultType('export');
702
  toast.success('Conversation exported!');
@@ -717,7 +806,7 @@ d) Cost reduction
717
  **Question 3:** True or False: AI systems should always prioritize accuracy over fairness.
718
 
719
  Generate quiz based on your conversation!`;
720
-
721
  setExportResult(quiz);
722
  setResultType('quiz');
723
  toast.success('Quiz generated!');
@@ -743,7 +832,7 @@ Generate quiz based on your conversation!`;
743
 
744
  ## Progress Update
745
  You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
746
-
747
  setExportResult(summary);
748
  setResultType('summary');
749
  toast.success('Summary generated!');
@@ -757,7 +846,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
757
  workspaceId?: string
758
  ) => {
759
  if (!content.trim()) return;
760
-
761
  // Summary should always be saved as file, not chat
762
  // If saving as chat (from RightPanel export/quiz only, not summary)
763
  if (saveAsChat && type !== 'summary') {
@@ -777,7 +866,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
777
  timestamp: new Date(),
778
  }
779
  ];
780
-
781
  const title = type === 'export' ? 'Exported Conversation' : 'Micro-Quiz';
782
  const newChat: SavedChat = {
783
  id: Date.now().toString(),
@@ -786,13 +875,13 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
786
  chatMode: 'ask',
787
  timestamp: new Date(),
788
  };
789
-
790
  setSavedChats(prev => [newChat, ...prev]);
791
  setLeftPanelVisible(true);
792
  toast.success('Chat saved!');
793
  return;
794
  }
795
-
796
  // Otherwise, save as file (existing behavior)
797
  // Check if already saved
798
  const existingItem = savedItems.find(item => item.content === content && item.type === type);
@@ -801,7 +890,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
801
  handleUnsave(existingItem.id);
802
  return;
803
  }
804
-
805
  // Save: add new item
806
  const title = type === 'export' ? 'Exported Conversation' : type === 'quiz' ? 'Micro-Quiz' : 'Summarization';
807
  const newItem: SavedItem = {
@@ -814,16 +903,16 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
814
  format,
815
  workspaceId: workspaceId || currentWorkspaceId,
816
  };
817
-
818
  setSavedItems(prev => [newItem, ...prev]);
819
  setRecentlySavedId(newItem.id);
820
  setLeftPanelVisible(true); // Open left panel
821
-
822
  // Clear the highlight after animation
823
  setTimeout(() => {
824
  setRecentlySavedId(null);
825
  }, 2000);
826
-
827
  toast.success('Saved for later!');
828
  };
829
 
@@ -841,7 +930,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
841
  }) => {
842
  const id = `group-${Date.now()}`;
843
  const avatar = `https://api.dicebear.com/7.x/shapes/svg?seed=${encodeURIComponent(payload.name)}`;
844
-
845
  // Add creator as first member
846
  const creatorMember: GroupMember = user ? {
847
  id: user.email,
@@ -849,7 +938,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
849
  email: user.email,
850
  avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}`,
851
  } : { id: 'unknown', name: 'Unknown', email: 'unknown@email.com' };
852
-
853
  const members: GroupMember[] = [
854
  creatorMember,
855
  ...payload.invites.map(email => ({
@@ -860,7 +949,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
860
  ];
861
 
862
  let newWorkspace: Workspace;
863
-
864
  if (payload.category === 'course') {
865
  const courseInfo = availableCourses.find(c => c.id === payload.courseId);
866
  newWorkspace = {
@@ -888,12 +977,12 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
888
 
889
  setWorkspaces(prev => [...prev, newWorkspace]);
890
  setCurrentWorkspaceId(id);
891
-
892
  // Set current course if it's a course workspace
893
  if (payload.category === 'course' && payload.courseId) {
894
  setCurrentCourseId(payload.courseId);
895
  }
896
-
897
  toast.success('New group workspace created');
898
  };
899
 
@@ -916,7 +1005,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
916
  // For testing: always show onboarding (comment out localStorage check)
917
  // const onboardingCompleted = localStorage.getItem(`onboarding_completed_${newUser.email}`);
918
  // if (!onboardingCompleted) {
919
- setShowOnboarding(true);
920
  // }
921
  };
922
 
@@ -946,8 +1035,8 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
946
  if (showOnboarding && user) {
947
  return (
948
  <>
949
- <Onboarding
950
- user={user}
951
  onComplete={handleOnboardingComplete}
952
  onSkip={handleOnboardingSkip}
953
  />
@@ -958,7 +1047,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
958
  return (
959
  <div className="min-h-screen bg-background flex flex-col">
960
  <Toaster />
961
- <Header
962
  user={user}
963
  onMenuClick={() => setLeftSidebarOpen(!leftSidebarOpen)}
964
  onUserClick={() => {}}
@@ -992,7 +1081,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
992
  </div>
993
  )}
994
 
995
- <div
996
  className="flex-1 flex overflow-hidden h-[calc(100vh-4rem)] relative"
997
  style={{ overscrollBehavior: 'none' }}
998
  >
@@ -1011,7 +1100,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
1011
  )}
1012
  {/* Mobile Sidebar Toggle - Left */}
1013
  {leftSidebarOpen && (
1014
- <div
1015
  className="fixed inset-0 bg-black/50 z-40 lg:hidden"
1016
  onClick={() => setLeftSidebarOpen(false)}
1017
  />
@@ -1019,7 +1108,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
1019
 
1020
  {/* Left Sidebar */}
1021
  {leftPanelVisible ? (
1022
- <aside
1023
  className="hidden lg:flex w-80 bg-card border-r border-border flex-col h-full min-h-0 relative"
1024
  style={{ borderRight: '1px solid var(--border)', height: 'calc(100vh - 4rem)' }}
1025
  >
@@ -1063,7 +1152,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
1063
  ) : null}
1064
 
1065
  {/* Left Sidebar - Mobile */}
1066
- <aside
1067
  className={`
1068
  fixed lg:hidden inset-y-0 left-0 z-50
1069
  w-80 bg-card border-r border-border
@@ -1156,4 +1245,4 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
1156
  );
1157
  }
1158
 
1159
- export default App;
 
6
  import { ProfileEditor } from './components/ProfileEditor';
7
  import { ReviewBanner } from './components/ReviewBanner';
8
  import { Onboarding } from './components/Onboarding';
9
+ import { X, ChevronLeft, ChevronRight } from 'lucide-react';
10
  import { Button } from './components/ui/button';
11
  import { Toaster } from './components/ui/sonner';
12
  import { toast } from 'sonner';
 
99
  timestamp: Date;
100
  }
101
 
102
+ type BackendRef = {
103
+ source_file?: string;
104
+ section?: string;
105
+ };
106
+
107
+ type ChatApiResponse = {
108
+ reply: string;
109
+ session_status_md?: string;
110
+ refs?: BackendRef[];
111
+ latency_ms?: number;
112
+ };
113
+
114
  function App() {
115
  const [isDarkMode, setIsDarkMode] = useState(() => {
116
  const saved = localStorage.getItem('theme');
117
  return saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches);
118
  });
119
  const [user, setUser] = useState<User | null>(null);
120
+
121
  // Global current course selection
122
  const [currentCourseId, setCurrentCourseId] = useState<string>(() => localStorage.getItem('myspace_selected_course') || 'course1');
123
 
 
148
  teachingAssistant: { name: 'James Brown', email: 'james.brown@university.edu' }
149
  },
150
  ];
151
+
152
  // Separate messages for each chat mode
153
  const [askMessages, setAskMessages] = useState<Message[]>([
154
  {
 
174
  timestamp: new Date(),
175
  }
176
  ]);
177
+
178
  const [learningMode, setLearningMode] = useState<LearningMode>('concept');
179
  const [language, setLanguage] = useState<Language>('auto');
180
  const [chatMode, setChatMode] = useState<ChatMode>('ask');
181
+
182
  // Get current messages based on chat mode
183
  const messages = chatMode === 'ask' ? askMessages : chatMode === 'review' ? reviewMessages : quizMessages;
184
+
185
  // Track previous chat mode to detect mode changes
186
  const prevChatModeRef = useRef<ChatMode>(chatMode);
187
+
188
  // Ensure welcome message exists when switching modes or when messages are empty
189
  useEffect(() => {
190
  // Check the actual state arrays, not the computed messages
191
  let currentMessages: Message[];
192
  let setCurrentMessages: (messages: Message[]) => void;
193
+
194
  if (chatMode === 'ask') {
195
  currentMessages = askMessages;
196
  setCurrentMessages = setAskMessages;
 
201
  currentMessages = quizMessages;
202
  setCurrentMessages = setQuizMessages;
203
  }
204
+
205
  const hasUserMessages = currentMessages.some(msg => msg.role === 'user');
206
  const expectedWelcomeId = chatMode === 'ask' ? '1' : chatMode === 'review' ? 'review-1' : 'quiz-1';
207
  const hasWelcomeMessage = currentMessages.some(msg => msg.id === expectedWelcomeId && msg.role === 'assistant');
208
  const modeChanged = prevChatModeRef.current !== chatMode;
209
+
210
  // If mode changed or messages are empty or missing welcome message, restore welcome message
211
  if ((modeChanged || currentMessages.length === 0 || !hasWelcomeMessage) && !hasUserMessages) {
212
  const initialMessages: Record<ChatMode, Message[]> = {
 
229
  timestamp: new Date(),
230
  }],
231
  };
232
+
233
  setCurrentMessages(initialMessages[chatMode]);
234
  }
235
+
236
  prevChatModeRef.current = chatMode;
237
  }, [chatMode, askMessages.length, reviewMessages.length, quizMessages.length]); // Only depend on lengths to avoid infinite loops
238
+
239
  const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
240
  const [memoryProgress, setMemoryProgress] = useState(36);
241
  const [quizState, setQuizState] = useState<{
 
253
  const [showProfileEditor, setShowProfileEditor] = useState(false);
254
  const [showOnboarding, setShowOnboarding] = useState(false);
255
  const [exportResult, setExportResult] = useState('');
256
+
257
  // Review banner state
258
  const [showReviewBanner, setShowReviewBanner] = useState(() => {
259
  // Temporarily force show for testing - remove this after confirming it works
 
263
  });
264
  const [resultType, setResultType] = useState<'export' | 'quiz' | 'summary' | null>(null);
265
  const [showClearDialog, setShowClearDialog] = useState(false);
266
+
267
  // Saved conversations/summaries
268
  const [savedItems, setSavedItems] = useState<SavedItem[]>([]);
269
  const [recentlySavedId, setRecentlySavedId] = useState<string | null>(null);
270
+
271
  // Saved chats
272
  const [savedChats, setSavedChats] = useState<SavedChat[]>([]);
273
+
274
  // Mock group members
275
  const [groupMembers] = useState<GroupMember[]>([
276
  { id: 'clare', name: 'Clare AI', email: 'clare@ai.assistant', isAI: true },
 
283
  const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
284
  const [currentWorkspaceId, setCurrentWorkspaceId] = useState<string>('individual');
285
 
286
+ // -----------------------------
287
+ // Backend mapping helpers
288
+ // -----------------------------
289
+ function toBackendLearningMode(mode: LearningMode): string {
290
+ switch (mode) {
291
+ case 'concept': return 'Concept Explainer';
292
+ case 'socratic': return 'Socratic Tutor';
293
+ case 'exam': return 'Exam Prep';
294
+ case 'assignment': return 'Assignment Helper';
295
+ case 'summary': return 'Quick Summary';
296
+ case 'general':
297
+ default: return 'General';
298
+ }
299
+ }
300
+
301
+ function toBackendLanguagePreference(lang: Language): string {
302
+ switch (lang) {
303
+ case 'en': return 'English';
304
+ case 'zh': return '中文';
305
+ case 'auto':
306
+ default: return 'Auto';
307
+ }
308
+ }
309
+
310
+ function toBackendDocTypeFromUploaded(files: UploadedFile[]): string {
311
+ // Prefer non-syllabus doc type if present; else Syllabus if exists; else Other
312
+ const hasSyllabus = files.some(f => f.type === 'syllabus');
313
+ const firstNonSyllabus = files.find(f => f.type !== 'syllabus');
314
+
315
+ const map: Record<FileType, string> = {
316
+ 'syllabus': 'Syllabus',
317
+ 'lecture-slides': 'Lecture Slides / PPT',
318
+ 'literature-review': 'Literature Review / Paper',
319
+ 'other': 'Other Course Document',
320
+ };
321
+
322
+ if (firstNonSyllabus) return map[firstNonSyllabus.type];
323
+ if (hasSyllabus) return map['syllabus'];
324
+ return map['other'];
325
+ }
326
+
327
+ function formatRefs(refs?: BackendRef[]): string[] | undefined {
328
+ if (!refs || refs.length === 0) return undefined;
329
+ const lines = refs
330
+ .map(r => {
331
+ const a = (r.source_file || '').trim();
332
+ const b = (r.section || '').trim();
333
+ if (a && b) return `${a} — ${b}`;
334
+ if (a) return a;
335
+ if (b) return b;
336
+ return '';
337
+ })
338
+ .filter(Boolean);
339
+ return lines.length ? lines : undefined;
340
+ }
341
+
342
+ async function callChatApi(payload: {
343
+ user_id: string;
344
+ message: string;
345
+ learning_mode: string;
346
+ language_preference: string;
347
+ doc_type: string;
348
+ }): Promise<ChatApiResponse> {
349
+ const res = await fetch('/api/chat', {
350
+ method: 'POST',
351
+ headers: { 'Content-Type': 'application/json' },
352
+ body: JSON.stringify(payload),
353
+ });
354
+
355
+ if (!res.ok) {
356
+ const text = await res.text().catch(() => '');
357
+ throw new Error(`Chat API failed: ${res.status} ${res.statusText}${text ? ` - ${text}` : ''}`);
358
+ }
359
+
360
+ return res.json();
361
+ }
362
+
363
  // Initialize workspaces when user logs in
364
  useEffect(() => {
365
  if (user) {
366
  const userAvatar = `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}`;
367
  const course1Info = availableCourses.find(c => c.id === 'course1');
368
  const course2Info = availableCourses.find(c => c.name === 'AI Ethics');
369
+
370
  setWorkspaces([
371
  {
372
  id: 'individual',
 
455
  explanation: "Transparency is crucial because it helps users understand AI decision-making processes, enables debugging, and ensures accountability."
456
  }
457
  ];
458
+
459
  const randomIndex = Math.floor(Math.random() * questions.length);
460
  return questions[randomIndex];
461
  };
462
 
463
+ const handleSendMessage = async (content: string) => {
464
  if (!content.trim() || !user) return;
465
 
466
  // Attach sender info for all user messages
 
488
  setQuizMessages(prev => [...prev, userMessage]);
489
  }
490
 
491
+ // -----------------------------
492
+ // Quiz mode: keep existing local simulation
493
+ // -----------------------------
494
  if (chatMode === 'quiz') {
495
  if (quizState.waitingForAnswer) {
496
  // User is answering a question
497
  const isCorrect = Math.random() > 0.3; // Simulate answer checking
498
+
499
  setIsTyping(true);
500
  setTimeout(() => {
501
+ const feedback = isCorrect
502
  ? "✅ Correct! Great job!"
503
  : "❌ Not quite right, but good effort!";
504
+
505
  const explanation = "Here's the explanation: The correct answer demonstrates understanding of the key concepts. Let me break it down for you...";
506
+
507
  const assistantMessage: Message = {
508
  id: (Date.now() + 1).toString(),
509
  role: 'assistant',
 
515
 
516
  // Close typing indicator first
517
  setIsTyping(false);
518
+
519
  // Wait a bit to ensure typing indicator disappears before adding message
520
  setTimeout(() => {
521
  setQuizMessages(prev => [...prev, assistantMessage]);
 
526
  return;
527
  }
528
 
529
+ // -----------------------------
530
+ // Ask + Review: call backend /api/chat
531
+ // -----------------------------
532
  const shouldAIRespond = true;
533
+ if (!shouldAIRespond) return;
534
+
535
+ setIsTyping(true);
536
+
537
+ try {
538
+ const docType = toBackendDocTypeFromUploaded(uploadedFiles);
539
+
540
+ const resp = await callChatApi({
541
+ user_id: user.email,
542
+ message: content,
543
+ learning_mode: toBackendLearningMode(learningMode),
544
+ language_preference: toBackendLanguagePreference(language),
545
+ doc_type: docType,
546
+ });
547
 
548
+ const assistantMessage: Message = {
549
+ id: (Date.now() + 1).toString(),
550
+ role: 'assistant',
551
+ content: (resp.reply || '').trim() || '(No response)',
552
+ timestamp: new Date(),
553
+ references: formatRefs(resp.refs),
554
+ sender: spaceType === 'group' ? groupMembers.find(m => m.isAI) : undefined,
555
+ };
556
+
557
+ setIsTyping(false);
558
+
559
+ // Ensure typing indicator disappears before adding message
560
  setTimeout(() => {
 
 
561
  if (chatMode === 'ask') {
562
+ setAskMessages(prev => [...prev, assistantMessage]);
 
 
 
 
 
 
 
 
563
  } else if (chatMode === 'review') {
564
+ setReviewMessages(prev => [...prev, assistantMessage]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
565
  }
566
+ }, 50);
567
+ } catch (err: any) {
568
+ console.error(err);
569
+ setIsTyping(false);
570
 
571
+ const assistantMessage: Message = {
572
+ id: (Date.now() + 1).toString(),
573
+ role: 'assistant',
574
+ content: "Sorry — I couldn't reach the chat server right now. Please try again in a moment.",
575
+ timestamp: new Date(),
576
+ sender: spaceType === 'group' ? groupMembers.find(m => m.isAI) : undefined,
577
+ };
578
+
579
+ setTimeout(() => {
580
+ if (chatMode === 'ask') {
581
+ setAskMessages(prev => [...prev, assistantMessage]);
582
+ } else if (chatMode === 'review') {
583
+ setReviewMessages(prev => [...prev, assistantMessage]);
584
+ }
585
+ }, 50);
586
+
587
+ toast.error('Chat failed. Check the API server logs.');
 
 
 
 
588
  }
589
  };
590
 
 
592
  setIsTyping(true);
593
  const question = generateQuizQuestion();
594
  let questionText = question.question;
595
+
596
  if (question.type === 'multiple-choice') {
597
+ questionText += '\n\n' + question.options!.join('\n');
598
  }
599
+
600
  setTimeout(() => {
601
  const assistantMessage: Message = {
602
  id: Date.now().toString(),
 
609
 
610
  // Close typing indicator first
611
  setIsTyping(false);
612
+
613
  // Wait a bit to ensure typing indicator disappears before adding message
614
  setTimeout(() => {
615
  setQuizMessages(prev => [...prev, assistantMessage]);
616
+ setQuizState(prev => ({
617
+ ...prev,
618
  currentQuestion: prev.currentQuestion + 1,
619
  waitingForAnswer: true,
620
+ showNextButton: false
621
  }));
622
  }, 50);
623
  }, 2000);
 
640
  };
641
 
642
  const handleFileTypeChange = (index: number, type: FileType) => {
643
+ setUploadedFiles(prev => prev.map((file, i) =>
644
  i === index ? { ...file, type } : file
645
  ));
646
  };
 
648
  // Helper function to check if current chat is already saved
649
  const isCurrentChatSaved = (): SavedChat | null => {
650
  if (messages.length <= 1) return null;
651
+
652
  // Find a saved chat that matches the current messages and chatMode
653
  return savedChats.find(chat => {
654
  if (chat.chatMode !== chatMode) return false;
655
  if (chat.messages.length !== messages.length) return false;
656
+
657
  // Check if all messages match
658
  return chat.messages.every((savedMsg, index) => {
659
  const currentMsg = messages[index];
 
671
  toast.info('No conversation to save');
672
  return;
673
  }
674
+
675
  // Check if already saved
676
  const existingChat = isCurrentChatSaved();
677
  if (existingChat) {
 
680
  toast.success('Chat unsaved');
681
  return;
682
  }
683
+
684
  // Save: add new chat
685
  const title = `Chat - ${chatMode === 'ask' ? 'Ask' : chatMode === 'review' ? 'Review' : 'Quiz'} - ${new Date().toLocaleDateString()}`;
686
  const newChat: SavedChat = {
 
690
  chatMode,
691
  timestamp: new Date(),
692
  };
693
+
694
  setSavedChats(prev => [newChat, ...prev]);
695
  setLeftPanelVisible(true);
696
  toast.success('Chat saved!');
 
699
  const handleLoadChat = (savedChat: SavedChat) => {
700
  // Set the chat mode first
701
  setChatMode(savedChat.chatMode);
702
+
703
  // Then set the messages for that mode
704
  if (savedChat.chatMode === 'ask') {
705
  setAskMessages(savedChat.messages);
 
714
  showNextButton: false,
715
  });
716
  }
717
+
718
  toast.success('Chat loaded!');
719
  };
720
 
 
724
  };
725
 
726
  const handleRenameSavedChat = (id: string, newTitle: string) => {
727
+ setSavedChats(prev => prev.map(chat =>
728
  chat.id === id ? { ...chat, title: newTitle } : chat
729
  ));
730
  toast.success('Chat renamed');
 
734
  if (shouldSave) {
735
  handleSaveChat();
736
  }
737
+
738
  const initialMessages: Record<ChatMode, Message[]> = {
739
  ask: [{
740
  id: '1',
 
755
  timestamp: new Date(),
756
  }],
757
  };
758
+
759
  // Clear only the current mode's conversation
760
  if (chatMode === 'ask') {
761
  setAskMessages(initialMessages.ask);
 
785
  3. Best practices for ethical AI development
786
 
787
  Exported successfully! ✓`;
788
+
789
  setExportResult(result);
790
  setResultType('export');
791
  toast.success('Conversation exported!');
 
806
  **Question 3:** True or False: AI systems should always prioritize accuracy over fairness.
807
 
808
  Generate quiz based on your conversation!`;
809
+
810
  setExportResult(quiz);
811
  setResultType('quiz');
812
  toast.success('Quiz generated!');
 
832
 
833
  ## Progress Update
834
  You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
835
+
836
  setExportResult(summary);
837
  setResultType('summary');
838
  toast.success('Summary generated!');
 
846
  workspaceId?: string
847
  ) => {
848
  if (!content.trim()) return;
849
+
850
  // Summary should always be saved as file, not chat
851
  // If saving as chat (from RightPanel export/quiz only, not summary)
852
  if (saveAsChat && type !== 'summary') {
 
866
  timestamp: new Date(),
867
  }
868
  ];
869
+
870
  const title = type === 'export' ? 'Exported Conversation' : 'Micro-Quiz';
871
  const newChat: SavedChat = {
872
  id: Date.now().toString(),
 
875
  chatMode: 'ask',
876
  timestamp: new Date(),
877
  };
878
+
879
  setSavedChats(prev => [newChat, ...prev]);
880
  setLeftPanelVisible(true);
881
  toast.success('Chat saved!');
882
  return;
883
  }
884
+
885
  // Otherwise, save as file (existing behavior)
886
  // Check if already saved
887
  const existingItem = savedItems.find(item => item.content === content && item.type === type);
 
890
  handleUnsave(existingItem.id);
891
  return;
892
  }
893
+
894
  // Save: add new item
895
  const title = type === 'export' ? 'Exported Conversation' : type === 'quiz' ? 'Micro-Quiz' : 'Summarization';
896
  const newItem: SavedItem = {
 
903
  format,
904
  workspaceId: workspaceId || currentWorkspaceId,
905
  };
906
+
907
  setSavedItems(prev => [newItem, ...prev]);
908
  setRecentlySavedId(newItem.id);
909
  setLeftPanelVisible(true); // Open left panel
910
+
911
  // Clear the highlight after animation
912
  setTimeout(() => {
913
  setRecentlySavedId(null);
914
  }, 2000);
915
+
916
  toast.success('Saved for later!');
917
  };
918
 
 
930
  }) => {
931
  const id = `group-${Date.now()}`;
932
  const avatar = `https://api.dicebear.com/7.x/shapes/svg?seed=${encodeURIComponent(payload.name)}`;
933
+
934
  // Add creator as first member
935
  const creatorMember: GroupMember = user ? {
936
  id: user.email,
 
938
  email: user.email,
939
  avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}`,
940
  } : { id: 'unknown', name: 'Unknown', email: 'unknown@email.com' };
941
+
942
  const members: GroupMember[] = [
943
  creatorMember,
944
  ...payload.invites.map(email => ({
 
949
  ];
950
 
951
  let newWorkspace: Workspace;
952
+
953
  if (payload.category === 'course') {
954
  const courseInfo = availableCourses.find(c => c.id === payload.courseId);
955
  newWorkspace = {
 
977
 
978
  setWorkspaces(prev => [...prev, newWorkspace]);
979
  setCurrentWorkspaceId(id);
980
+
981
  // Set current course if it's a course workspace
982
  if (payload.category === 'course' && payload.courseId) {
983
  setCurrentCourseId(payload.courseId);
984
  }
985
+
986
  toast.success('New group workspace created');
987
  };
988
 
 
1005
  // For testing: always show onboarding (comment out localStorage check)
1006
  // const onboardingCompleted = localStorage.getItem(`onboarding_completed_${newUser.email}`);
1007
  // if (!onboardingCompleted) {
1008
+ setShowOnboarding(true);
1009
  // }
1010
  };
1011
 
 
1035
  if (showOnboarding && user) {
1036
  return (
1037
  <>
1038
+ <Onboarding
1039
+ user={user}
1040
  onComplete={handleOnboardingComplete}
1041
  onSkip={handleOnboardingSkip}
1042
  />
 
1047
  return (
1048
  <div className="min-h-screen bg-background flex flex-col">
1049
  <Toaster />
1050
+ <Header
1051
  user={user}
1052
  onMenuClick={() => setLeftSidebarOpen(!leftSidebarOpen)}
1053
  onUserClick={() => {}}
 
1081
  </div>
1082
  )}
1083
 
1084
+ <div
1085
  className="flex-1 flex overflow-hidden h-[calc(100vh-4rem)] relative"
1086
  style={{ overscrollBehavior: 'none' }}
1087
  >
 
1100
  )}
1101
  {/* Mobile Sidebar Toggle - Left */}
1102
  {leftSidebarOpen && (
1103
+ <div
1104
  className="fixed inset-0 bg-black/50 z-40 lg:hidden"
1105
  onClick={() => setLeftSidebarOpen(false)}
1106
  />
 
1108
 
1109
  {/* Left Sidebar */}
1110
  {leftPanelVisible ? (
1111
+ <aside
1112
  className="hidden lg:flex w-80 bg-card border-r border-border flex-col h-full min-h-0 relative"
1113
  style={{ borderRight: '1px solid var(--border)', height: 'calc(100vh - 4rem)' }}
1114
  >
 
1152
  ) : null}
1153
 
1154
  {/* Left Sidebar - Mobile */}
1155
+ <aside
1156
  className={`
1157
  fixed lg:hidden inset-y-0 left-0 z-50
1158
  w-80 bg-card border-r border-border
 
1245
  );
1246
  }
1247
 
1248
+ export default App;