SarahXia0405 commited on
Commit
00072e4
·
verified ·
1 Parent(s): 2f04655

Update web/src/App.tsx

Browse files
Files changed (1) hide show
  1. web/src/App.tsx +394 -539
web/src/App.tsx CHANGED
@@ -1,26 +1,30 @@
1
- import React, { useState, useEffect, useRef } from 'react';
2
- import { Header } from './components/Header';
3
- import { LeftSidebar } from './components/LeftSidebar';
4
- import { ChatArea } from './components/ChatArea';
5
- 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 { 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';
 
 
 
 
13
 
14
  export interface Message {
15
  id: string;
16
- role: 'user' | 'assistant';
17
  content: string;
18
  timestamp: Date;
19
  references?: string[];
20
  sender?: GroupMember; // For group chat
21
  showNextButton?: boolean; // For quiz mode
22
  questionData?: {
23
- type: 'multiple-choice' | 'fill-in-blank' | 'open-ended';
24
  question: string;
25
  options?: string[];
26
  correctAnswer: string;
@@ -42,7 +46,7 @@ export interface GroupMember {
42
  isAI?: boolean;
43
  }
44
 
45
- export type SpaceType = 'individual' | 'group';
46
 
47
  export interface CourseInfo {
48
  id: string;
@@ -63,31 +67,31 @@ export interface Workspace {
63
  type: SpaceType;
64
  avatar: string;
65
  members?: GroupMember[];
66
- category?: 'course' | 'personal';
67
  courseName?: string;
68
  courseInfo?: CourseInfo;
69
  isEditable?: boolean; // For personal interest workspaces
70
  }
71
 
72
- export type FileType = 'syllabus' | 'lecture-slides' | 'literature-review' | 'other';
73
 
74
  export interface UploadedFile {
75
  file: File;
76
  type: FileType;
77
  }
78
 
79
- export type LearningMode = 'general' | 'concept' | 'socratic' | 'exam' | 'assignment' | 'summary';
80
- export type Language = 'auto' | 'en' | 'zh';
81
- export type ChatMode = 'ask' | 'review' | 'quiz';
82
 
83
  export interface SavedItem {
84
  id: string;
85
  title: string;
86
  content: string;
87
- type: 'export' | 'quiz' | 'summary';
88
  timestamp: Date;
89
  isSaved: boolean;
90
- format?: 'pdf' | 'text';
91
  workspaceId: string;
92
  }
93
 
@@ -99,102 +103,104 @@ export interface SavedChat {
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
 
124
  // Available courses with instructor/TA info
125
  const availableCourses: CourseInfo[] = [
126
  {
127
- id: 'course1',
128
- name: 'Introduction to AI',
129
- instructor: { name: 'Dr. Sarah Johnson', email: 'sarah.johnson@university.edu' },
130
- teachingAssistant: { name: 'Michael Chen', email: 'michael.chen@university.edu' }
131
  },
132
  {
133
- id: 'course2',
134
- name: 'Machine Learning',
135
- instructor: { name: 'Prof. David Lee', email: 'david.lee@university.edu' },
136
- teachingAssistant: { name: 'Emily Zhang', email: 'emily.zhang@university.edu' }
137
  },
138
  {
139
- id: 'course3',
140
- name: 'Data Structures',
141
- instructor: { name: 'Dr. Robert Smith', email: 'robert.smith@university.edu' },
142
- teachingAssistant: { name: 'Lisa Wang', email: 'lisa.wang@university.edu' }
143
  },
144
  {
145
- id: 'course4',
146
- name: 'Web Development',
147
- instructor: { name: 'Prof. Maria Garcia', email: 'maria.garcia@university.edu' },
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
  {
155
- id: '1',
156
- role: 'assistant',
157
- content: "👋 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!",
 
158
  timestamp: new Date(),
159
- }
160
  ]);
 
161
  const [reviewMessages, setReviewMessages] = useState<Message[]>([
162
  {
163
- id: 'review-1',
164
- role: 'assistant',
165
  content: "📚 Welcome to Review mode! I'll help you review and consolidate your learning. Let's go through what you've learned!",
166
  timestamp: new Date(),
167
- }
168
  ]);
 
169
  const [quizMessages, setQuizMessages] = useState<Message[]>([
170
  {
171
- id: 'quiz-1',
172
- role: 'assistant',
173
  content: "🎯 Welcome to Quiz mode! I'll test your understanding with personalized questions based on your learning history. Ready to start?",
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;
197
- } else if (chatMode === 'review') {
198
  currentMessages = reviewMessages;
199
  setCurrentMessages = setReviewMessages;
200
  } else {
@@ -202,42 +208,49 @@ function App() {
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[]> = {
213
- ask: [{
214
- id: '1',
215
- role: 'assistant',
216
- content: "👋 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!",
217
- timestamp: new Date(),
218
- }],
219
- review: [{
220
- id: 'review-1',
221
- role: 'assistant',
222
- content: "📚 Welcome to Review mode! I'll help you review and consolidate your learning. Let's go through what you've learned!",
223
- timestamp: new Date(),
224
- }],
225
- quiz: [{
226
- id: 'quiz-1',
227
- role: 'assistant',
228
- content: "🎯 Welcome to Quiz mode! I'll test your understanding with personalized questions based on your learning history. Ready to start?",
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<{
242
  currentQuestion: number;
243
  waitingForAnswer: boolean;
@@ -247,21 +260,18 @@ function App() {
247
  waitingForAnswer: false,
248
  showNextButton: false,
249
  });
 
250
  const [isTyping, setIsTyping] = useState(false);
251
  const [leftSidebarOpen, setLeftSidebarOpen] = useState(false);
252
  const [leftPanelVisible, setLeftPanelVisible] = useState(true);
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
260
- // const dismissed = localStorage.getItem('reviewBannerDismissed');
261
- // return !dismissed || dismissed === 'false';
262
- return true; // Force show for testing
263
  });
264
- const [resultType, setResultType] = useState<'export' | 'quiz' | 'summary' | null>(null);
265
  const [showClearDialog, setShowClearDialog] = useState(false);
266
 
267
  // Saved conversations/summaries
@@ -273,125 +283,51 @@ function App() {
273
 
274
  // Mock group members
275
  const [groupMembers] = useState<GroupMember[]>([
276
- { id: 'clare', name: 'Clare AI', email: 'clare@ai.assistant', isAI: true },
277
- { id: '1', name: 'Sarah Johnson', email: 'sarah.j@university.edu' },
278
- { id: '2', name: 'Michael Chen', email: 'michael.c@university.edu' },
279
- { id: '3', name: 'Emma Williams', email: 'emma.w@university.edu' },
280
  ]);
281
 
282
- // Workspaces - individual workspace uses user's avatar, group workspaces use group avatars
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',
373
- name: 'My Space',
374
- type: 'individual',
375
  avatar: userAvatar,
376
  },
377
  {
378
- id: 'group-1',
379
- name: 'CS 101 Study Group',
380
- type: 'group',
381
- avatar: 'https://api.dicebear.com/7.x/shapes/svg?seed=cs101group',
382
  members: groupMembers,
383
- category: 'course',
384
- courseName: course1Info?.name || 'CS 101',
385
  courseInfo: course1Info,
386
  },
387
  {
388
- id: 'group-2',
389
- name: 'AI Ethics Team',
390
- type: 'group',
391
- avatar: 'https://api.dicebear.com/7.x/shapes/svg?seed=aiethicsteam',
392
  members: groupMembers,
393
- category: 'course',
394
- courseName: course2Info?.name || 'AI Ethics',
395
  courseInfo: course2Info,
396
  },
397
  ]);
@@ -399,35 +335,56 @@ function App() {
399
  }, [user, groupMembers, availableCourses]);
400
 
401
  // Get current workspace
402
- const currentWorkspace = workspaces.find(w => w.id === currentWorkspaceId) || workspaces[0];
403
- const spaceType: SpaceType = currentWorkspace?.type || 'individual';
404
 
405
  // Keep current course in sync with workspace type
406
  useEffect(() => {
407
  if (!currentWorkspace) return;
408
- if (currentWorkspace.type === 'group' && currentWorkspace.category === 'course' && currentWorkspace.courseName) {
409
- setCurrentCourseId(currentWorkspace.courseName);
410
- } else if (currentWorkspace.type === 'individual') {
411
- const saved = localStorage.getItem('myspace_selected_course');
 
 
 
 
 
 
412
  if (saved) setCurrentCourseId(saved);
413
  }
414
  }, [currentWorkspaceId, currentWorkspace]);
415
 
416
  // Persist selection for My Space
417
  useEffect(() => {
418
- if (currentWorkspace?.type === 'individual') {
419
- localStorage.setItem('myspace_selected_course', currentCourseId);
420
  }
421
  }, [currentCourseId, currentWorkspace]);
422
 
423
  useEffect(() => {
424
- document.documentElement.classList.toggle('dark', isDarkMode);
425
- localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
426
  }, [isDarkMode]);
427
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
428
  const generateQuizQuestion = () => {
429
  const questions: Array<{
430
- type: 'multiple-choice' | 'fill-in-blank' | 'open-ended';
431
  question: string;
432
  options?: string[];
433
  correctAnswer: string;
@@ -435,35 +392,49 @@ function App() {
435
  sampleAnswer?: string;
436
  }> = [
437
  {
438
- type: 'multiple-choice',
439
  question: "Which of the following is NOT a principle of Responsible AI?",
440
  options: ["A) Fairness", "B) Transparency", "C) Profit Maximization", "D) Accountability"],
441
  correctAnswer: "C",
442
- explanation: "Profit Maximization is not a principle of Responsible AI. The key principles are Fairness, Transparency, and Accountability."
443
  },
444
  {
445
- type: 'fill-in-blank',
446
  question: "Algorithmic fairness ensures that AI systems do not discriminate against individuals based on their _____.",
447
  correctAnswer: "protected characteristics",
448
- explanation: "Protected characteristics include attributes like race, gender, age, religion, etc. AI systems should not discriminate based on these."
 
449
  },
450
  {
451
- type: 'open-ended',
452
  question: "Explain why transparency is important in AI systems.",
453
- correctAnswer: "Transparency allows users to understand how AI systems make decisions, which builds trust and enables accountability.",
454
- sampleAnswer: "Transparency allows users to understand how AI systems make decisions, which builds trust and enables accountability.",
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
467
  const sender: GroupMember = {
468
  id: user.email,
469
  name: user.name,
@@ -473,118 +444,114 @@ function App() {
473
 
474
  const userMessage: Message = {
475
  id: Date.now().toString(),
476
- role: 'user',
477
  content,
478
  timestamp: new Date(),
479
  sender,
480
  };
481
 
482
- // Add user message to the appropriate mode's message list
483
- if (chatMode === 'ask') {
484
- setAskMessages(prev => [...prev, userMessage]);
485
- } else if (chatMode === 'review') {
486
- setReviewMessages(prev => [...prev, userMessage]);
487
  } else {
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',
510
  content: `${feedback}\n\n${explanation}`,
511
  timestamp: new Date(),
512
- sender: spaceType === 'group' ? groupMembers.find(m => m.isAI) : undefined,
513
  showNextButton: true,
514
  };
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]);
522
- setQuizState(prev => ({ ...prev, waitingForAnswer: false, showNextButton: true }));
523
  }, 50);
524
  }, 2000);
525
  }
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
 
@@ -593,31 +560,29 @@ function App() {
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(),
603
- role: 'assistant',
604
  content: questionText,
605
  timestamp: new Date(),
606
- sender: spaceType === 'group' ? groupMembers.find(m => m.isAI) : undefined,
607
  questionData: question,
608
  };
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);
@@ -628,61 +593,78 @@ function App() {
628
  };
629
 
630
  const handleFileUpload = (files: File[]) => {
631
- const newFiles: UploadedFile[] = files.map(file => ({
632
  file,
633
- type: 'other' as FileType, // Default type
634
  }));
635
- setUploadedFiles(prev => [...prev, ...newFiles]);
636
  };
637
 
638
  const handleRemoveFile = (index: number) => {
639
- setUploadedFiles(prev => prev.filter((_, i) => i !== index));
640
  };
641
 
642
- const handleFileTypeChange = (index: number, type: FileType) => {
643
- setUploadedFiles(prev => prev.map((file, i) =>
644
- i === index ? { ...file, type } : file
645
- ));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
646
  };
647
 
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];
660
- return (
661
- savedMsg.id === currentMsg.id &&
662
- savedMsg.role === currentMsg.role &&
663
- savedMsg.content === currentMsg.content
664
- );
665
- });
666
- }) || null;
667
  };
