claudqunwang Cursor commited on
Commit
56c2a52
·
1 Parent(s): fe421f4

refactor: ClareCourseWare 教师专用 - 移除所有学生界面,登录后直接显示 TeacherDashboard;简化 App.tsx 为教师专用版本

Browse files
web/src/App.tsx CHANGED
@@ -1,192 +1,17 @@
1
- // web/src/App.tsx
2
- import React, { useState, useEffect, useRef, useMemo } from "react";
3
- import { Header } from "./components/Header";
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
- import { LeftSidebar } from "./components/sidebar/LeftSidebar";
14
  import { TeacherDashboard } from "./components/TeacherDashboard";
15
-
16
- // backend API bindings
17
- import { apiChat, apiUpload, apiMemoryline, apiQuizStart } from "./lib/api";
18
-
19
- // NEW: review-star logic
20
- import {
21
- type ReviewStarState,
22
- type ReviewEventType,
23
- markReviewActive,
24
- normalizeToday,
25
- starOpacity,
26
- energyPct,
27
- } from "./lib/reviewStar";
28
-
29
-
30
- export type MessageAttachmentKind = "pdf" | "ppt" | "doc" | "image" | "other";
31
-
32
- export interface MessageAttachment {
33
- name: string;
34
- kind: MessageAttachmentKind;
35
- size: number;
36
- // 这两个只是展示用,不影响后端
37
- fileType?: FileType; // syllabus / lecture-slides / ...
38
- }
39
-
40
- export interface Message {
41
- id: string;
42
- role: "user" | "assistant";
43
- content: string;
44
- timestamp: Date;
45
- references?: string[];
46
- sender?: GroupMember;
47
- showNextButton?: boolean;
48
-
49
- // ✅ NEW: show files “with” the user message (metadata only)
50
- attachments?: MessageAttachment[];
51
-
52
- questionData?: {
53
- type: "multiple-choice" | "fill-in-blank" | "open-ended";
54
- question: string;
55
- options?: string[];
56
- correctAnswer: string;
57
- explanation: string;
58
- sampleAnswer?: string;
59
- };
60
- }
61
-
62
-
63
 
64
  export interface User {
65
- // required identity
66
  name: string;
67
  email: string;
68
-
69
- // profile fields
70
- studentId?: string;
71
- department?: string;
72
- yearLevel?: string;
73
- major?: string;
74
- bio?: string; // may be generated by Clare, then user can edit in ProfileEditor
75
-
76
- // learning preferences
77
- learningStyle?: string; // "visual" | "auditory" | ...
78
- learningPace?: string; // "slow" | "moderate" | "fast"
79
-
80
- // avatar
81
- avatarUrl?: string;
82
-
83
- // control flags
84
- onboardingCompleted?: boolean;
85
- }
86
-
87
- export interface GroupMember {
88
- id: string;
89
- name: string;
90
- email: string;
91
- avatar?: string;
92
- isAI?: boolean;
93
- }
94
-
95
- export type SpaceType = "individual" | "group";
96
-
97
- export interface CourseInfo {
98
- id: string;
99
- name: string;
100
- instructor: { name: string; email: string };
101
- teachingAssistant: { name: string; email: string };
102
- teachingAssistants?: { name: string; email: string }[];
103
- }
104
-
105
- export interface Workspace {
106
- id: string;
107
- name: string;
108
- type: SpaceType;
109
- avatar: string;
110
- members?: GroupMember[];
111
- category?: "course" | "personal";
112
- courseName?: string;
113
- courseInfo?: CourseInfo;
114
- isEditable?: boolean;
115
- }
116
-
117
- export type FileType = "syllabus" | "lecture-slides" | "literature-review" | "other";
118
-
119
- export interface UploadedFile {
120
- file: File;
121
- type: FileType;
122
- }
123
-
124
- export type LearningMode = "general" | "concept" | "socratic" | "exam" | "assignment" | "summary";
125
- export type Language = "auto" | "en" | "zh";
126
- export type ChatMode = "ask" | "review" | "quiz";
127
-
128
- export interface SavedItem {
129
- id: string;
130
- title: string;
131
- content: string;
132
- type: "export" | "quiz" | "summary";
133
- timestamp: Date;
134
- isSaved: boolean;
135
- format?: "pdf" | "text";
136
- workspaceId: string;
137
- }
138
-
139
- export interface SavedChat {
140
- id: string;
141
- title: string;
142
- messages: Message[];
143
- chatMode: ChatMode;
144
- timestamp: Date;
145
- }
146
-
147
- const DOC_TYPE_MAP: Record<FileType, string> = {
148
- syllabus: "Syllabus",
149
- "lecture-slides": "Lecture Slides / PPT",
150
- "literature-review": "Literature Review / Paper",
151
- other: "Other Course Document",
152
- };
153
-
154
- function mapLanguagePref(lang: Language): string {
155
- if (lang === "zh") return "中文";
156
- if (lang === "en") return "English";
157
- return "Auto";
158
- }
159
-
160
- // ✅ localStorage helpers for saved chats
161
- function savedChatsStorageKey(email: string) {
162
- return `saved_chats::${email}`;
163
- }
164
-
165
- function hydrateSavedChats(raw: any): SavedChat[] {
166
- if (!Array.isArray(raw)) return [];
167
- return raw
168
- .map((c: any) => {
169
- try {
170
- return {
171
- ...c,
172
- timestamp: c?.timestamp ? new Date(c.timestamp) : new Date(),
173
- messages: Array.isArray(c?.messages)
174
- ? c.messages.map((m: any) => ({
175
- ...m,
176
- timestamp: m?.timestamp ? new Date(m.timestamp) : new Date(),
177
- }))
178
- : [],
179
- } as SavedChat;
180
- } catch {
181
- return null;
182
- }
183
- })
184
- .filter(Boolean) as SavedChat[];
185
  }
186
 
187
  // ✅ localStorage helpers for user profile
188
  function profileStorageKey(email: string) {
189
- return `user_profile::${email}`;
190
  }
191
 