668
 
669
  const handleSaveChat = () => {
670
  if (messages.length <= 1) {
671
- toast.info('No conversation to save');
672
  return;
673
  }
674
 
675
- // Check if already saved
676
  const existingChat = isCurrentChatSaved();
677
  if (existingChat) {
678
- // Unsave: remove from saved chats
679
  handleDeleteSavedChat(existingChat.id);
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 = {
687
  id: Date.now().toString(),
688
  title,
@@ -691,23 +673,20 @@ function App() {
691
  timestamp: new Date(),
692
  };
693
 
694
- setSavedChats(prev => [newChat, ...prev]);
695
  setLeftPanelVisible(true);
696
- toast.success('Chat saved!');
697
  };
698
 
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);
706
- } else if (savedChat.chatMode === 'review') {
707
  setReviewMessages(savedChat.messages);
708
  } else {
709
  setQuizMessages(savedChat.messages);
710
- // Reset quiz state
711
  setQuizState({
712
  currentQuestion: 0,
713
  waitingForAnswer: false,
@@ -715,19 +694,17 @@ function App() {
715
  });
716
  }
717
 
718
- toast.success('Chat loaded!');
719
  };
720
 
721
  const handleDeleteSavedChat = (id: string) => {
722
- setSavedChats(prev => prev.filter(chat => chat.id !== id));
723
- toast.success('Chat deleted');
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');
731
  };
732
 
733
  const handleClearConversation = (shouldSave: boolean = false) => {
@@ -736,30 +713,36 @@ function App() {
736
  }
737
 
738
  const initialMessages: Record<ChatMode, Message[]> = {
739
- ask: [{
740
- id: '1',
741
- role: 'assistant',
742
- content: "👋 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!",
743
- timestamp: new Date(),
744
- }],
745
- review: [{
746
- id: 'review-1',
747
- role: 'assistant',
748
- content: "📚 Welcome to Review mode! I'll help you review and consolidate your learning. Let's go through what you've learned!",
749
- timestamp: new Date(),
750
- }],
751
- quiz: [{
752
- id: 'quiz-1',
753
- role: 'assistant',
754
- content: "🎯 Welcome to Quiz mode! I'll test your understanding with personalized questions based on your learning history. Ready to start?",
755
- timestamp: new Date(),
756
- }],
 
 
 
 
 
 
 
757
  };
758
 
759
- // Clear only the current mode's conversation
760
- if (chatMode === 'ask') {
761
  setAskMessages(initialMessages.ask);
762
- } else if (chatMode === 'review') {
763
  setReviewMessages(initialMessages.review);
764
  } else {
765
  setQuizMessages(initialMessages.quiz);
@@ -771,128 +754,54 @@ function App() {
771
  }
772
  };
773
 
774
- const handleExport = () => {
775
- const result = `# Conversation Export
776
- Date: ${new Date().toLocaleDateString()}
777
- Student: ${user?.name}
778
-
779
- ## Summary
780
- This conversation covered key concepts in Module 10 – Responsible AI, including ethical considerations, fairness, transparency, and accountability in AI systems.
781
-
782
- ## Key Takeaways
783
- 1. Understanding the principles of Responsible AI
784
- 2. Real-world applications and implications
785
- 3. Best practices for ethical AI development
786
-
787
- Exported successfully! ✓`;
788
-
789
- setExportResult(result);
790
- setResultType('export');
791
- toast.success('Conversation exported!');
792
- };
793
-
794
- const handleQuiz = () => {
795
- const quiz = `# Micro-Quiz: Responsible AI
796
-
797
- **Question 1:** Which of the following is a key principle of Responsible AI?
798
- a) Profit maximization
799
- b) Transparency
800
- c) Rapid deployment
801
- d) Cost reduction
802
-
803
- **Question 2:** What is algorithmic fairness?
804
- (Short answer expected)
805
-
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!');
813
- };
814
-
815
- const handleSummary = () => {
816
- const summary = `# Learning Summary
817
-
818
- ## Today's Session
819
- **Duration:** 25 minutes
820
- **Topics Covered:** 3
821
- **Messages Exchanged:** 12
822
-
823
- ## Key Concepts Discussed
824
- • Principles of Responsible AI
825
- • Ethical considerations in AI development
826
- • Fairness and transparency in algorithms
827
-
828
- ## Recommended Next Steps
829
- 1. Review Module 10, Section 2.3
830
- 2. Complete practice quiz on algorithmic fairness
831
- 3. Read additional resources on AI ethics
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!');
839
- };
840
-
841
  const handleSave = (
842
  content: string,
843
- type: 'export' | 'quiz' | 'summary',
844
  saveAsChat: boolean = false,
845
- format: 'pdf' | 'text' = 'text',
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') {
853
- // Convert the export result to a chat format
854
- // Create messages from the export content
855
  const chatMessages: Message[] = [
856
  {
857
- id: '1',
858
- role: 'assistant',
859
- content: "👋 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!",
 
860
  timestamp: new Date(),
861
  },
862
  {
863
  id: Date.now().toString(),
864
- role: 'assistant',
865
  content,
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(),
873
  title: `${title} - ${new Date().toLocaleDateString()}`,
874
  messages: chatMessages,
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);
888
  if (existingItem) {
889
- // Unsave: remove from saved items
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 = {
897
  id: Date.now().toString(),
898
  title: `${title} - ${new Date().toLocaleDateString()}`,
@@ -904,149 +813,118 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
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
 
919
  const handleUnsave = (id: string) => {
920
- setSavedItems(prev => prev.filter(item => item.id !== id));
921
- toast.success('Removed from saved items');
922
  };
923
 
924
  // Create a new group workspace
925
- const handleCreateWorkspace = (payload: {
926
- name: string;
927
- category: 'course' | 'personal';
928
- courseId?: string;
929
- invites: string[];
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,
937
- name: user.name,
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 => ({
945
  id: email,
946
- name: email.split('@')[0] || email,
947
  email,
948
- }))
949
  ];
950
 
951
  let newWorkspace: Workspace;
952
 
953
- if (payload.category === 'course') {
954
- const courseInfo = availableCourses.find(c => c.id === payload.courseId);
955
  newWorkspace = {
956
  id,
957
  name: payload.name,
958
- type: 'group',
959
  avatar,
960
  members,
961
- category: 'course',
962
- courseName: courseInfo?.name || 'Untitled Course',
963
  courseInfo,
964
  };
965
  } else {
966
- // Personal interest workspace
967
  newWorkspace = {
968
  id,
969
  name: payload.name,
970
- type: 'group',
971
  avatar,
972
  members,
973
- category: 'personal',
974
  isEditable: true,
975
  };
976
  }
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
 
989
- // Handle review click - switch to review mode
990
  const handleReviewClick = () => {
991
- setChatMode('review');
992
  setShowReviewBanner(false);
993
- localStorage.setItem('reviewBannerDismissed', 'true');
994
  };
995
 
996
- // Handle dismiss review banner
997
  const handleDismissReviewBanner = () => {
998
  setShowReviewBanner(false);
999
- localStorage.setItem('reviewBannerDismissed', 'true');
1000
  };
1001
 
1002
- // Handle login - check if user needs onboarding
1003
  const handleLogin = (newUser: User) => {
1004
  setUser(newUser);
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
 
1012
- // Handle onboarding completion
1013
  const handleOnboardingComplete = (updatedUser: User) => {
1014
  setUser(updatedUser);
1015
  setShowOnboarding(false);
1016
- // For testing: don't save to localStorage (comment out)
1017
- // localStorage.setItem(`onboarding_completed_${updatedUser.email}`, 'true');
1018
  };
1019
 
1020
- // Handle onboarding skip
1021
  const handleOnboardingSkip = () => {
1022
- // For testing: don't save to localStorage (comment out)
1023
- // if (user) {
1024
- // localStorage.setItem(`onboarding_completed_${user.email}`, 'true');
1025
- // }
1026
  setShowOnboarding(false);
1027
  };
1028
 
1029
- // Show login screen if user is not logged in
1030
  if (!user) {
1031
  return <LoginScreen onLogin={handleLogin} />;
1032
  }
1033
 
1034
- // Show onboarding if user just logged in and hasn't completed it
1035
  if (showOnboarding && user) {
1036
- return (
1037
- <>
1038
- <Onboarding
1039
- user={user}
1040
- onComplete={handleOnboardingComplete}
1041
- onSkip={handleOnboardingSkip}
1042
- />
1043
- </>
1044
- );
1045
  }
1046
 
1047
  return (
1048
  <div className="min-h-screen bg-background flex flex-col">
1049
  <Toaster />
 
1050
  <Header
1051
  user={user}
1052
  onMenuClick={() => setLeftSidebarOpen(!leftSidebarOpen)}
@@ -1063,66 +941,49 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
1063
  availableCourses={availableCourses}
1064
  onUserUpdate={setUser}
1065
  />
 
1066
  {showProfileEditor && user && (
1067
- <ProfileEditor
1068
- user={user}
1069
- onSave={setUser}
1070
- onClose={() => setShowProfileEditor(false)}
1071
- />
1072
  )}
1073
 
1074
- {/* Review Banner - Below Header */}
1075
  {showReviewBanner && (
1076
  <div className="w-full bg-background border-b border-border flex-shrink-0 relative z-50">
1077
- <ReviewBanner
1078
- onReview={handleReviewClick}
1079
- onDismiss={handleDismissReviewBanner}
1080
- />
1081
  </div>
1082
  )}
1083
 
1084
- <div
1085
- className="flex-1 flex overflow-hidden h-[calc(100vh-4rem)] relative"
1086
- style={{ overscrollBehavior: 'none' }}
1087
- >
1088
- {/* Toggle Button - When panel is closed, at left edge, center axis aligned to left edge */}
1089
  {!leftPanelVisible && (
1090
  <Button
1091
  variant="secondary"
1092
  size="icon"
1093
  onClick={() => setLeftPanelVisible(true)}
1094
  className="hidden lg:flex absolute z-[100] h-8 w-5 shadow-lg rounded-full bg-card border border-border transition-all duration-200 ease-in-out hover:translate-x-[10px]"
1095
- style={{ left: '-5px', top: '1rem' }}
1096
  title="Open panel"
1097
  >
1098
  <ChevronRight className="h-3 w-3" />
1099
  </Button>
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
- />
1107
- )}
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
  >
1115
- {/* Toggle Button - Inside panel, right edge aligned to panel right edge */}
1116
  <Button
1117
  variant="secondary"
1118
  size="icon"
1119
  onClick={() => setLeftPanelVisible(false)}
1120
  className="absolute z-[70] h-8 w-5 shadow-lg rounded-full bg-card border border-border"
1121
- style={{ right: '-10px', top: '1rem' }}
1122
  title="Close panel"
1123
  >
1124
  <ChevronLeft className="h-3 w-3" />
1125
  </Button>
 
1126
  <LeftSidebar
1127
  learningMode={learningMode}
1128
  language={language}
@@ -1151,13 +1012,12 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
1151
  </aside>
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
1159
  transform transition-transform duration-300 ease-in-out
1160
- ${leftSidebarOpen ? 'translate-x-0' : '-translate-x-full'}
1161
  flex flex-col
1162
  mt-16
1163
  h-[calc(100vh-4rem)]
@@ -1166,14 +1026,11 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
1166
  >
1167
  <div className="p-4 border-b border-border flex justify-between items-center">
1168
  <h3>Settings & Guide</h3>
1169
- <Button
1170
- variant="ghost"
1171
- size="icon"
1172
- onClick={() => setLeftSidebarOpen(false)}
1173
- >
1174
  <X className="h-5 w-5" />
1175
  </Button>
1176
  </div>
 
1177
  <LeftSidebar
1178
  learningMode={learningMode}
1179
  language={language}
@@ -1200,7 +1057,6 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
1200
  />
1201
  </aside>
1202
 
1203
- {/* Main Chat Area */}
1204
  <main className="flex-1 flex flex-col min-w-0 min-h-0 h-full">
1205
  <ChatArea
1206
  messages={messages}
@@ -1231,7 +1087,7 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
1231
  savedChats={savedChats}
1232
  workspaces={workspaces}
1233
  currentWorkspaceId={currentWorkspaceId}
1234
- onSaveFile={(content, type, _format, targetWorkspaceId) => handleSave(content, type, false, _format ?? 'text', targetWorkspaceId)}
1235
  leftPanelVisible={leftPanelVisible}
1236
  currentCourseId={currentCourseId}
1237
  onCourseChange={setCurrentCourseId}
@@ -1239,7 +1095,6 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
1239
  showReviewBanner={showReviewBanner}
1240
  />
1241
  </main>
1242
-
1243
  </div>
1244
  </div>
1245
  );
 
1
+ // web/src/App.tsx
2
+ import React, { useState, useEffect, useRef } from "react";
3
+ import { Header } from "./components/Header";
4
+ import { LeftSidebar } from "./components/LeftSidebar";
5
+ import { ChatArea } from "./components/ChatArea";
6
+ import { LoginScreen } from "./components/LoginScreen";
7
+ import { ProfileEditor } from "./components/ProfileEditor";
8
+ import { ReviewBanner } from "./components/ReviewBanner";
9
+ import { Onboarding } from "./components/Onboarding";
10
+ import { X, ChevronLeft, ChevronRight } from "lucide-react";
11
+ import { Button } from "./components/ui/button";
12
+ import { Toaster } from "./components/ui/sonner";
13
+ import { toast } from "sonner";
14
+
15
+ // ✅ NEW: backend API bindings
16
+ import { apiChat, apiUpload, apiMemoryline } from "./lib/api";
17
 
18
  export interface Message {
19
  id: string;
20
+ role: "user" | "assistant";
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;
29
  options?: string[];
30
  correctAnswer: string;
 
46
  isAI?: boolean;
47
  }
48
 
49
+ export type SpaceType = "individual" | "group";
50
 
51
  export interface CourseInfo {
52
  id: string;
 
67
  type: SpaceType;
68
  avatar: string;
69
  members?: GroupMember[];
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";
77
 
78
  export interface UploadedFile {
79
  file: File;
80
  type: FileType;
81
  }
82
 
83
+ export type LearningMode = "general" | "concept" | "socratic" | "exam" | "assignment" | "summary";
84
+ export type Language = "auto" | "en" | "zh";
85
+ export type ChatMode = "ask" | "review" | "quiz";
86
 
87
  export interface SavedItem {
88
  id: string;
89
  title: string;
90
  content: string;
91
+ type: "export" | "quiz" | "summary";
92
  timestamp: Date;
93
  isSaved: boolean;
94
+ format?: "pdf" | "text";
95
  workspaceId: string;
96
  }
97
 
 
103
  timestamp: Date;
104
  }
105
 
106
+ const DOC_TYPE_MAP: Record<FileType, string> = {
107
+ syllabus: "Syllabus",
108
+ "lecture-slides": "Lecture Slides / PPT",
109
+ "literature-review": "Literature Review / Paper",
110
+ other: "Other Course Document",
111
  };
112
 
113
+ function mapLanguagePref(lang: Language): string {
114
+ if (lang === "zh") return "中文";
115
+ if (lang === "en") return "English";
116
+ return "Auto";
117
+ }
 
118
 
119
  function App() {
120
  const [isDarkMode, setIsDarkMode] = useState(() => {
121
+ const saved = localStorage.getItem("theme");
122
+ return saved === "dark" || (!saved && window.matchMedia("(prefers-color-scheme: dark)").matches);
123
  });
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",
134
+ name: "Introduction to AI",
135
+ instructor: { name: "Dr. Sarah Johnson", email: "sarah.johnson@university.edu" },
136
+ teachingAssistant: { name: "Michael Chen", email: "michael.chen@university.edu" },
137
  },
138
  {
139
+ id: "course2",
140
+ name: "Machine Learning",
141
+ instructor: { name: "Prof. David Lee", email: "david.lee@university.edu" },
142
+ teachingAssistant: { name: "Emily Zhang", email: "emily.zhang@university.edu" },
143
  },
144
  {
145
+ id: "course3",
146
+ name: "Data Structures",
147
+ instructor: { name: "Dr. Robert Smith", email: "robert.smith@university.edu" },
148
+ teachingAssistant: { name: "Lisa Wang", email: "lisa.wang@university.edu" },
149
  },
150
  {
151
+ id: "course4",
152
+ name: "Web Development",
153
+ instructor: { name: "Prof. Maria Garcia", email: "maria.garcia@university.edu" },
154
+ teachingAssistant: { name: "James Brown", email: "james.brown@university.edu" },
155
  },
156
  ];
157
 
158
  // Separate messages for each chat mode
159
  const [askMessages, setAskMessages] = useState<Message[]>([
160
  {
161
+ id: "1",
162
+ role: "assistant",
163
+ content:
164
+ "👋 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!",
165
  timestamp: new Date(),
166
+ },
167
  ]);
168
+
169
  const [reviewMessages, setReviewMessages] = useState<Message[]>([
170
  {
171
+ id: "review-1",
172
+ role: "assistant",
173
  content: "📚 Welcome to Review mode! I'll help you review and consolidate your learning. Let's go through what you've learned!",
174
  timestamp: new Date(),
175
+ },
176
  ]);
177
+
178
  const [quizMessages, setQuizMessages] = useState<Message[]>([
179
  {
180
+ id: "quiz-1",
181
+ role: "assistant",
182
  content: "🎯 Welcome to Quiz mode! I'll test your understanding with personalized questions based on your learning history. Ready to start?",
183
  timestamp: new Date(),
184
+ },
185
  ]);
186
 
187
+ const [learningMode, setLearningMode] = useState<LearningMode>("concept");
188
+ const [language, setLanguage] = useState<Language>("auto");
189
+ const [chatMode, setChatMode] = useState<ChatMode>("ask");
190
 
191
+ const messages = chatMode === "ask" ? askMessages : chatMode === "review" ? reviewMessages : quizMessages;
 
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;
199
 
200
+ if (chatMode === "ask") {
201
  currentMessages = askMessages;
202
  setCurrentMessages = setAskMessages;
203
+ } else if (chatMode === "review") {
204
  currentMessages = reviewMessages;
205
  setCurrentMessages = setReviewMessages;
206
  } else {
 
208
  setCurrentMessages = setQuizMessages;
209
  }
210
 
211
+ const hasUserMessages = currentMessages.some((msg) => msg.role === "user");
212
+ const expectedWelcomeId = chatMode === "ask" ? "1" : chatMode === "review" ? "review-1" : "quiz-1";
213
+ const hasWelcomeMessage = currentMessages.some((msg) => msg.id === expectedWelcomeId && msg.role === "assistant");
214
  const modeChanged = prevChatModeRef.current !== chatMode;
215
 
 
216
  if ((modeChanged || currentMessages.length === 0 || !hasWelcomeMessage) && !hasUserMessages) {
217
  const initialMessages: Record<ChatMode, Message[]> = {
218
+ ask: [
219
+ {
220
+ id: "1",
221
+ role: "assistant",
222
+ content:
223
+ "👋 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!",
224
+ timestamp: new Date(),
225
+ },
226
+ ],
227
+ review: [
228
+ {
229
+ id: "review-1",
230
+ role: "assistant",
231
+ content: "📚 Welcome to Review mode! I'll help you review and consolidate your learning. Let's go through what you've learned!",
232
+ timestamp: new Date(),
233
+ },
234
+ ],
235
+ quiz: [
236
+ {
237
+ id: "quiz-1",
238
+ role: "assistant",
239
+ content: "🎯 Welcome to Quiz mode! I'll test your understanding with personalized questions based on your learning history. Ready to start?",
240
+ timestamp: new Date(),
241
+ },
242
+ ],
243
  };
244
 
245
  setCurrentMessages(initialMessages[chatMode]);
246
  }
247
 
248
  prevChatModeRef.current = chatMode;
249
+ }, [chatMode, askMessages.length, reviewMessages.length, quizMessages.length]);
250
 
251
  const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
252
  const [memoryProgress, setMemoryProgress] = useState(36);
253
+
254
  const [quizState, setQuizState] = useState<{
255
  currentQuestion: number;
256
  waitingForAnswer: boolean;
 
260
  waitingForAnswer: false,
261
  showNextButton: false,
262
  });
263
+
264
  const [isTyping, setIsTyping] = useState(false);
265
  const [leftSidebarOpen, setLeftSidebarOpen] = useState(false);
266
  const [leftPanelVisible, setLeftPanelVisible] = useState(true);
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
 
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" },
288
+ { id: "2", name: "Michael Chen", email: "michael.c@university.edu" },
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",
316
+ type: "group",
317
+ avatar: "https://api.dicebear.com/7.x/shapes/svg?seed=cs101group",
318
  members: groupMembers,
319
+ category: "course",
320
+ courseName: course1Info?.name || "CS 101",
321
  courseInfo: course1Info,
322
  },
323
  {
324
+ id: "group-2",
325
+ name: "AI Ethics Team",
326
+ type: "group",
327
+ avatar: "https://api.dicebear.com/7.x/shapes/svg?seed=aiethicsteam",
328
  members: groupMembers,
329
+ category: "course",
330
+ courseName: course2Info?.name || "AI Ethics",
331
  courseInfo: course2Info,
332
  },
333
  ]);
 
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;
350
+ }
351
+
352
+ if (currentWorkspace.type === "individual") {
353
+ const saved = localStorage.getItem("myspace_selected_course");
354
  if (saved) setCurrentCourseId(saved);
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);
362
  }