192
  function hydrateUserFromStorage(base: User): User {
@@ -201,29 +26,8 @@ function hydrateUserFromStorage(base: User): User {
201
  }
202
 
203
  function App() {
204
- const [isDarkMode, setIsDarkMode] = useState(() => {
205
- const saved = localStorage.getItem("theme");
206
- return saved === "dark" || (!saved && window.matchMedia("(prefers-color-scheme: dark)").matches);
207
- });
208
-
209
  const [user, setUser] = useState<User | null>(null);
210
 
211
- // ✅ unified user update helpers
212
- const updateUser = (patch: Partial<User>) => {
213
- setUser((prev) => (prev ? { ...prev, ...patch } : prev));
214
- };
215
-
216
- const handleUserSave = (next: User) => {
217
- setUser((prev) => {
218
- if (!prev) return next;
219
- return {
220
- ...prev,
221
- ...next,
222
- onboardingCompleted: next.onboardingCompleted ?? prev.onboardingCompleted,
223
- };
224
- });
225
- };
226
-
227
  // ✅ persist user profile whenever it changes (per-email)
228
  useEffect(() => {
229
  if (!user?.email) return;
@@ -234,1322 +38,19 @@ function App() {
234
  }
235
  }, [user]);
236
 
237
- // -------------------------
238
- // ✅ Course selection (stable)
239
- // -------------------------
240
- const MYSPACE_COURSE_KEY = "myspace_selected_course";
241
-
242
- const [currentCourseId, setCurrentCourseId] = useState<string>(() => {
243
- return localStorage.getItem(MYSPACE_COURSE_KEY) || "course1";
244
- });
245
-
246
- const availableCourses: CourseInfo[] = [
247
- {
248
- id: "ist345",
249
- name: "IST345",
250
- instructor: { name: "Yan Li", email: "Yan.Li@cgu.edu" },
251
- teachingAssistant: { name: "Kaijie Yu", email: "Kaijie.Yu@cgu.edu" },
252
- teachingAssistants: [
253
- { name: "Kaijie Yu", email: "Kaijie.Yu@cgu.edu" },
254
- { name: "Yongjia Sun", email: "Yongjia.Sun@cgu.edu" },
255
- ],
256
- },
257
- {
258
- id: "course1",
259
- name: "Introduction to AI",
260
- instructor: { name: "Dr. Sarah Johnson", email: "sarah.johnson@university.edu" },
261
- teachingAssistant: { name: "Michael Chen", email: "michael.chen@university.edu" },
262
- },
263
- {
264
- id: "course2",
265
- name: "Machine Learning",
266
- instructor: { name: "Prof. David Lee", email: "david.lee@university.edu" },
267
- teachingAssistant: { name: "Emily Zhang", email: "emily.zhang@university.edu" },
268
- },
269
- {
270
- id: "course3",
271
- name: "Data Structures",
272
- instructor: { name: "Dr. Robert Smith", email: "robert.smith@university.edu" },
273
- teachingAssistant: { name: "Lisa Wang", email: "lisa.wang@university.edu" },
274
- },
275
- {
276
- id: "course4",
277
- name: "Web Development",
278
- instructor: { name: "Prof. Maria Garcia", email: "maria.garcia@university.edu" },
279
- teachingAssistant: { name: "James Brown", email: "james.brown@university.edu" },
280
- },
281
- ];
282
-
283
- const [askMessages, setAskMessages] = useState<Message[]>([
284
- {
285
- id: "1",
286
- role: "assistant",
287
- content:
288
- "👋 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!",
289
- timestamp: new Date(),
290
- },
291
- ]);
292
-
293
- const [reviewMessages, setReviewMessages] = useState<Message[]>([
294
- {
295
- id: "review-1",
296
- role: "assistant",
297
- content:
298
- "📚 Welcome to Review mode! I'll help you review and consolidate your learning. Let's go through what you've learned!",
299
- timestamp: new Date(),
300
- },
301
- ]);
302
-
303
- const [quizMessages, setQuizMessages] = useState<Message[]>([
304
- {
305
- id: "quiz-1",
306
- role: "assistant",
307
- content:
308
- "🎯 Welcome to Quiz mode! I'll test your understanding with personalized questions based on your learning history. Ready to start?",
309
- timestamp: new Date(),
310
- },
311
- ]);
312
-
313
- const [learningMode, setLearningMode] = useState<LearningMode>("concept");
314
- const [language, setLanguage] = useState<Language>("auto");
315
- const [chatMode, setChatMode] = useState<ChatMode>("ask");
316
-
317
- const messages = chatMode === "ask" ? askMessages : chatMode === "review" ? reviewMessages : quizMessages;
318
-
319
- const prevChatModeRef = useRef<ChatMode>(chatMode);
320
-
321
- useEffect(() => {
322
- let currentMessages: Message[];
323
- let setCurrentMessages: (messages: Message[]) => void;
324
-
325
- if (chatMode === "ask") {
326
- currentMessages = askMessages;
327
- setCurrentMessages = setAskMessages;
328
- } else if (chatMode === "review") {
329
- currentMessages = reviewMessages;
330
- setCurrentMessages = setReviewMessages;
331
- } else {
332
- currentMessages = quizMessages;
333
- setCurrentMessages = setQuizMessages;
334
- }
335
-
336
- const hasUserMessages = currentMessages.some((msg) => msg.role === "user");
337
- const expectedWelcomeId = chatMode === "ask" ? "1" : chatMode === "review" ? "review-1" : "quiz-1";
338
- const hasWelcomeMessage = currentMessages.some((msg) => msg.id === expectedWelcomeId && msg.role === "assistant");
339
- const modeChanged = prevChatModeRef.current !== chatMode;
340
-
341
- if ((modeChanged || currentMessages.length === 0 || !hasWelcomeMessage) && !hasUserMessages) {
342
- const initialMessages: Record<ChatMode, Message[]> = {
343
- ask: [
344
- {
345
- id: "1",
346
- role: "assistant",
347
- content:
348
- "👋 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!",
349
- timestamp: new Date(),
350
- },
351
- ],
352
- review: [
353
- {
354
- id: "review-1",
355
- role: "assistant",
356
- content:
357
- "📚 Welcome to Review mode! I'll help you review and consolidate your learning. Let's go through what you've learned!",
358
- timestamp: new Date(),
359
- },
360
- ],
361
- quiz: [
362
- {
363
- id: "quiz-1",
364
- role: "assistant",
365
- content:
366
- "🎯 Welcome to Quiz mode! I'll test your understanding with personalized questions based on your learning history. Ready to start?",
367
- timestamp: new Date(),
368
- },
369
- ],
370
- };
371
-
372
- setCurrentMessages(initialMessages[chatMode]);
373
- }
374
-
375
- prevChatModeRef.current = chatMode;
376
- }, [chatMode, askMessages.length, reviewMessages.length, quizMessages.length]);
377
-
378
- const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
379
- const [memoryProgress, setMemoryProgress] = useState(36);
380
-
381
- const [quizState, setQuizState] = useState<{
382
- currentQuestion: number;
383
- waitingForAnswer: boolean;
384
- showNextButton: boolean;
385
- }>({
386
- currentQuestion: 0,
387
- waitingForAnswer: false,
388
- showNextButton: false,
389
- });
390
-
391
- const [isTyping, setIsTyping] = useState(false);
392
- const [leftSidebarOpen, setLeftSidebarOpen] = useState(false);
393
- const [showTeacherDashboard, setShowTeacherDashboard] = useState(false);
394
- const [leftPanelVisible, setLeftPanelVisible] = useState(true);
395
- const [showProfileEditor, setShowProfileEditor] = useState(false);
396
- const [showOnboarding, setShowOnboarding] = useState(false);
397
-
398
- const [showReviewBanner, setShowReviewBanner] = useState(() => true);
399
- const [showClearDialog, setShowClearDialog] = useState(false);
400
-
401
- const [savedItems, setSavedItems] = useState<SavedItem[]>([]);
402
- const [recentlySavedId, setRecentlySavedId] = useState<string | null>(null);
403
-
404
- const [savedChats, setSavedChats] = useState<SavedChat[]>([]);
405
-
406
- // ✅ load saved chats after login
407
- useEffect(() => {
408
- if (!user?.email) return;
409
- try {
410
- const raw = localStorage.getItem(savedChatsStorageKey(user.email));
411
- if (!raw) {
412
- setSavedChats([]);
413
- return;
414
- }
415
- const parsed = JSON.parse(raw);
416
- setSavedChats(hydrateSavedChats(parsed));
417
- } catch {
418
- setSavedChats([]);
419
- }
420
- }, [user?.email]);
421
-
422
- // ✅ persist saved chats whenever changed
423
- useEffect(() => {
424
- if (!user?.email) return;
425
- try {
426
- localStorage.setItem(savedChatsStorageKey(user.email), JSON.stringify(savedChats));
427
- } catch {
428
- // ignore
429
- }
430
- }, [savedChats, user?.email]);
431
-
432
- const [groupMembers] = useState<GroupMember[]>([
433
- { id: "clare", name: "Clare AI", email: "clare@ai.assistant", isAI: true },
434
- { id: "1", name: "Sarah Johnson", email: "sarah.j@university.edu" },
435
- { id: "2", name: "Michael Chen", email: "michael.c@university.edu" },
436
- { id: "3", name: "Emma Williams", email: "emma.w@university.edu" },
437
- ]);
438
-
439
- const [workspaces, setWorkspaces] = useState<Workspace[]>([]);
440
- const [currentWorkspaceId, setCurrentWorkspaceId] = useState<string>("individual");
441
-
442
- // ✅ used to prevent duplicate upload per file fingerprint
443
- const uploadedFingerprintsRef = useRef<Set<string>>(new Set());
444
-
445
- useEffect(() => {
446
- if (user) {
447
- const userAvatar = `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}`;
448
- const course1Info = availableCourses.find((c) => c.id === "course1");
449
- const course2Info = availableCourses.find((c) => c.name === "AI Ethics"); // may be undefined, that's OK
450
-
451
- setWorkspaces([
452
- { id: "individual", name: "My Space", type: "individual", avatar: userAvatar },
453
- {
454
- id: "group-1",
455
- name: "CS 101 Study Group",
456
- type: "group",
457
- avatar: "https://api.dicebear.com/7.x/shapes/svg?seed=cs101group",
458
- members: groupMembers,
459
- category: "course",
460
- courseName: course1Info?.name || "CS 101",
461
- courseInfo: course1Info,
462
- },
463
- {
464
- id: "group-2",
465
- name: "AI Ethics Team",
466
- type: "group",
467
- avatar: "https://api.dicebear.com/7.x/shapes/svg?seed=aiethicsteam",
468
- members: groupMembers,
469
- category: "course",
470
- courseName: course2Info?.name || "AI Ethics",
471
- courseInfo: course2Info,
472
- },
473
- ]);
474
- }
475
- }, [user, groupMembers, availableCourses]);
476
-
477
- const fallbackWorkspace: Workspace = {
478
- id: "individual",
479
- name: "My Space",
480
- type: "individual",
481
- avatar: user ? `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}` : "",
482
- };
483
-
484
- const currentWorkspace: Workspace =
485
- workspaces.find((w) => w.id === currentWorkspaceId) || workspaces[0] || fallbackWorkspace;
486
-
487
- const spaceType: SpaceType = currentWorkspace?.type || "individual";
488
-
489
- // =========================
490
- // ✅ Scheme 1: "My Space" uses Group-like sidebar view model
491
- // =========================
492
- const mySpaceCourseInfo = useMemo(() => {
493
- return availableCourses.find((c) => c.id === currentCourseId);
494
- }, [availableCourses, currentCourseId]);
495
-
496
- const mySpaceUserMember: GroupMember | null = useMemo(() => {
497
- if (!user) return null;
498
- return {
499
- id: user.email,
500
- name: user.name,
501
- email: user.email,
502
- avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}`,
503
- };
504
- }, [user]);
505
-
506
- const clareMember: GroupMember = useMemo(
507
- () => ({ id: "clare", name: "Clare AI", email: "clare@ai.assistant", isAI: true }),
508
- []
509
- );
510
-
511
- const sidebarWorkspaces: Workspace[] = useMemo(() => {
512
- if (!workspaces?.length) return workspaces;
513
- if (!mySpaceUserMember) return workspaces;
514
-
515
- return workspaces.map((w) => {
516
- if (w.id !== "individual") return w;
517
-
518
- return {
519
- ...w,
520
- category: "course",
521
- courseName: mySpaceCourseInfo?.name || w.courseName || "My Course",
522
- courseInfo: mySpaceCourseInfo,
523
- members: [clareMember, mySpaceUserMember],
524
- };
525
- });
526
- }, [workspaces, mySpaceCourseInfo, mySpaceUserMember, clareMember]);
527
-
528
- const sidebarSpaceType: SpaceType = useMemo(() => {
529
- return currentWorkspaceId === "individual" ? "group" : spaceType;
530
- }, [currentWorkspaceId, spaceType]);
531
-
532
- const sidebarGroupMembers: GroupMember[] = useMemo(() => {
533
- if (currentWorkspaceId === "individual" && mySpaceUserMember) {
534
- return [clareMember, mySpaceUserMember];
535
- }
536
- return groupMembers;
537
- }, [currentWorkspaceId, mySpaceUserMember, clareMember, groupMembers]);
538
-
539
- // =========================
540
- // ✅ Stable course switching logic
541
- // =========================
542
- const didHydrateMySpaceRef = useRef(false);
543
-
544
- const handleCourseChange = (nextCourseId: string) => {
545
- if (!nextCourseId) return;
546
-
547
- if (currentWorkspace.type === "group" && currentWorkspace.category === "course") {
548
- return;
549
- }
550
-
551
- setCurrentCourseId(nextCourseId);
552
- try {
553
- localStorage.setItem(MYSPACE_COURSE_KEY, nextCourseId);
554
- } catch {
555
- // ignore
556
- }
557
- };
558
-
559
- useEffect(() => {
560
- if (!currentWorkspace) return;
561
-
562
- if (currentWorkspace.type === "group" && currentWorkspace.category === "course") {
563
- const cid = currentWorkspace.courseInfo?.id;
564
- if (cid && cid !== currentCourseId) setCurrentCourseId(cid);
565
- didHydrateMySpaceRef.current = false;
566
- return;
567
- }
568
-
569
- if (currentWorkspace.type === "individual") {
570
- if (!didHydrateMySpaceRef.current) {
571
- didHydrateMySpaceRef.current = true;
572
-
573
- const saved = localStorage.getItem(MYSPACE_COURSE_KEY);
574
- const valid = saved && availableCourses.some((c) => c.id === saved) ? saved : undefined;
575
-
576
- const next = valid || currentCourseId || "course1";
577
- if (next !== currentCourseId) setCurrentCourseId(next);
578
- }
579
- }
580
- }, [
581
- currentWorkspaceId,
582
- currentWorkspace?.type,
583
- currentWorkspace?.category,
584
- currentWorkspace?.courseInfo?.id,
585
- availableCourses,
586
- currentCourseId,
587
- currentWorkspace,
588
- ]);
589
-
590
- useEffect(() => {
591
- if (currentWorkspace?.type !== "individual") return;
592
- try {
593
- const prev = localStorage.getItem(MYSPACE_COURSE_KEY);
594
- if (prev !== currentCourseId) localStorage.setItem(MYSPACE_COURSE_KEY, currentCourseId);
595
- } catch {
596
- // ignore
597
- }
598
- }, [currentCourseId, currentWorkspace?.type]);
599
-
600
- useEffect(() => {
601
- document.documentElement.classList.toggle("dark", isDarkMode);
602
- localStorage.setItem("theme", isDarkMode ? "dark" : "light");
603
- }, [isDarkMode]);
604
-
605
- useEffect(() => {
606
- const prev = document.body.style.overflow;
607
- document.body.style.overflow = "hidden";
608
- return () => {
609
- document.body.style.overflow = prev;
610
- };
611
- }, []);
612
-
613
- useEffect(() => {
614
- if (!user) return;
615
-
616
- (async () => {
617
- try {
618
- const r = await apiMemoryline(user.email);
619
- const pct = Math.round((r.progress_pct ?? 0) * 100);
620
- setMemoryProgress(pct);
621
- } catch {
622
- // silent
623
- }
624
- })();
625
- }, [user]);
626
-
627
- // =========================
628
- // ✅ Review Star (按天) state
629
- // =========================
630
- const reviewStarKey = useMemo(() => {
631
- if (!user) return "";
632
- return `review_star::${user.email}::${currentWorkspaceId}`;
633
- }, [user, currentWorkspaceId]);
634
-
635
- const [reviewStarState, setReviewStarState] = useState<ReviewStarState | null>(null);
636
-
637
- useEffect(() => {
638
- if (!user || !reviewStarKey) return;
639
- if (chatMode !== "review") return;
640
-
641
- const next = normalizeToday(reviewStarKey);
642
- setReviewStarState(next);
643
- }, [chatMode, reviewStarKey, user]);
644
-
645
- const handleReviewActivity = (event: ReviewEventType) => {
646
- if (!user || !reviewStarKey) return;
647
- const next = markReviewActive(reviewStarKey, event);
648
- setReviewStarState(next);
649
- };
650
-
651
- const reviewStarOpacity = starOpacity(reviewStarState);
652
- const reviewEnergyPct = energyPct(reviewStarState);
653
-
654
- const getCurrentDocTypeForChat = (): string => {
655
- if (uploadedFiles.length > 0) {
656
- const last = uploadedFiles[uploadedFiles.length - 1];
657
- return DOC_TYPE_MAP[last.type] || "Syllabus";
658
- }
659
- return "Syllabus";
660
- };
661
-
662
- const handleSendMessage = async (content: string) => {
663
- if (!user) return;
664
-
665
- const hasText = !!content.trim();
666
- const hasFiles = uploadedFiles.length > 0;
667
-
668
- if (!hasText && !hasFiles) return;
669
-
670
- const fileNames = hasFiles ? uploadedFiles.map((f) => f.file.name) : [];
671
- const fileLine = fileNames.length ? `Uploaded files: ${fileNames.join(", ")}` : "";
672
-
673
- const effectiveContent = hasText
674
- ? content
675
- : `I've uploaded file(s). Please read them and help me based on their content.\n${fileLine}`.trim();
676
-
677
- const sender: GroupMember = {
678
- id: user.email,
679
- name: user.name,
680
- email: user.email,
681
- avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}`,
682
- };
683
-
684
- const userVisibleContent = hasText
685
- ? content
686
- : `📎 Sent ${fileNames.length} file(s)\n${fileNames.map((n) => `- ${n}`).join("\n")}`;
687
-
688
- // ✅ snapshot attachments at send-time
689
- const attachmentsSnapshot: MessageAttachment[] =
690
- uploadedFiles.map((uf) => {
691
- const lower = uf.file.name.toLowerCase();
692
- const kind: MessageAttachmentKind =
693
- lower.endsWith(".pdf")
694
- ? "pdf"
695
- : lower.endsWith(".ppt") || lower.endsWith(".pptx")
696
- ? "ppt"
697
- : lower.endsWith(".doc") || lower.endsWith(".docx")
698
- ? "doc"
699
- : [".jpg", ".jpeg", ".png", ".gif", ".webp"].some((e) => lower.endsWith(e))
700
- ? "image"
701
- : "other";
702
-
703
- return {
704
- name: uf.file.name,
705
- size: uf.file.size,
706
- kind,
707
- fileType: uf.type,
708
- };
709
- });
710
-
711
- const userMessage: Message = {
712
- id: Date.now().toString(),
713
- role: "user",
714
- content: userVisibleContent,
715
- timestamp: new Date(),
716
- sender,
717
- attachments: attachmentsSnapshot.length ? attachmentsSnapshot : undefined,
718
- };
719
-
720
- if (chatMode === "ask") setAskMessages((prev) => [...prev, userMessage]);
721
- else if (chatMode === "review") setReviewMessages((prev) => [...prev, userMessage]);
722
- else setQuizMessages((prev) => [...prev, userMessage]);
723
-
724
- if (chatMode === "quiz") {
725
- setIsTyping(true);
726
-
727
- try {
728
- const docType = getCurrentDocTypeForChat();
729
-
730
- const r = await apiChat({
731
- user_id: user.email,
732
- message: effectiveContent,
733
- learning_mode: "quiz",
734
- language_preference: mapLanguagePref(language),
735
- doc_type: docType,
736
- });
737
-
738
- const normalizeRefs = (raw: any): string[] => {
739
- const arr = Array.isArray(raw) ? raw : [];
740
- return arr
741
- .map((x) => {
742
- if (typeof x === "string") {
743
- const s = x.trim();
744
- return s ? s : null;
745
- }
746
- const a = x?.source_file ? String(x.source_file) : "";
747
- const b = x?.section ? String(x.section) : "";
748
- const s = `${a}${a && b ? " — " : ""}${b}`.trim();
749
- return s || null;
750
- })
751
- .filter(Boolean) as string[];
752
- };
753
-
754
- const refs = normalizeRefs((r as any).refs ?? (r as any).references);
755
-
756
- const assistantMessage: Message = {
757
- id: (Date.now() + 1).toString(),
758
- role: "assistant",
759
- content: r.reply || "",
760
- timestamp: new Date(),
761
- references: refs.length ? refs : undefined,
762
- sender: spaceType === "group" ? groupMembers.find((m) => m.isAI) : undefined,
763
- showNextButton: false,
764
- };
765
-
766
- setIsTyping(false);
767
-
768
- setTimeout(() => {
769
- setQuizMessages((prev) => [...prev, assistantMessage]);
770
- setQuizState((prev) => ({ ...prev, waitingForAnswer: true, showNextButton: false }));
771
- }, 50);
772
- } catch (e: any) {
773
- setIsTyping(false);
774
- toast.error(e?.message || "Quiz failed");
775
-
776
- const assistantMessage: Message = {
777
- id: (Date.now() + 1).toString(),
778
- role: "assistant",
779
- content: "Sorry — quiz request failed. Please try again.",
780
- timestamp: new Date(),
781
- sender: spaceType === "group" ? groupMembers.find((m) => m.isAI) : undefined,
782
- };
783
-
784
- setTimeout(() => {
785
- setQuizMessages((prev) => [...prev, assistantMessage]);
786
- }, 50);
787
- }
788
-
789
- return;
790
- }
791
-
792
- setIsTyping(true);
793
- try {
794
- const docType = getCurrentDocTypeForChat();
795
-
796
- const r = await apiChat({
797
- user_id: user.email,
798
- message: effectiveContent,
799
- learning_mode: learningMode,
800
- language_preference: mapLanguagePref(language),
801
- doc_type: docType,
802
- });
803
-
804
- const refs = (r.refs || [])
805
- .map((x: any) => {
806
- const a = x?.source_file ? String(x.source_file) : "";
807
- const b = x?.section ? String(x.section) : "";
808
- const s = `${a}${a && b ? " — " : ""}${b}`.trim();
809
- return s || null;
810
- })
811
- .filter(Boolean) as string[];
812
-
813
- const assistantMessage: Message = {
814
- id: (Date.now() + 1).toString(),
815
- role: "assistant",
816
- content: r.reply || "",
817
- timestamp: new Date(),
818
- references: refs.length ? refs : undefined,
819
- sender: spaceType === "group" ? groupMembers.find((m) => m.isAI) : undefined,
820
- };
821
-
822
- setIsTyping(false);
823
-
824
- setTimeout(() => {
825
- if (chatMode === "ask") setAskMessages((prev) => [...prev, assistantMessage]);
826
- else if (chatMode === "review") setReviewMessages((prev) => [...prev, assistantMessage]);
827
- }, 50);
828
-
829
- try {
830
- const ml = await apiMemoryline(user.email);
831
- setMemoryProgress(Math.round((ml.progress_pct ?? 0) * 100));
832
- } catch {
833
- // ignore
834
- }
835
- } catch (e: any) {
836
- setIsTyping(false);
837
- toast.error(e?.message || "Chat failed");
838
-
839
- const assistantMessage: Message = {
840
- id: (Date.now() + 1).toString(),
841
- role: "assistant",
842
- content: "Sorry — chat request failed. Please try again.",
843
- timestamp: new Date(),
844
- sender: spaceType === "group" ? groupMembers.find((m) => m.isAI) : undefined,
845
- };
846
-
847
- setTimeout(() => {
848
- if (chatMode === "ask") setAskMessages((prev) => [...prev, assistantMessage]);
849
- if (chatMode === "review") setReviewMessages((prev) => [...prev, assistantMessage]);
850
- }, 50);
851
- }
852
- };
853
-
854
- const handleNextQuestion = async () => {
855
- if (!user) return;
856
-
857
- const prompt = "Please give me another question of the same quiz style.";
858
- const sender: GroupMember = {
859
- id: user.email,
860
- name: user.name,
861
- email: user.email,
862
- avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}`,
863
- };
864
-
865
- const userMessage: Message = {
866
- id: Date.now().toString(),
867
- role: "user",
868
- content: prompt,
869
- timestamp: new Date(),
870
- sender,
871
- };
872
-
873
- setQuizMessages((prev) => [...prev, userMessage]);
874
- setIsTyping(true);
875
-
876
- try {
877
- const docType = getCurrentDocTypeForChat();
878
- const r = await apiChat({
879
- user_id: user.email,
880
- message: prompt,
881
- learning_mode: "quiz",
882
- language_preference: mapLanguagePref(language),
883
- doc_type: docType,
884
- });
885
-
886
- const refs = (r.refs || [])
887
- .map((x: any) => {
888
- const a = x?.source_file ? String(x.source_file) : "";
889
- const b = x?.section ? String(x.section) : "";
890
- const s = `${a}${a && b ? " — " : ""}${b}`.trim();
891
- return s || null;
892
- })
893
- .filter(Boolean) as string[];
894
-
895
- const assistantMessage: Message = {
896
- id: (Date.now() + 1).toString(),
897
- role: "assistant",
898
- content: r.reply || "",
899
- timestamp: new Date(),
900
- references: refs.length ? refs : undefined,
901
- sender: spaceType === "group" ? groupMembers.find((m) => m.isAI) : undefined,
902
- showNextButton: false,
903
- };
904
-
905
- setIsTyping(false);
906
-
907
- setTimeout(() => {
908
- setQuizMessages((prev) => [...prev, assistantMessage]);
909
- setQuizState((prev) => ({
910
- ...prev,
911
- currentQuestion: prev.currentQuestion + 1,
912
- waitingForAnswer: true,
913
- showNextButton: false,
914
- }));
915
- }, 50);
916
- } catch (e: any) {
917
- setIsTyping(false);
918
- toast.error(e?.message || "Quiz failed");
919
-
920
- const assistantMessage: Message = {
921
- id: (Date.now() + 1).toString(),
922
- role: "assistant",
923
- content: "Sorry — quiz request failed. Please try again.",
924
- timestamp: new Date(),
925
- sender: spaceType === "group" ? groupMembers.find((m) => m.isAI) : undefined,
926
- };
927
-
928
- setTimeout(() => setQuizMessages((prev) => [...prev, assistantMessage]), 50);
929
- }
930
- };
931
-
932
- const handleStartQuiz = async () => {
933
- if (!user) return;
934
-
935
- setIsTyping(true);
936
- try {
937
- const docType = getCurrentDocTypeForChat();
938
-
939
- const r = await apiQuizStart({
940
- user_id: user.email,
941
- language_preference: mapLanguagePref(language),
942
- doc_type: docType,
943
- learning_mode: "quiz",
944
- });
945
-
946
- const refs = (r.refs || [])
947
- .map((x: any) => {
948
- const a = x?.source_file ? String(x.source_file) : "";
949
- const b = x?.section ? String(x.section) : "";
950
- const s = `${a}${a && b ? " — " : ""}${b}`.trim();
951
- return s || null;
952
- })
953
- .filter(Boolean) as string[];
954
-
955
- const assistantMessage: Message = {
956
- id: Date.now().toString(),
957
- role: "assistant",
958
- content: r.reply || "",
959
- timestamp: new Date(),
960
- references: refs.length ? refs : undefined,
961
- sender: spaceType === "group" ? groupMembers.find((m) => m.isAI) : undefined,
962
- showNextButton: false,
963
- };
964
-
965
- setIsTyping(false);
966
-
967
- setTimeout(() => {
968
- setQuizMessages((prev) => [...prev, assistantMessage]);
969
- setQuizState({ currentQuestion: 0, waitingForAnswer: true, showNextButton: false });
970
- }, 50);
971
- } catch (e: any) {
972
- setIsTyping(false);
973
- toast.error(e?.message || "Start quiz failed");
974
-
975
- const assistantMessage: Message = {
976
- id: Date.now().toString(),
977
- role: "assistant",
978
- content: "Sorry — could not start the quiz. Please try again.",
979
- timestamp: new Date(),
980
- sender: spaceType === "group" ? groupMembers.find((m) => m.isAI) : undefined,
981
- };
982
-
983
- setTimeout(() => setQuizMessages((prev) => [...prev, assistantMessage]), 50);
984
- }
985
- };
986
-
987
- // =========================
988
- // File Upload (FIXED)
989
- // =========================
990
-
991
- const handleFileUpload = async (input: File[] | FileList | null | undefined) => {
992
- const files = Array.isArray(input) ? input : input ? Array.from(input) : [];
993
- if (!files.length) return;
994
-
995
- const newFiles: UploadedFile[] = files.map((file) => ({ file, type: "other" as FileType }));
996
-
997
- setUploadedFiles((prev) => [...prev, ...newFiles]);
998
-
999
- if (!user) return;
1000
-
1001
- for (const f of files) {
1002
- const fp = `${f.name}::${f.size}::${f.lastModified}`;
1003
- if (uploadedFingerprintsRef.current.has(fp)) continue;
1004
- uploadedFingerprintsRef.current.add(fp);
1005
-
1006
- try {
1007
- await apiUpload({
1008
- user_id: user.email,
1009
- doc_type: DOC_TYPE_MAP["other"] || "Other Course Document",
1010
- file: f,
1011
- });
1012
- toast.success(`File uploaded: ${f.name}`);
1013
- } catch (e: any) {
1014
- toast.error(e?.message || `Upload failed: ${f.name}`);
1015
- uploadedFingerprintsRef.current.delete(fp);
1016
- }
1017
- }
1018
- };
1019
-
1020
- const handleRemoveFile = (arg: any) => {
1021
- setUploadedFiles((prev) => {
1022
- if (!prev.length) return prev;
1023
-
1024
- let idx = -1;
1025
-
1026
- if (typeof arg === "number") {
1027
- idx = arg;
1028
- } else {
1029
- const file =
1030
- arg?.file instanceof File
1031
- ? (arg as UploadedFile).file
1032
- : arg instanceof File
1033
- ? (arg as File)
1034
- : null;
1035
-
1036
- if (file) {
1037
- idx = prev.findIndex(
1038
- (x) =>
1039
- x.file.name === file.name && x.file.size === file.size && x.file.lastModified === file.lastModified
1040
- );
1041
- }
1042
- }
1043
-
1044
- if (idx < 0 || idx >= prev.length) return prev;
1045
-
1046
- const removed = prev[idx]?.file;
1047
- const next = prev.filter((_, i) => i !== idx);
1048
-
1049
- if (removed) {
1050
- const fp = `${removed.name}::${removed.size}::${removed.lastModified}`;
1051
- uploadedFingerprintsRef.current.delete(fp);
1052
- }
1053
-
1054
- return next;
1055
- });
1056
- };
1057
-
1058
- const handleFileTypeChange = async (index: number, type: FileType) => {
1059
- if (!user) return;
1060
-
1061
- const target = uploadedFiles[index]?.file;
1062
- if (!target) return;
1063
-
1064
- setUploadedFiles((prev) => prev.map((f, i) => (i === index ? { ...f, type } : f)));
1065
-
1066
- const fp = `${target.name}::${target.size}::${target.lastModified}`;
1067
- if (uploadedFingerprintsRef.current.has(fp)) return;
1068
- uploadedFingerprintsRef.current.add(fp);
1069
-
1070
- try {
1071
- await apiUpload({
1072
- user_id: user.email,
1073
- doc_type: DOC_TYPE_MAP[type] || "Other Course Document",
1074
- file: target,
1075
- });
1076
- toast.success("File uploaded to backend");
1077
- } catch (e: any) {
1078
- toast.error(e?.message || "Upload failed");
1079
- uploadedFingerprintsRef.current.delete(fp);
1080
- }
1081
- };
1082
-
1083
- const isCurrentChatSaved = (): SavedChat | null => {
1084
- if (messages.length <= 1) return null;
1085
-
1086
- return (
1087
- savedChats.find((chat) => {
1088
- if (chat.chatMode !== chatMode) return false;
1089
- if (chat.messages.length !== messages.length) return false;
1090
-
1091
- return chat.messages.every((savedMsg, idx) => {
1092
- const currentMsg = messages[idx];
1093
- return savedMsg.id === currentMsg.id && savedMsg.role === currentMsg.role && savedMsg.content === currentMsg.content;
1094
- });
1095
- }) || null
1096
- );
1097
- };
1098
-
1099
- const handleDeleteSavedChat = (id: string) => {
1100
- setSavedChats((prev) => prev.filter((chat) => chat.id !== id));
1101
- toast.success("Chat deleted");
1102
- };
1103
-
1104
- const handleRenameSavedChat = (id: string, newTitle: string) => {
1105
- setSavedChats((prev) => prev.map((chat) => (chat.id === id ? { ...chat, title: newTitle } : chat)));
1106
- toast.success("Chat renamed");
1107
- };
1108
-
1109
- const handleSaveChat = () => {
1110
- if (messages.length <= 1) {
1111
- toast.info("No conversation to save");
1112
- return;
1113
- }
1114
-
1115
- const existingChat = isCurrentChatSaved();
1116
- if (existingChat) {
1117
- handleDeleteSavedChat(existingChat.id);
1118
- toast.success("Chat unsaved");
1119
- return;
1120
- }
1121
-
1122
- const title = `Chat - ${chatMode === "ask" ? "Ask" : chatMode === "review" ? "Review" : "Quiz"} - ${new Date().toLocaleDateString()}`;
1123
-
1124
- const newChat: SavedChat = {
1125
- id: Date.now().toString(),
1126
- title,
1127
- messages: [...messages],
1128
- chatMode,
1129
- timestamp: new Date(),
1130
- };
1131
-
1132
- setSavedChats((prev) => [newChat, ...prev]);
1133
- setLeftPanelVisible(true);
1134
- toast.success("Chat saved!");
1135
- };
1136
-
1137
- const handleLoadChat = (savedChat: SavedChat) => {
1138
- setChatMode(savedChat.chatMode);
1139
-
1140
- if (savedChat.chatMode === "ask") setAskMessages(savedChat.messages);
1141
- else if (savedChat.chatMode === "review") setReviewMessages(savedChat.messages);
1142
- else {
1143
- setQuizMessages(savedChat.messages);
1144
- setQuizState({ currentQuestion: 0, waitingForAnswer: false, showNextButton: false });
1145
- }
1146
-
1147
- toast.success("Chat loaded!");
1148
- };
1149
-
1150
- const handleClearConversation = (shouldSave: boolean = false) => {
1151
- if (shouldSave) handleSaveChat();
1152
-
1153
- const initialMessages: Record<ChatMode, Message[]> = {
1154
- ask: [
1155
- {
1156
- id: "1",
1157
- role: "assistant",
1158
- content:
1159
- "👋 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!",
1160
- timestamp: new Date(),
1161
- },
1162
- ],
1163
- review: [
1164
- {
1165
- id: "review-1",
1166
- role: "assistant",
1167
- content:
1168
- "📚 Welcome to Review mode! I'll help you review and consolidate your learning. Let's go through what you've learned!",
1169
- timestamp: new Date(),
1170
- },
1171
- ],
1172
- quiz: [
1173
- {
1174
- id: "quiz-1",
1175
- role: "assistant",
1176
- content:
1177
- "🎯 Welcome to Quiz mode! I'll test your understanding with personalized questions based on your learning history. Ready to start?",
1178
- timestamp: new Date(),
1179
- },
1180
- ],
1181
- };
1182
-
1183
- if (chatMode === "ask") setAskMessages(initialMessages.ask);
1184
- else if (chatMode === "review") setReviewMessages(initialMessages.review);
1185
- else {
1186
- setQuizMessages(initialMessages.quiz);
1187
- setQuizState({ currentQuestion: 0, waitingForAnswer: false, showNextButton: false });
1188
- }
1189
- };
1190
-
1191
- const handleSave = (
1192
- content: string,
1193
- type: "export" | "quiz" | "summary",
1194
- saveAsChat: boolean = false,
1195
- format: "pdf" | "text" = "text",
1196
- workspaceId?: string
1197
- ) => {
1198
- if (!content.trim()) return;
1199
-
1200
- if (saveAsChat && type !== "summary") {
1201
- const chatMessages: Message[] = [
1202
- {
1203
- id: "1",
1204
- role: "assistant",
1205
- content:
1206
- "👋 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!",
1207
- timestamp: new Date(),
1208
- },
1209
- { id: Date.now().toString(), role: "assistant", content, timestamp: new Date() },
1210
- ];
1211
-
1212
- const title = type === "export" ? "Exported Conversation" : "Micro-Quiz";
1213
- const newChat: SavedChat = {
1214
- id: Date.now().toString(),
1215
- title: `${title} - ${new Date().toLocaleDateString()}`,
1216
- messages: chatMessages,
1217
- chatMode: "ask",
1218
- timestamp: new Date(),
1219
- };
1220
-
1221
- setSavedChats((prev) => [newChat, ...prev]);
1222
- setLeftPanelVisible(true);
1223
- toast.success("Chat saved!");
1224
- return;
1225
- }
1226
-
1227
- const existingItem = savedItems.find((item) => item.content === content && item.type === type);
1228
- if (existingItem) {
1229
- handleUnsave(existingItem.id);
1230
- return;
1231
- }
1232
-
1233
- const title = type === "export" ? "Exported Conversation" : type === "quiz" ? "Micro-Quiz" : "Summarization";
1234
- const newItem: SavedItem = {
1235
- id: Date.now().toString(),
1236
- title: `${title} - ${new Date().toLocaleDateString()}`,
1237
- content,
1238
- type,
1239
- timestamp: new Date(),
1240
- isSaved: true,
1241
- format,
1242
- workspaceId: workspaceId || currentWorkspaceId,
1243
- };
1244
-
1245
- setSavedItems((prev) => [newItem, ...prev]);
1246
- setRecentlySavedId(newItem.id);
1247
- setLeftPanelVisible(true);
1248
-
1249
- setTimeout(() => setRecentlySavedId(null), 2000);
1250
- toast.success("Saved for later!");
1251
- };
1252
-
1253
- const handleUnsave = (id: string) => {
1254
- setSavedItems((prev) => prev.filter((item) => item.id !== id));
1255
- toast.success("Removed from saved items");
1256
- };
1257
-
1258
- const handleCreateWorkspace = (payload: { name: string; category: "course" | "personal"; courseId?: string; invites: string[] }) => {
1259
- const id = `group-${Date.now()}`;
1260
- const avatar = `https://api.dicebear.com/7.x/shapes/svg?seed=${encodeURIComponent(payload.name)}`;
1261
-
1262
- const creatorMember: GroupMember = user
1263
- ? {
1264
- id: user.email,
1265
- name: user.name,
1266
- email: user.email,
1267
- avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(user.email)}`,
1268
- }
1269
- : { id: "unknown", name: "Unknown", email: "unknown@email.com" };
1270
-
1271
- const members: GroupMember[] = [
1272
- creatorMember,
1273
- ...payload.invites.map((email) => ({
1274
- id: email,
1275
- name: email.split("@")[0] || email,
1276
- email,
1277
- })),
1278
- ];
1279
-
1280
- let newWorkspace: Workspace;
1281
-
1282
- if (payload.category === "course") {
1283
- const courseInfo = availableCourses.find((c) => c.id === payload.courseId);
1284
- newWorkspace = {
1285
- id,
1286
- name: payload.name,
1287
- type: "group",
1288
- avatar,
1289
- members,
1290
- category: "course",
1291
- courseName: courseInfo?.name || "Untitled Course",
1292
- courseInfo,
1293
- };
1294
- } else {
1295
- newWorkspace = {
1296
- id,
1297
- name: payload.name,
1298
- type: "group",
1299
- avatar,
1300
- members,
1301
- category: "personal",
1302
- isEditable: true,
1303
- };
1304
- }
1305
-
1306
- setWorkspaces((prev) => [...prev, newWorkspace]);
1307
- setCurrentWorkspaceId(id);
1308
-
1309
- if (payload.category === "course" && payload.courseId) {
1310
- setCurrentCourseId(payload.courseId);
1311
- }
1312
-
1313
- toast.success("New group workspace created");
1314
- };
1315
-
1316
- const handleReviewClick = () => {
1317
- setChatMode("review");
1318
- setShowReviewBanner(false);
1319
- localStorage.setItem("reviewBannerDismissed", "true");
1320
- };
1321
-
1322
- const handleDismissReviewBanner = () => {
1323
- setShowReviewBanner(false);
1324
- localStorage.setItem("reviewBannerDismissed", "true");
1325
- };
1326
-
1327
- // ✅ login: hydrate profile and only show onboarding if not completed
1328
  const handleLogin = (newUser: User) => {
1329
  const hydrated = hydrateUserFromStorage(newUser);
1330
  setUser(hydrated);
1331
- setShowOnboarding(!hydrated.onboardingCompleted);
1332
- };
1333
-
1334
- const handleOnboardingComplete = (updatedUser: User) => {
1335
- handleUserSave({ ...updatedUser, onboardingCompleted: true });
1336
- setShowOnboarding(false);
1337
- };
1338
-
1339
- const handleOnboardingSkip = () => {
1340
- updateUser({ onboardingCompleted: true });
1341
- setShowOnboarding(false);
1342
  };
1343
 
1344
  if (!user) return <LoginScreen onLogin={handleLogin} />;
1345
 
1346
- if (showOnboarding && user)
1347
- return <Onboarding user={user} onComplete={handleOnboardingComplete} onSkip={handleOnboardingSkip} />;
1348
-
1349
  return (
1350
  <div className="fixed inset-0 w-full bg-background overflow-hidden">
1351
  <Toaster />
1352
-
1353
- <div className="flex h-full min-h-0 min-w-0 flex-col overflow-hidden">
1354
- <div className="flex-shrink-0">
1355
- <Header
1356
- user={user}
1357
- onMenuClick={() => setLeftSidebarOpen(!leftSidebarOpen)}
1358
- onUserClick={() => setShowProfileEditor(true)}
1359
- isDarkMode={isDarkMode}
1360
- onToggleDarkMode={() => setIsDarkMode(!isDarkMode)}
1361
- language={language}
1362
- onLanguageChange={setLanguage}
1363
- workspaces={workspaces}
1364
- currentWorkspace={currentWorkspace}
1365
- onWorkspaceChange={setCurrentWorkspaceId}
1366
- onCreateWorkspace={handleCreateWorkspace}
1367
- onLogout={() => setUser(null)}
1368
- availableCourses={availableCourses}
1369
- onUserUpdate={handleUserSave}
1370
- reviewStarOpacity={reviewStarOpacity}
1371
- reviewEnergyPct={reviewEnergyPct}
1372
- onStarClick={() => {
1373
- setChatMode("review");
1374
- setShowReviewBanner(false);
1375
- localStorage.setItem("reviewBannerDismissed", "true");
1376
- }}
1377
- showTeacherView={showTeacherDashboard}
1378
- onShowTeacherDashboard={() => setShowTeacherDashboard(true)}
1379
- onBackFromTeacher={() => setShowTeacherDashboard(false)}
1380
- />
1381
- </div>
1382
-
1383
- {showProfileEditor && user && (
1384
- <ProfileEditor user={user} onSave={handleUserSave} onClose={() => setShowProfileEditor(false)} />
1385
- )}
1386
-
1387
- {showReviewBanner && (
1388
- <div className="flex-shrink-0 w-full bg-background border-b border-border relative z-50">
1389
- <ReviewBanner onReview={handleReviewClick} onDismiss={handleDismissReviewBanner} />
1390
- </div>
1391
- )}
1392
-
1393
- <div className="flex flex-1 min-h-0 min-w-0 overflow-hidden relative">
1394
- {!leftPanelVisible && (
1395
- <Button
1396
- variant="secondary"
1397
- size="icon"
1398
- onClick={() => setLeftPanelVisible(true)}
1399
- 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]"
1400
- style={{ left: "-5px", top: "1rem" }}
1401
- title="Open panel"
1402
- >
1403
- <ChevronRight className="h-3 w-3" />
1404
- </Button>
1405
- )}
1406
-
1407
- {leftSidebarOpen && (
1408
- <div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setLeftSidebarOpen(false)} />
1409
- )}
1410
-
1411
- {leftPanelVisible ? (
1412
- <aside className="hidden lg:flex w-80 h-full min-h-0 min-w-0 bg-card border-r border-border overflow-hidden relative flex-col">
1413
- <Button
1414
- variant="secondary"
1415
- size="icon"
1416
- onClick={() => setLeftPanelVisible(false)}
1417
- className="absolute z-[70] h-8 w-5 shadow-lg rounded-full bg-card border border-border"
1418
- style={{ right: "-10px", top: "1rem" }}
1419
- title="Close panel"
1420
- >
1421
- <ChevronLeft className="h-3 w-3" />
1422
- </Button>
1423
-
1424
- <div className="flex-1 min-h-0 min-w-0 overflow-hidden">
1425
- <LeftSidebar
1426
- learningMode={learningMode}
1427
- language={language}
1428
- onLearningModeChange={setLearningMode}
1429
- onLanguageChange={setLanguage}
1430
- spaceType={sidebarSpaceType}
1431
- groupMembers={sidebarGroupMembers}
1432
- user={user}
1433
- onLogin={setUser}
1434
- onLogout={() => setUser(null)}
1435
- isLoggedIn={!!user}
1436
- onEditProfile={() => setShowProfileEditor(true)}
1437
- savedItems={savedItems}
1438
- recentlySavedId={recentlySavedId}
1439
- onUnsave={handleUnsave}
1440
- onSave={handleSave}
1441
- savedChats={savedChats}
1442
- onLoadChat={handleLoadChat}
1443
- onDeleteSavedChat={handleDeleteSavedChat}
1444
- onRenameSavedChat={handleRenameSavedChat}
1445
- currentWorkspaceId={currentWorkspaceId}
1446
- workspaces={sidebarWorkspaces}
1447
- selectedCourse={currentCourseId}
1448
- availableCourses={availableCourses}
1449
- />
1450
- </div>
1451
- </aside>
1452
- ) : null}
1453
-
1454
- <aside
1455
- className={[
1456
- "fixed lg:hidden z-50",
1457
- "left-0 top-0 bottom-0",
1458
- "w-80 bg-card border-r border-border",
1459
- "transform transition-transform duration-300 ease-in-out",
1460
- leftSidebarOpen ? "translate-x-0" : "-translate-x-full",
1461
- "overflow-hidden flex flex-col",
1462
- ].join(" ")}
1463
- >
1464
- <div className="p-4 border-b border-border flex justify-between items-center flex-shrink-0">
1465
- <h3>Settings & Guide</h3>
1466
- <Button variant="ghost" size="icon" onClick={() => setLeftSidebarOpen(false)}>
1467
- <X className="h-5 w-5" />
1468
- </Button>
1469
- </div>
1470
-
1471
- <div className="flex-1 min-h-0 overflow-hidden">
1472
- <LeftSidebar
1473
- learningMode={learningMode}
1474
- language={language}
1475
- onLearningModeChange={setLearningMode}
1476
- onLanguageChange={setLanguage}
1477
- spaceType={sidebarSpaceType}
1478
- groupMembers={sidebarGroupMembers}
1479
- user={user}
1480
- onLogin={setUser}
1481
- onLogout={() => setUser(null)}
1482
- isLoggedIn={!!user}
1483
- onEditProfile={() => setShowProfileEditor(true)}
1484
- savedItems={savedItems}
1485
- recentlySavedId={recentlySavedId}
1486
- onUnsave={handleUnsave}
1487
- onSave={handleSave}
1488
- savedChats={savedChats}
1489
- onLoadChat={handleLoadChat}
1490
- onDeleteSavedChat={handleDeleteSavedChat}
1491
- onRenameSavedChat={handleRenameSavedChat}
1492
- currentWorkspaceId={currentWorkspaceId}
1493
- workspaces={sidebarWorkspaces}
1494
- selectedCourse={currentCourseId}
1495
- availableCourses={availableCourses}
1496
- />
1497
- </div>
1498
- </aside>
1499
-
1500
- <main className="flex flex-1 min-w-0 min-h-0 overflow-hidden flex-col">
1501
- <div className="flex-1 min-h-0 min-w-0 overflow-hidden">
1502
- {showTeacherDashboard ? (
1503
- <TeacherDashboard onBack={() => setShowTeacherDashboard(false)} />
1504
- ) : (
1505
- <ChatArea
1506
- messages={messages}
1507
- onSendMessage={handleSendMessage}
1508
- uploadedFiles={uploadedFiles}
1509
- onFileUpload={handleFileUpload}
1510
- onRemoveFile={handleRemoveFile}
1511
- onFileTypeChange={handleFileTypeChange}
1512
- memoryProgress={memoryProgress}
1513
- isLoggedIn={!!user}
1514
- learningMode={learningMode}
1515
- onClearConversation={() => setShowClearDialog(true)}
1516
- onSaveChat={handleSaveChat}
1517
- onLearningModeChange={setLearningMode}
1518
- spaceType={spaceType}
1519
- chatMode={chatMode}
1520
- onChatModeChange={setChatMode}
1521
- onNextQuestion={handleNextQuestion}
1522
- onStartQuiz={handleStartQuiz}
1523
- quizState={quizState}
1524
- isTyping={isTyping}
1525
- showClearDialog={showClearDialog}
1526
- onConfirmClear={(shouldSave) => {
1527
- handleClearConversation(shouldSave);
1528
- setShowClearDialog(false);
1529
- }}
1530
- onCancelClear={() => setShowClearDialog(false)}
1531
- savedChats={savedChats}
1532
- workspaces={workspaces}
1533
- currentWorkspaceId={currentWorkspaceId}
1534
- onSaveFile={(content, type, _format, targetWorkspaceId) =>
1535
- handleSave(content, type, false, (_format ?? "text") as "pdf" | "text", targetWorkspaceId)
1536
- }
1537
- leftPanelVisible={leftPanelVisible}
1538
- currentCourseId={currentCourseId}
1539
- onCourseChange={handleCourseChange}
1540
- availableCourses={availableCourses}
1541
- showReviewBanner={showReviewBanner}
1542
- onReviewActivity={handleReviewActivity}
1543
- currentUserId={user?.email}
1544
- docType={"Syllabus"}
1545
- // ✅ bio is still allowed to be updated by chat/Clare
1546
- onProfileBioUpdate={(bio) => updateUser({ bio })}
1547
- />
1548
- )}
1549
- </div>
1550
- </main>
1551
- </div>
1552
- </div>
1553
  </div>
1554
  );
1555
  }
 
1
+ // web/src/App.tsx - 教师专用版本(ClareCourseWare)
2
+ import React, { useState, useEffect } from "react";
 
 
3
  import { LoginScreen } from "./components/LoginScreen";
 
 
 
 
 
 
 
 
4
  import { TeacherDashboard } from "./components/TeacherDashboard";
5
+ import { Toaster } from "./components/ui/sonner";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
  export interface User {
 
8
  name: string;
9
  email: string;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  }
11
 
12
  // ✅ localStorage helpers for user profile
13
  function profileStorageKey(email: string) {
14
+ return `teacher_profile::${email}`;
15
  }
16
 
17
  function hydrateUserFromStorage(base: User): User {
 
26
  }
27
 
28
  function App() {
 
 
 
 
 
29
  const [user, setUser] = useState<User | null>(null);
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  // ✅ persist user profile whenever it changes (per-email)
32
  useEffect(() => {
33
  if (!user?.email) return;
 
38
  }
39
  }, [user]);
40
 
41
+ // ✅ 教师专用:登录后直接进入 TeacherDashboard
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  const handleLogin = (newUser: User) => {
43
  const hydrated = hydrateUserFromStorage(newUser);
44
  setUser(hydrated);
 
 
 
 
 
 
 
 
 
 
 
45
  };
46
 
47
  if (!user) return <LoginScreen onLogin={handleLogin} />;
48
 
49
+ // 教师专用:只显示 TeacherDashboard,移除所有学生界面
 
 
50
  return (
51
  <div className="fixed inset-0 w-full bg-background overflow-hidden">
52
  <Toaster />
53
+ <TeacherDashboard onBack={() => setUser(null)} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  </div>
55
  );
56
  }
web/src/components/LoginScreen.tsx CHANGED
@@ -57,9 +57,9 @@ export function LoginScreen({ onLogin }: LoginScreenProps) {
57
  </div>
58
 
59
  <div className="text-center space-y-2">
60
- <h1 className="text-2xl">Welcome to Clare</h1>
61
  <p className="text-sm text-muted-foreground">
62
- Your AI teaching assistant for personalized learning
63
  </p>
64
  </div>
65
 
@@ -82,13 +82,13 @@ export function LoginScreen({ onLogin }: LoginScreenProps) {
82
  </div>
83
 
84
  <div className="space-y-2">
85
- <Label htmlFor="login-email">Email / Student ID</Label>
86
  <Input
87
  id="login-email"
88
  type="email"
89
  value={email}
90
  onChange={(e) => setEmail(e.target.value)}
91
- placeholder="Enter your email or ID"
92
  required
93
  disabled={submitting}
94
  />
 
57
  </div>
58
 
59
  <div className="text-center space-y-2">
60
+ <h1 className="text-2xl">ClareCourseWare</h1>
61
  <p className="text-sm text-muted-foreground">
62
+ AI 智能建课助手 - 教师专用
63
  </p>
64
  </div>
65
 
 
82
  </div>
83
 
84
  <div className="space-y-2">
85
+ <Label htmlFor="login-email">Email / 教师 ID</Label>
86
  <Input
87
  id="login-email"
88
  type="email"
89
  value={email}
90
  onChange={(e) => setEmail(e.target.value)}
91
+ placeholder="Enter your email or teacher ID"
92
  required
93
  disabled={submitting}
94
  />
web/src/components/TeacherDashboard.tsx CHANGED
@@ -7,7 +7,7 @@ import { Input } from "./ui/input";
7
  import { Textarea } from "./ui/textarea";
8
  import { Label } from "./ui/label";
9
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
10
- import { ArrowLeft, Loader2, Lightbulb, FileText, ClipboardList, BarChart3, Target, CalendarDays, MessageSquare, TrendingUp, BookOpen, Languages } from "lucide-react";
11
  import {
12
  apiTeacherStatus,
13
  apiTeacherCourseDescription,
@@ -372,11 +372,8 @@ export function TeacherDashboard({ onBack }: Props) {
372
  return (
373
  <div className="h-full flex flex-col bg-background">
374
  <div className="flex-shrink-0 flex items-center gap-4 px-4 py-3 border-b border-border">
375
- <Button variant="ghost" size="icon" onClick={onBack} title="返回">
376
- <ArrowLeft className="h-5 w-5" />
377
- </Button>
378
  <div>
379
- <h1 className="text-lg font-semibold">AI 智能建课</h1>
380
  <p className="text-sm text-muted-foreground">
381
  共 9 项功能 · 基于 GENAI 课程知识库
382
  {status?.weaviate_configured && (
@@ -396,6 +393,9 @@ export function TeacherDashboard({ onBack }: Props) {
396
  <SelectItem value="auto">跟随输入</SelectItem>
397
  </SelectContent>
398
  </Select>
 
 
 
399
  </div>
400
  </div>
401
 
 
7
  import { Textarea } from "./ui/textarea";
8
  import { Label } from "./ui/label";
9
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
10
+ import { Loader2, Lightbulb, FileText, ClipboardList, BarChart3, Target, CalendarDays, MessageSquare, TrendingUp, BookOpen, Languages } from "lucide-react";
11
  import {
12
  apiTeacherStatus,
13
  apiTeacherCourseDescription,
 
372
  return (
373
  <div className="h-full flex flex-col bg-background">
374
  <div className="flex-shrink-0 flex items-center gap-4 px-4 py-3 border-b border-border">
 
 
 
375
  <div>
376
+ <h1 className="text-lg font-semibold">ClareCourseWare - AI 智能建课</h1>
377
  <p className="text-sm text-muted-foreground">
378
  共 9 项功能 · 基于 GENAI 课程知识库
379
  {status?.weaviate_configured && (
 
393
  <SelectItem value="auto">跟随输入</SelectItem>
394
  </SelectContent>
395
  </Select>
396
+ <Button variant="ghost" size="sm" onClick={onBack} className="ml-2">
397
+ 登出
398
+ </Button>
399
  </div>
400
  </div>
401