363
  }, [currentCourseId, currentWorkspace]);
364
 
365
  useEffect(() => {
366
+ document.documentElement.classList.toggle("dark", isDarkMode);
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
+
374
+ (async () => {
375
+ try {
376
+ const r = await apiMemoryline(user.email);
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]);
384
+
385
  const generateQuizQuestion = () => {
386
  const questions: Array<{
387
+ type: "multiple-choice" | "fill-in-blank" | "open-ended";
388
  question: string;
389
  options?: string[];
390
  correctAnswer: string;
 
392
  sampleAnswer?: string;
393
  }> = [
394
  {
395
+ type: "multiple-choice",
396
  question: "Which of the following is NOT a principle of Responsible AI?",
397
  options: ["A) Fairness", "B) Transparency", "C) Profit Maximization", "D) Accountability"],
398
  correctAnswer: "C",
399
+ explanation: "Profit Maximization is not a principle of Responsible AI. The key principles are Fairness, Transparency, and Accountability.",
400
  },
401
  {
402
+ type: "fill-in-blank",
403
  question: "Algorithmic fairness ensures that AI systems do not discriminate against individuals based on their _____.",
404
  correctAnswer: "protected characteristics",
405
+ explanation:
406
+ "Protected characteristics include attributes like race, gender, age, religion, etc. AI systems should not discriminate based on these.",
407
  },
408
  {
409
+ type: "open-ended",
410
  question: "Explain why transparency is important in AI systems.",
411
+ correctAnswer:
412
+ "Transparency allows users to understand how AI systems make decisions, which builds trust and enables accountability.",
413
+ sampleAnswer:
414
+ "Transparency allows users to understand how AI systems make decisions, which builds trust and enables accountability.",
415
+ explanation:
416
+ "Transparency is crucial because it helps users understand AI decision-making processes, enables debugging, and ensures accountability.",
417
+ },
418
  ];
419
 
420
  const randomIndex = Math.floor(Math.random() * questions.length);
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];
430
+ return DOC_TYPE_MAP[last.type] || "Syllabus";
431
+ }
432
+ return "Syllabus";
433
+ };
434
+
435
  const handleSendMessage = async (content: string) => {
436
  if (!content.trim() || !user) return;
437
 
 
438
  const sender: GroupMember = {
439
  id: user.email,
440
  name: user.name,
 
444
 
445
  const userMessage: Message = {
446
  id: Date.now().toString(),
447
+ role: "user",
448
  content,
449
  timestamp: new Date(),
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;
 
 
465
  setIsTyping(true);
 
 
 
 
466
 
467
+ setTimeout(() => {
468
+ const feedback = isCorrect ? "✅ Correct! Great job!" : "❌ Not quite right, but good effort!";
469
+ const explanation =
470
+ "Here's the explanation: The correct answer demonstrates understanding of the key concepts. Let me break it down for you...";
471
 
472
  const assistantMessage: Message = {
473
  id: (Date.now() + 1).toString(),
474
+ role: "assistant",
475
  content: `${feedback}\n\n${explanation}`,
476
  timestamp: new Date(),
477
+ sender: spaceType === "group" ? groupMembers.find((m) => m.isAI) : undefined,
478
  showNextButton: true,
479
  };
480
 
 
481
  setIsTyping(false);
 
 
482
  setTimeout(() => {
483
+ setQuizMessages((prev) => [...prev, assistantMessage]);
484
+ setQuizState((prev) => ({ ...prev, waitingForAnswer: false, showNextButton: true }));
485
  }, 50);
486
  }, 2000);
487
  }
488
  return;
489
  }
490
 
491
+ // Ask / Review: real backend call
 
 
 
 
 
492
  setIsTyping(true);
 
493
  try {
494
+ const docType = getCurrentDocTypeForChat();
495
 
496
+ const r = await apiChat({
497
  user_id: user.email,
498
  message: content,
499
+ learning_mode: learningMode,
500
+ language_preference: mapLanguagePref(language),
501
  doc_type: docType,
502
  });
503
 
504
+ const refs = (r.refs || [])
505
+ .map((x) => {
506
+ const a = x?.source_file ? String(x.source_file) : "";
507
+ const b = x?.section ? String(x.section) : "";
508
+ const s = `${a}${a && b ? " — " : ""}${b}`.trim();
509
+ return s || null;
510
+ })
511
+ .filter(Boolean) as string[];
512
+
513
  const assistantMessage: Message = {
514
  id: (Date.now() + 1).toString(),
515
+ role: "assistant",
516
+ content: r.reply || "",
517
  timestamp: new Date(),
518
+ references: refs.length ? refs : undefined,
519
+ sender: spaceType === "group" ? groupMembers.find((m) => m.isAI) : undefined,
520
  };
521
 
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));
536
+ } catch {
537
+ // ignore
538
+ }
539
+ } catch (e: any) {
540
  setIsTyping(false);
541
+ toast.error(e?.message || "Chat failed");
542
 
543
  const assistantMessage: Message = {
544
  id: (Date.now() + 1).toString(),
545
+ role: "assistant",
546
+ content: "Sorry — chat request failed. Please try again.",
547
  timestamp: new Date(),
548
+ sender: spaceType === "group" ? groupMembers.find((m) => m.isAI) : undefined,
549
  };
550
 
551
  setTimeout(() => {
552
+ if (chatMode === "ask") setAskMessages((prev) => [...prev, assistantMessage]);
553
+ if (chatMode === "review") setReviewMessages((prev) => [...prev, assistantMessage]);
 
 
 
554
  }, 50);
 
 
555
  }
556
  };
557
 
 
560
  const question = generateQuizQuestion();
561
  let questionText = question.question;
562
 
563
+ if (question.type === "multiple-choice") {
564
+ questionText += "\n\n" + (question.options || []).join("\n");
565
  }
566
 
567
  setTimeout(() => {
568
  const assistantMessage: Message = {
569
  id: Date.now().toString(),
570
+ role: "assistant",
571
  content: questionText,
572
  timestamp: new Date(),
573
+ sender: spaceType === "group" ? groupMembers.find((m) => m.isAI) : undefined,
574
  questionData: question,
575
  };
576
 
 
577
  setIsTyping(false);
578
 
 
579
  setTimeout(() => {
580
+ setQuizMessages((prev) => [...prev, assistantMessage]);
581
+ setQuizState((prev) => ({
582
  ...prev,
583
  currentQuestion: prev.currentQuestion + 1,
584
  waitingForAnswer: true,
585
+ showNextButton: false,
586
  }));
587
  }, 50);
588
  }, 2000);
 
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
 
603
  const handleRemoveFile = (index: number) => {
604
+ setUploadedFiles((prev) => prev.filter((_, i) => i !== index));
605
  };
606
 
607
+ const handleFileTypeChange = async (index: number, type: FileType) => {
608
+ // update FE state first
609
+ setUploadedFiles((prev) =>
610
+ prev.map((file, i) => (i === index ? { ...file, type } : file))
611
+ );
612
+
613
+ // upload to backend once (best effort)
614
+ if (!user) return;
615
+
616
+ const target = uploadedFiles[index];
617
+ const file = target?.file;
618
+ if (!file) return;
619
+
620
+ const fp = `${file.name}::${file.size}::${file.lastModified}`;
621
+ if (uploadedFingerprintsRef.current.has(fp)) return;
622
+
623
+ uploadedFingerprintsRef.current.add(fp);
624
+
625
+ try {
626
+ await apiUpload({
627
+ user_id: user.email,
628
+ doc_type: DOC_TYPE_MAP[type] || "Other Course Document",
629
+ file,
630
+ });
631
+ toast.success("File uploaded to backend");
632
+ } catch (e: any) {
633
+ toast.error(e?.message || "Upload failed");
634
+ }
635
  };
636
 
637
  // Helper function to check if current chat is already saved
638
  const isCurrentChatSaved = (): SavedChat | null => {
639
  if (messages.length <= 1) return null;
640
 
641
+ return (
642
+ savedChats.find((chat) => {
643
+ if (chat.chatMode !== chatMode) return false;
644
+ if (chat.messages.length !== messages.length) return false;
645
+
646
+ return chat.messages.every((savedMsg, idx) => {
647
+ const currentMsg = messages[idx];
648
+ return savedMsg.id === currentMsg.id && savedMsg.role === currentMsg.role && savedMsg.content === currentMsg.content;
649
+ });
650
+ }) || null
651
+ );
 
 
 
 
652
  };
653
 
654
  const handleSaveChat = () => {
655
  if (messages.length <= 1) {
656
+ toast.info("No conversation to save");
657
  return;
658
  }
659
 
 
660
  const existingChat = isCurrentChatSaved();
661
  if (existingChat) {
 
662
  handleDeleteSavedChat(existingChat.id);
663
+ toast.success("Chat unsaved");
664
  return;
665
  }
666
 
667
+ const title = `Chat - ${chatMode === "ask" ? "Ask" : chatMode === "review" ? "Review" : "Quiz"} - ${new Date().toLocaleDateString()}`;
 
668
  const newChat: SavedChat = {
669
  id: Date.now().toString(),
670
  title,
 
673
  timestamp: new Date(),
674
  };
675
 
676
+ setSavedChats((prev) => [newChat, ...prev]);
677
  setLeftPanelVisible(true);
678
+ toast.success("Chat saved!");
679
  };
680
 
681
  const handleLoadChat = (savedChat: SavedChat) => {
 
682
  setChatMode(savedChat.chatMode);
683
 
684
+ if (savedChat.chatMode === "ask") {
 
685
  setAskMessages(savedChat.messages);
686
+ } else if (savedChat.chatMode === "review") {
687
  setReviewMessages(savedChat.messages);
688
  } else {
689
  setQuizMessages(savedChat.messages);
 
690
  setQuizState({
691
  currentQuestion: 0,
692
  waitingForAnswer: false,
 
694
  });
695
  }
696
 
697
+ toast.success("Chat loaded!");
698
  };
699
 
700
  const handleDeleteSavedChat = (id: string) => {
701
+ setSavedChats((prev) => prev.filter((chat) => chat.id !== id));
702
+ toast.success("Chat deleted");
703
  };
704
 
705
  const handleRenameSavedChat = (id: string, newTitle: string) => {
706
+ setSavedChats((prev) => prev.map((chat) => (chat.id === id ? { ...chat, title: newTitle } : chat)));
707
+ toast.success("Chat renamed");
 
 
708
  };
709
 
710
  const handleClearConversation = (shouldSave: boolean = false) => {
 
713
  }
714
 
715
  const initialMessages: Record<ChatMode, Message[]> = {
716
+ ask: [
717
+ {
718
+ id: "1",
719
+ role: "assistant",
720
+ content:
721
+ "👋 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!",
722
+ timestamp: new Date(),
723
+ },
724
+ ],
725
+ review: [
726
+ {
727
+ id: "review-1",
728
+ role: "assistant",
729
+ content: "📚 Welcome to Review mode! I'll help you review and consolidate your learning. Let's go through what you've learned!",
730
+ timestamp: new Date(),
731
+ },
732
+ ],
733
+ quiz: [
734
+ {
735
+ id: "quiz-1",
736
+ role: "assistant",
737
+ content: "🎯 Welcome to Quiz mode! I'll test your understanding with personalized questions based on your learning history. Ready to start?",
738
+ timestamp: new Date(),
739
+ },
740
+ ],
741
  };
742
 
743
+ if (chatMode === "ask") {
 
744
  setAskMessages(initialMessages.ask);
745
+ } else if (chatMode === "review") {
746
  setReviewMessages(initialMessages.review);
747
  } else {
748
  setQuizMessages(initialMessages.quiz);
 
754
  }
755
  };
756
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
757
  const handleSave = (
758
  content: string,
759
+ type: "export" | "quiz" | "summary",
760
  saveAsChat: boolean = false,
761
+ format: "pdf" | "text" = "text",
762
  workspaceId?: string
763
  ) => {
764
  if (!content.trim()) return;
765
 
766
+ if (saveAsChat && type !== "summary") {
 
 
 
 
767
  const chatMessages: Message[] = [
768
  {
769
+ id: "1",
770
+ role: "assistant",
771
+ content:
772
+ "👋 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!",
773
  timestamp: new Date(),
774
  },
775
  {
776
  id: Date.now().toString(),
777
+ role: "assistant",
778
  content,
779
  timestamp: new Date(),
780
+ },
781
  ];
782
 
783
+ const title = type === "export" ? "Exported Conversation" : "Micro-Quiz";
784
  const newChat: SavedChat = {
785
  id: Date.now().toString(),
786
  title: `${title} - ${new Date().toLocaleDateString()}`,
787
  messages: chatMessages,
788
+ chatMode: "ask",
789
  timestamp: new Date(),
790
  };
791
 
792
+ setSavedChats((prev) => [newChat, ...prev]);
793
  setLeftPanelVisible(true);
794
+ toast.success("Chat saved!");
795
  return;
796
  }
797
 
798
+ const existingItem = savedItems.find((item) => item.content === content && item.type === type);
 
 
799
  if (existingItem) {
 
800
  handleUnsave(existingItem.id);
801
  return;
802
  }
803
 
804
+ const title = type === "export" ? "Exported Conversation" : type === "quiz" ? "Micro-Quiz" : "Summarization";
 
805
  const newItem: SavedItem = {
806
  id: Date.now().toString(),
807
  title: `${title} - ${new Date().toLocaleDateString()}`,
 
813
  workspaceId: workspaceId || currentWorkspaceId,
814
  };
815
 
816
+ setSavedItems((prev) => [newItem, ...prev]);
817
  setRecentlySavedId(newItem.id);
818
+ setLeftPanelVisible(true);
819
 
 
820
  setTimeout(() => {
821
  setRecentlySavedId(null);
822
  }, 2000);
823
 
824
+ toast.success("Saved for later!");
825
  };
826
 
827
  const handleUnsave = (id: string) => {
828
+ setSavedItems((prev) => prev.filter((item) => item.id !== id));
829
+ toast.success("Removed from saved items");
830
  };
831
 
832
  // Create a new group workspace
833
+ const handleCreateWorkspace = (payload: { name: string; category: "course" | "personal"; courseId?: string; invites: string[] }) => {
 
 
 
 
 
834
  const id = `group-${Date.now()}`;
835
  const avatar = `https://api.dicebear.com/7.x/shapes/svg?seed=${encodeURIComponent(payload.name)}`;
836
 
837
+ const creatorMember: GroupMember = user
838
+ ? {
839
+ id: user.email,
840
+ name: user.name,
841
+ email: user.email,
842
+ avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}`,
843
+ }
844
+ : { id: "unknown", name: "Unknown", email: "unknown@email.com" };
845
 
846
  const members: GroupMember[] = [
847
  creatorMember,
848
+ ...payload.invites.map((email) => ({
849
  id: email,
850
+ name: email.split("@")[0] || email,
851
  email,
852
+ })),
853
  ];
854
 
855
  let newWorkspace: Workspace;
856
 
857
+ if (payload.category === "course") {
858
+ const courseInfo = availableCourses.find((c) => c.id === payload.courseId);
859
  newWorkspace = {
860
  id,
861
  name: payload.name,
862
+ type: "group",
863
  avatar,
864
  members,
865
+ category: "course",
866
+ courseName: courseInfo?.name || "Untitled Course",
867
  courseInfo,
868
  };
869
  } else {
 
870
  newWorkspace = {
871
  id,
872
  name: payload.name,
873
+ type: "group",
874
  avatar,
875
  members,
876
+ category: "personal",
877
  isEditable: true,
878
  };
879
  }
880
 
881
+ setWorkspaces((prev) => [...prev, newWorkspace]);
882
  setCurrentWorkspaceId(id);
883
 
884
+ if (payload.category === "course" && payload.courseId) {
 
885
  setCurrentCourseId(payload.courseId);
886
  }
887
 
888
+ toast.success("New group workspace created");
889
  };
890
 
 
891
  const handleReviewClick = () => {
892
+ setChatMode("review");
893
  setShowReviewBanner(false);
894
+ localStorage.setItem("reviewBannerDismissed", "true");
895
  };
896
 
 
897
  const handleDismissReviewBanner = () => {
898
  setShowReviewBanner(false);
899
+ localStorage.setItem("reviewBannerDismissed", "true");
900
  };
901
 
 
902
  const handleLogin = (newUser: User) => {
903
  setUser(newUser);
 
 
 
904
  setShowOnboarding(true);
 
905
  };
906
 
 
907
  const handleOnboardingComplete = (updatedUser: User) => {
908
  setUser(updatedUser);
909
  setShowOnboarding(false);
 
 
910
  };
911
 
 
912
  const handleOnboardingSkip = () => {
 
 
 
 
913
  setShowOnboarding(false);
914
  };
915
 
 
916
  if (!user) {
917
  return <LoginScreen onLogin={handleLogin} />;
918
  }
919
 
 
920
  if (showOnboarding && user) {
921
+ return <Onboarding user={user} onComplete={handleOnboardingComplete} onSkip={handleOnboardingSkip} />;
 
 
 
 
 
 
 
 
922
  }
923
 
924
  return (
925
  <div className="min-h-screen bg-background flex flex-col">
926
  <Toaster />
927
+
928
  <Header
929
  user={user}
930
  onMenuClick={() => setLeftSidebarOpen(!leftSidebarOpen)}
 
941
  availableCourses={availableCourses}
942
  onUserUpdate={setUser}
943
  />
944
+
945
  {showProfileEditor && user && (
946
+ <ProfileEditor user={user} onSave={setUser} onClose={() => setShowProfileEditor(false)} />
 
 
 
 
947
  )}
948
 
 
949
  {showReviewBanner && (
950
  <div className="w-full bg-background border-b border-border flex-shrink-0 relative z-50">
951
+ <ReviewBanner onReview={handleReviewClick} onDismiss={handleDismissReviewBanner} />
 
 
 
952
  </div>
953
  )}
954
 
955
+ <div className="flex-1 flex overflow-hidden h-[calc(100vh-4rem)] relative" style={{ overscrollBehavior: "none" }}>
 
 
 
 
956
  {!leftPanelVisible && (
957
  <Button
958
  variant="secondary"
959
  size="icon"
960
  onClick={() => setLeftPanelVisible(true)}
961
  className="hidden lg:flex absolute z-[100] h-8 w-5 shadow-lg rounded-full bg-card border border-border transition-all duration-200 ease-in-out hover:translate-x-[10px]"
962
+ style={{ left: "-5px", top: "1rem" }}
963
  title="Open panel"
964
  >
965
  <ChevronRight className="h-3 w-3" />
966
  </Button>
967
  )}
 
 
 
 
 
 
 
968
 
969
+ {leftSidebarOpen && <div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setLeftSidebarOpen(false)} />}
970
+
971
  {leftPanelVisible ? (
972
  <aside
973
  className="hidden lg:flex w-80 bg-card border-r border-border flex-col h-full min-h-0 relative"
974
+ style={{ borderRight: "1px solid var(--border)", height: "calc(100vh - 4rem)" }}
975
  >
 
976
  <Button
977
  variant="secondary"
978
  size="icon"
979
  onClick={() => setLeftPanelVisible(false)}
980
  className="absolute z-[70] h-8 w-5 shadow-lg rounded-full bg-card border border-border"
981
+ style={{ right: "-10px", top: "1rem" }}
982
  title="Close panel"
983
  >
984
  <ChevronLeft className="h-3 w-3" />
985
  </Button>
986
+
987
  <LeftSidebar
988
  learningMode={learningMode}
989
  language={language}
 
1012
  </aside>
1013
  ) : null}
1014
 
 
1015
  <aside
1016
  className={`
1017
  fixed lg:hidden inset-y-0 left-0 z-50
1018
  w-80 bg-card border-r border-border
1019
  transform transition-transform duration-300 ease-in-out
1020
+ ${leftSidebarOpen ? "translate-x-0" : "-translate-x-full"}
1021
  flex flex-col
1022
  mt-16
1023
  h-[calc(100vh-4rem)]
 
1026
  >
1027
  <div className="p-4 border-b border-border flex justify-between items-center">
1028
  <h3>Settings & Guide</h3>
1029
+ <Button variant="ghost" size="icon" onClick={() => setLeftSidebarOpen(false)}>
 
 
 
 
1030
  <X className="h-5 w-5" />
1031
  </Button>
1032
  </div>
1033
+
1034
  <LeftSidebar
1035
  learningMode={learningMode}
1036
  language={language}
 
1057
  />
1058
  </aside>
1059
 
 
1060
  <main className="flex-1 flex flex-col min-w-0 min-h-0 h-full">
1061
  <ChatArea
1062
  messages={messages}
 
1087
  savedChats={savedChats}
1088
  workspaces={workspaces}
1089
  currentWorkspaceId={currentWorkspaceId}
1090
+ onSaveFile={(content, type, _format, targetWorkspaceId) => handleSave(content, type, false, (_format ?? "text") as "pdf" | "text", targetWorkspaceId)}
1091
  leftPanelVisible={leftPanelVisible}
1092
  currentCourseId={currentCourseId}
1093
  onCourseChange={setCurrentCourseId}
 
1095
  showReviewBanner={showReviewBanner}
1096
  />
1097
  </main>
 
1098
  </div>
1099
  </div>
1100
  );