SarahXia0405 commited on
Commit
54a2b2a
·
verified ·
1 Parent(s): f671bf8

Update web/src/App.tsx

Browse files
Files changed (1) hide show
  1. web/src/App.tsx +305 -147
web/src/App.tsx CHANGED
@@ -4,10 +4,9 @@ import { LeftSidebar } from './components/LeftSidebar';
4
  import { ChatArea } from './components/ChatArea';
5
  import { RightPanel } from './components/RightPanel';
6
  import { FloatingActionButtons } from './components/FloatingActionButtons';
7
- import { Menu, X, User } from 'lucide-react';
8
  import { Button } from './components/ui/button';
9
  import { Toaster } from './components/ui/sonner';
10
- import { ChevronLeft, ChevronRight } from 'lucide-react';
11
  import { toast } from 'sonner';
12
 
13
  export interface Message {
@@ -15,13 +14,14 @@ export interface Message {
15
  role: 'user' | 'assistant';
16
  content: string;
17
  timestamp: Date;
18
- references?: string[];
19
- sender?: GroupMember; // For group chat
20
  }
21
 
22
  export interface User {
23
  name: string;
24
  email: string;
 
25
  }
26
 
27
  export interface GroupMember {
@@ -34,6 +34,7 @@ export interface GroupMember {
34
 
35
  export type SpaceType = 'individual' | 'group';
36
 
 
37
  export type FileType = 'syllabus' | 'lecture-slides' | 'literature-review' | 'other';
38
 
39
  export interface UploadedFile {
@@ -49,27 +50,38 @@ function App() {
49
  const saved = localStorage.getItem('theme');
50
  return saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches);
51
  });
 
52
  const [user, setUser] = useState<User | null>(null);
 
53
  const [messages, setMessages] = useState<Message[]>([
54
  {
55
  id: '1',
56
  role: 'assistant',
57
- content: "👋 Hi! I'm Clare, your AI teaching assistant for Module 10 – Responsible AI. 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!",
 
58
  timestamp: new Date(),
59
- }
60
  ]);
 
61
  const [learningMode, setLearningMode] = useState<LearningMode>('concept');
62
  const [language, setLanguage] = useState<Language>('auto');
63
  const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
 
 
64
  const [memoryProgress, setMemoryProgress] = useState(36);
 
65
  const [leftSidebarOpen, setLeftSidebarOpen] = useState(false);
66
  const [rightPanelOpen, setRightPanelOpen] = useState(false);
67
  const [rightPanelVisible, setRightPanelVisible] = useState(true);
 
68
  const [spaceType, setSpaceType] = useState<SpaceType>('individual');
 
69
  const [exportResult, setExportResult] = useState('');
70
  const [resultType, setResultType] = useState<'export' | 'quiz' | 'summary' | null>(null);
71
-
72
- // Mock group members
 
 
73
  const [groupMembers] = useState<GroupMember[]>([
74
  { id: 'clare', name: 'Clare AI', email: 'clare@ai.assistant', isAI: true },
75
  { id: '1', name: 'Sarah Johnson', email: 'sarah.j@university.edu' },
@@ -82,13 +94,99 @@ function App() {
82
  localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
83
  }, [isDarkMode]);
84
 
85
- const handleSendMessage = (content: string) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  if (!content.trim() || !user) return;
87
 
88
- // In group mode, add sender info
89
- const sender: GroupMember | undefined = spaceType === 'group'
90
- ? { id: user.email, name: user.name, email: user.email }
91
- : undefined;
92
 
93
  const userMessage: Message = {
94
  id: Date.now().toString(),
@@ -98,135 +196,212 @@ function App() {
98
  sender,
99
  };
100
 
101
- setMessages(prev => [...prev, userMessage]);
102
-
103
- // In group mode, only respond if @Clare or @clare is mentioned
104
- const shouldAIRespond = spaceType === 'individual' ||
105
- content.toLowerCase().includes('@clare');
106
-
107
- if (shouldAIRespond) {
108
- // Simulate AI response
109
- setTimeout(() => {
110
- const responses: Record<LearningMode, string> = {
111
- concept: "Great question! Let me break this concept down for you. In Responsible AI, this relates to ensuring our AI systems are fair, transparent, and accountable. Would you like me to explain any specific aspect in more detail?",
112
- socratic: "That's an interesting point! Let me ask you this: What do you think are the key ethical considerations when deploying AI systems? Take a moment to think about it.",
113
- exam: "Let me test your understanding with a quick question: Which of the following is NOT a principle of Responsible AI? A) Fairness B) Transparency C) Profit Maximization D) Accountability",
114
- assignment: "I can help you with that assignment! Let's break it down into manageable steps. First, what specific aspect are you working on?",
115
- summary: "Here's a quick summary: Responsible AI focuses on developing and deploying AI systems that are ethical, fair, transparent, and accountable to society.",
116
- };
117
-
118
- const assistantMessage: Message = {
119
- id: (Date.now() + 1).toString(),
120
- role: 'assistant',
121
- content: responses[learningMode],
122
- timestamp: new Date(),
123
- references: ['Module 10, Section 2.3', 'Lecture Notes - Week 5'],
124
- sender: spaceType === 'group' ? groupMembers.find(m => m.isAI) : undefined,
125
- };
126
-
127
- setMessages(prev => [...prev, assistantMessage]);
128
- }, 1000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  }
130
  };
131
 
132
- const handleFileUpload = (files: File[]) => {
133
- const newFiles: UploadedFile[] = files.map(file => ({
 
 
 
 
 
 
134
  file,
135
- type: 'other' as FileType, // Default type
136
  }));
137
- setUploadedFiles(prev => [...prev, ...newFiles]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  };
139
 
140
  const handleRemoveFile = (index: number) => {
141
- setUploadedFiles(prev => prev.filter((_, i) => i !== index));
142
  };
143
 
144
- const handleFileTypeChange = (index: number, type: FileType) => {
145
- setUploadedFiles(prev => prev.map((file, i) =>
146
- i === index ? { ...file, type } : file
147
- ));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  };
149
 
150
  const handleClearConversation = () => {
151
- setMessages([{
152
- id: '1',
153
- role: 'assistant',
154
- content: "👋 Hi! I'm Clare, your AI teaching assistant for Module 10 – Responsible AI. 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!",
155
- timestamp: new Date(),
156
- }]);
 
 
 
 
157
  };
158
 
159
- const handleExport = () => {
160
- const result = `# Conversation Export
161
- Date: ${new Date().toLocaleDateString()}
162
- Student: ${user?.name}
163
-
164
- ## Summary
165
- This conversation covered key concepts in Module 10 – Responsible AI, including ethical considerations, fairness, transparency, and accountability in AI systems.
166
-
167
- ## Key Takeaways
168
- 1. Understanding the principles of Responsible AI
169
- 2. Real-world applications and implications
170
- 3. Best practices for ethical AI development
171
-
172
- Exported successfully! ✓`;
173
-
174
- setExportResult(result);
175
- setResultType('export');
176
- toast.success('Conversation exported!');
 
 
 
177
  };
178
 
179
- const handleQuiz = () => {
180
- const quiz = `# Micro-Quiz: Responsible AI
181
-
182
- **Question 1:** Which of the following is a key principle of Responsible AI?
183
- a) Profit maximization
184
- b) Transparency
185
- c) Rapid deployment
186
- d) Cost reduction
187
-
188
- **Question 2:** What is algorithmic fairness?
189
- (Short answer expected)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
190
 
191
- **Question 3:** True or False: AI systems should always prioritize accuracy over fairness.
 
 
 
 
192
 
193
- Generate quiz based on your conversation!`;
194
-
195
  setExportResult(quiz);
196
  setResultType('quiz');
197
  toast.success('Quiz generated!');
198
  };
199
 
200
- const handleSummary = () => {
201
- const summary = `# Learning Summary
202
-
203
- ## Today's Session
204
- **Duration:** 25 minutes
205
- **Topics Covered:** 3
206
- **Messages Exchanged:** 12
207
-
208
- ## Key Concepts Discussed
209
- • Principles of Responsible AI
210
- • Ethical considerations in AI development
211
- • Fairness and transparency in algorithms
212
-
213
- ## Recommended Next Steps
214
- 1. Review Module 10, Section 2.3
215
- 2. Complete practice quiz on algorithmic fairness
216
- 3. Read additional resources on AI ethics
217
-
218
- ## Progress Update
219
- You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
220
-
221
- setExportResult(summary);
222
- setResultType('summary');
223
- toast.success('Summary generated!');
224
- };
225
-
226
  return (
227
  <div className="min-h-screen bg-background flex flex-col">
228
  <Toaster />
229
- <Header
230
  user={user}
231
  onMenuClick={() => setLeftSidebarOpen(!leftSidebarOpen)}
232
  onUserClick={() => setRightPanelOpen(!rightPanelOpen)}
@@ -237,14 +412,11 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
237
  <div className="flex-1 flex overflow-hidden">
238
  {/* Mobile Sidebar Toggle - Left */}
239
  {leftSidebarOpen && (
240
- <div
241
- className="fixed inset-0 bg-black/50 z-40 lg:hidden"
242
- onClick={() => setLeftSidebarOpen(false)}
243
- />
244
  )}
245
 
246
  {/* Left Sidebar */}
247
- <aside
248
  className={`
249
  fixed lg:static inset-y-0 left-0 z-50
250
  w-80 bg-card border-r border-border
@@ -257,14 +429,11 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
257
  >
258
  <div className="lg:hidden p-4 border-b border-border flex justify-between items-center">
259
  <h3>Settings & Guide</h3>
260
- <Button
261
- variant="ghost"
262
- size="icon"
263
- onClick={() => setLeftSidebarOpen(false)}
264
- >
265
  <X className="h-5 w-5" />
266
  </Button>
267
  </div>
 
268
  <LeftSidebar
269
  learningMode={learningMode}
270
  language={language}
@@ -291,20 +460,18 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
291
  onClearConversation={handleClearConversation}
292
  onLearningModeChange={setLearningMode}
293
  spaceType={spaceType}
 
294
  />
295
  </main>
296
 
297
  {/* Mobile Sidebar Toggle - Right */}
298
  {rightPanelOpen && (
299
- <div
300
- className="fixed inset-0 bg-black/50 z-40 lg:hidden"
301
- onClick={() => setRightPanelOpen(false)}
302
- />
303
  )}
304
 
305
  {/* Right Panel */}
306
  {rightPanelVisible && (
307
- <aside
308
  className={`
309
  fixed lg:static inset-y-0 right-0 z-50
310
  w-80 bg-card border-l border-border
@@ -317,18 +484,15 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
317
  >
318
  <div className="lg:hidden p-4 border-b border-border flex justify-between items-center">
319
  <h3>Account & Actions</h3>
320
- <Button
321
- variant="ghost"
322
- size="icon"
323
- onClick={() => setRightPanelOpen(false)}
324
- >
325
  <X className="h-5 w-5" />
326
  </Button>
327
  </div>
 
328
  <RightPanel
329
  user={user}
330
- onLogin={setUser}
331
- onLogout={() => setUser(null)}
332
  isLoggedIn={!!user}
333
  onClose={() => setRightPanelVisible(false)}
334
  exportResult={exportResult}
@@ -338,24 +502,18 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
338
  />
339
  </aside>
340
  )}
341
-
342
  {/* Toggle Right Panel Button - Desktop only */}
343
  <Button
344
  variant="outline"
345
  size="icon"
346
  onClick={() => setRightPanelVisible(!rightPanelVisible)}
347
  className={`hidden lg:flex fixed top-20 z-[70] h-8 w-5 shadow-lg transition-all rounded-l-full rounded-r-none border-r-0 ${
348
- rightPanelVisible
349
- ? 'right-[320px]'
350
- : 'right-0'
351
  }`}
352
  title={rightPanelVisible ? 'Close panel' : 'Open panel'}
353
  >
354
- {rightPanelVisible ? (
355
- <ChevronRight className="h-3 w-3" />
356
- ) : (
357
- <ChevronLeft className="h-3 w-3" />
358
- )}
359
  </Button>
360
 
361
  {/* Floating Action Buttons - Desktop only, when panel is closed */}
@@ -374,4 +532,4 @@ You've covered 65% of Module 10 content. Keep up the great work! 🎉`;
374
  );
375
  }
376
 
377
- export default App;
 
4
  import { ChatArea } from './components/ChatArea';
5
  import { RightPanel } from './components/RightPanel';
6
  import { FloatingActionButtons } from './components/FloatingActionButtons';
7
+ import { X, ChevronLeft, ChevronRight } from 'lucide-react';
8
  import { Button } from './components/ui/button';
9
  import { Toaster } from './components/ui/sonner';
 
10
  import { toast } from 'sonner';
11
 
12
  export interface Message {
 
14
  role: 'user' | 'assistant';
15
  content: string;
16
  timestamp: Date;
17
+ references?: string[]; // rendered strings for UI
18
+ sender?: GroupMember; // for group chat
19
  }
20
 
21
  export interface User {
22
  name: string;
23
  email: string;
24
+ userId: string; // used by backend session
25
  }
26
 
27
  export interface GroupMember {
 
34
 
35
  export type SpaceType = 'individual' | 'group';
36
 
37
+ // UI file types -> backend doc_type mapping in upload
38
  export type FileType = 'syllabus' | 'lecture-slides' | 'literature-review' | 'other';
39
 
40
  export interface UploadedFile {
 
50
  const saved = localStorage.getItem('theme');
51
  return saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches);
52
  });
53
+
54
  const [user, setUser] = useState<User | null>(null);
55
+
56
  const [messages, setMessages] = useState<Message[]>([
57
  {
58
  id: '1',
59
  role: 'assistant',
60
+ content:
61
+ "Hi! I'm Clare, your AI teaching assistant for Module 10 – Responsible AI. Ask me anything about the course, or upload documents to get started.",
62
  timestamp: new Date(),
63
+ },
64
  ]);
65
+
66
  const [learningMode, setLearningMode] = useState<LearningMode>('concept');
67
  const [language, setLanguage] = useState<Language>('auto');
68
  const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
69
+
70
+ // MemoryLine expects 0-100-ish number; your backend returns progress_pct 0..1
71
  const [memoryProgress, setMemoryProgress] = useState(36);
72
+
73
  const [leftSidebarOpen, setLeftSidebarOpen] = useState(false);
74
  const [rightPanelOpen, setRightPanelOpen] = useState(false);
75
  const [rightPanelVisible, setRightPanelVisible] = useState(true);
76
+
77
  const [spaceType, setSpaceType] = useState<SpaceType>('individual');
78
+
79
  const [exportResult, setExportResult] = useState('');
80
  const [resultType, setResultType] = useState<'export' | 'quiz' | 'summary' | null>(null);
81
+
82
+ const [isTyping, setIsTyping] = useState(false);
83
+
84
+ // Mock group members (UI only)
85
  const [groupMembers] = useState<GroupMember[]>([
86
  { id: 'clare', name: 'Clare AI', email: 'clare@ai.assistant', isAI: true },
87
  { id: '1', name: 'Sarah Johnson', email: 'sarah.j@university.edu' },
 
94
  localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
95
  }, [isDarkMode]);
96
 
97
+ // ---------- helpers ----------
98
+ const mapLanguageToBackend = (lang: Language) => {
99
+ if (lang === 'en') return 'English';
100
+ if (lang === 'zh') return 'Chinese';
101
+ return 'Auto';
102
+ };
103
+
104
+ const mapFileTypeToDocType = (t: FileType) => {
105
+ switch (t) {
106
+ case 'syllabus':
107
+ return 'Syllabus';
108
+ case 'lecture-slides':
109
+ return 'Lecture Slides';
110
+ case 'literature-review':
111
+ return 'Literature Review / Paper';
112
+ default:
113
+ return 'Other';
114
+ }
115
+ };
116
+
117
+ const normalizeRefs = (refs: any): string[] => {
118
+ if (!Array.isArray(refs)) return [];
119
+ return refs
120
+ .map((r) => {
121
+ const sf = r?.source_file ?? r?.sourceFile ?? '';
122
+ const sec = r?.section ?? '';
123
+ const s = [sf, sec].filter(Boolean).join(' · ');
124
+ return s || '';
125
+ })
126
+ .filter(Boolean);
127
+ };
128
+
129
+ const fetchMemoryLine = async (u: User) => {
130
+ try {
131
+ const res = await fetch(`/api/memoryline?user_id=${encodeURIComponent(u.userId)}`);
132
+ if (!res.ok) return;
133
+ const data = await res.json();
134
+ const pct = typeof data?.progress_pct === 'number' ? Math.round(data.progress_pct * 100) : null;
135
+ if (pct !== null) setMemoryProgress(pct);
136
+ } catch {
137
+ // ignore
138
+ }
139
+ };
140
+
141
+ // ---------- API actions ----------
142
+ const handleLogin = async (payload: { name: string; email: string }) => {
143
+ const name = payload.name.trim();
144
+ const email = payload.email.trim();
145
+
146
+ // Use email as stable userId (simple MVP)
147
+ const userId = email;
148
+
149
+ try {
150
+ const res = await fetch('/api/login', {
151
+ method: 'POST',
152
+ headers: { 'Content-Type': 'application/json' },
153
+ body: JSON.stringify({ name, user_id: userId }),
154
+ });
155
+
156
+ if (!res.ok) {
157
+ const txt = await res.text();
158
+ toast.error(`Login failed: ${txt}`);
159
+ return;
160
+ }
161
+
162
+ const data = await res.json();
163
+ if (!data?.ok) {
164
+ toast.error('Login failed');
165
+ return;
166
+ }
167
+
168
+ const u: User = { name, email, userId };
169
+ setUser(u);
170
+ toast.success(`Welcome, ${name}!`);
171
+
172
+ // pull memoryline once after login
173
+ fetchMemoryLine(u);
174
+ } catch (e: any) {
175
+ toast.error(`Login error: ${e?.message ?? 'unknown'}`);
176
+ }
177
+ };
178
+
179
+ const handleLogout = () => {
180
+ setUser(null);
181
+ toast.success('Logged out successfully');
182
+ };
183
+
184
+ const handleSendMessage = async (content: string) => {
185
  if (!content.trim() || !user) return;
186
 
187
+ // In group mode, add sender info (UI)
188
+ const sender: GroupMember | undefined =
189
+ spaceType === 'group' ? { id: user.email, name: user.name, email: user.email } : undefined;
 
190
 
191
  const userMessage: Message = {
192
  id: Date.now().toString(),
 
196
  sender,
197
  };
198
 
199
+ setMessages((prev) => [...prev, userMessage]);
200
+
201
+ // In group mode, only respond if @Clare is mentioned
202
+ const shouldAIRespond = spaceType === 'individual' || content.toLowerCase().includes('@clare');
203
+ if (!shouldAIRespond) return;
204
+
205
+ // Optional: strip @clare mention from text to reduce noise
206
+ const cleaned = spaceType === 'group' ? content.replace(/@clare/gi, '').trim() : content;
207
+
208
+ setIsTyping(true);
209
+ try {
210
+ const res = await fetch('/api/chat', {
211
+ method: 'POST',
212
+ headers: { 'Content-Type': 'application/json' },
213
+ body: JSON.stringify({
214
+ user_id: user.userId,
215
+ message: cleaned,
216
+ learning_mode: learningMode,
217
+ language_preference: mapLanguageToBackend(language),
218
+ doc_type: 'Syllabus', // for now (backend uses uploaded doc_type for parsing; chat can stay Syllabus)
219
+ }),
220
+ });
221
+
222
+ if (!res.ok) {
223
+ const txt = await res.text();
224
+ toast.error(`Chat failed: ${txt}`);
225
+ return;
226
+ }
227
+
228
+ const data = await res.json();
229
+ const reply = data?.reply ?? '';
230
+ const refs = normalizeRefs(data?.refs);
231
+
232
+ const assistantMessage: Message = {
233
+ id: (Date.now() + 1).toString(),
234
+ role: 'assistant',
235
+ content: reply || '(No response)',
236
+ timestamp: new Date(),
237
+ references: refs,
238
+ sender: spaceType === 'group' ? groupMembers.find((m) => m.isAI) : undefined,
239
+ };
240
+
241
+ setMessages((prev) => [...prev, assistantMessage]);
242
+
243
+ // Refresh memoryline after each AI response (cheap + keeps UI alive)
244
+ fetchMemoryLine(user);
245
+ } catch (e: any) {
246
+ toast.error(`Chat error: ${e?.message ?? 'unknown'}`);
247
+ } finally {
248
+ setIsTyping(false);
249
  }
250
  };
251
 
252
+ const handleFileUpload = async (files: File[]) => {
253
+ if (!user) {
254
+ toast.info('Please log in first');
255
+ return;
256
+ }
257
+
258
+ // UI list (keep for display)
259
+ const newFiles: UploadedFile[] = files.map((file) => ({
260
  file,
261
+ type: 'other' as FileType,
262
  }));
263
+ setUploadedFiles((prev) => [...prev, ...newFiles]);
264
+
265
+ // Upload each file immediately (default doc_type=Other; user can change type later, but MVP OK)
266
+ // If you want “type change triggers re-upload”, we can add next iteration.
267
+ for (const f of files) {
268
+ try {
269
+ const form = new FormData();
270
+ form.append('user_id', user.userId);
271
+ form.append('doc_type', 'Other');
272
+ form.append('file', f);
273
+
274
+ const res = await fetch('/api/upload', { method: 'POST', body: form });
275
+ if (!res.ok) {
276
+ const txt = await res.text();
277
+ toast.error(`Upload failed (${f.name}): ${txt}`);
278
+ continue;
279
+ }
280
+
281
+ const data = await res.json();
282
+ toast.success(`Uploaded: ${f.name} (+${data?.added_chunks ?? 0} chunks)`);
283
+ fetchMemoryLine(user);
284
+ } catch (e: any) {
285
+ toast.error(`Upload error (${f.name}): ${e?.message ?? 'unknown'}`);
286
+ }
287
+ }
288
  };
289
 
290
  const handleRemoveFile = (index: number) => {
291
+ setUploadedFiles((prev) => prev.filter((_, i) => i !== index));
292
  };
293
 
294
+ const handleFileTypeChange = async (index: number, type: FileType) => {
295
+ setUploadedFiles((prev) =>
296
+ prev.map((file, i) => (i === index ? { ...file, type } : file))
297
+ );
298
+
299
+ // MVP behavior: re-upload that file with the selected doc_type so backend can parse correctly.
300
+ // This avoids “I selected Syllabus but backend never saw it.”
301
+ if (!user) return;
302
+ const target = uploadedFiles[index];
303
+ if (!target?.file) return;
304
+
305
+ try {
306
+ const form = new FormData();
307
+ form.append('user_id', user.userId);
308
+ form.append('doc_type', mapFileTypeToDocType(type));
309
+ form.append('file', target.file);
310
+
311
+ const res = await fetch('/api/upload', { method: 'POST', body: form });
312
+ if (!res.ok) {
313
+ const txt = await res.text();
314
+ toast.error(`Re-upload failed: ${txt}`);
315
+ return;
316
+ }
317
+
318
+ const data = await res.json();
319
+ toast.success(`Updated type: ${target.file.name} → ${mapFileTypeToDocType(type)} (+${data?.added_chunks ?? 0})`);
320
+ fetchMemoryLine(user);
321
+ } catch (e: any) {
322
+ toast.error(`Re-upload error: ${e?.message ?? 'unknown'}`);
323
+ }
324
  };
325
 
326
  const handleClearConversation = () => {
327
+ setMessages([
328
+ {
329
+ id: '1',
330
+ role: 'assistant',
331
+ content:
332
+ "Hi! I'm Clare, your AI teaching assistant for Module 10 – Responsible AI. Ask me anything about the course, or upload documents to get started.",
333
+ timestamp: new Date(),
334
+ },
335
+ ]);
336
+ toast.success('Conversation cleared');
337
  };
338
 
339
+ const handleExport = async () => {
340
+ if (!user) return toast.info('Please log in first');
341
+
342
+ try {
343
+ const res = await fetch('/api/export', {
344
+ method: 'POST',
345
+ headers: { 'Content-Type': 'application/json' },
346
+ body: JSON.stringify({ user_id: user.userId, learning_mode: learningMode }),
347
+ });
348
+ if (!res.ok) {
349
+ const txt = await res.text();
350
+ toast.error(`Export failed: ${txt}`);
351
+ return;
352
+ }
353
+ const data = await res.json();
354
+ setExportResult(data?.markdown ?? '');
355
+ setResultType('export');
356
+ toast.success('Conversation exported!');
357
+ } catch (e: any) {
358
+ toast.error(`Export error: ${e?.message ?? 'unknown'}`);
359
+ }
360
  };
361
 
362
+ const handleSummary = async () => {
363
+ if (!user) return toast.info('Please log in first');
364
+
365
+ try {
366
+ const res = await fetch('/api/summary', {
367
+ method: 'POST',
368
+ headers: { 'Content-Type': 'application/json' },
369
+ body: JSON.stringify({
370
+ user_id: user.userId,
371
+ learning_mode: learningMode,
372
+ language_preference: mapLanguageToBackend(language),
373
+ }),
374
+ });
375
+ if (!res.ok) {
376
+ const txt = await res.text();
377
+ toast.error(`Summary failed: ${txt}`);
378
+ return;
379
+ }
380
+ const data = await res.json();
381
+ setExportResult(data?.markdown ?? '');
382
+ setResultType('summary');
383
+ toast.success('Summary generated!');
384
+ } catch (e: any) {
385
+ toast.error(`Summary error: ${e?.message ?? 'unknown'}`);
386
+ }
387
+ };
388
 
389
+ // You的后端目前没提供 /api/quiz,这里先保持“前端生成文案”占位;
390
+ // 下一步如果你要真实 quiz,我们加 /api/quiz endpoint 即可。
391
+ const handleQuiz = async () => {
392
+ const quiz = `# Micro-Quiz (MVP)
393
+ We can wire this to a real /api/quiz endpoint next.
394
 
395
+ Try asking Clare in Exam Prep mode to generate quiz questions from your uploaded docs.`;
 
396
  setExportResult(quiz);
397
  setResultType('quiz');
398
  toast.success('Quiz generated!');
399
  };
400
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
401
  return (
402
  <div className="min-h-screen bg-background flex flex-col">
403
  <Toaster />
404
+ <Header
405
  user={user}
406
  onMenuClick={() => setLeftSidebarOpen(!leftSidebarOpen)}
407
  onUserClick={() => setRightPanelOpen(!rightPanelOpen)}
 
412
  <div className="flex-1 flex overflow-hidden">
413
  {/* Mobile Sidebar Toggle - Left */}
414
  {leftSidebarOpen && (
415
+ <div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setLeftSidebarOpen(false)} />
 
 
 
416
  )}
417
 
418
  {/* Left Sidebar */}
419
+ <aside
420
  className={`
421
  fixed lg:static inset-y-0 left-0 z-50
422
  w-80 bg-card border-r border-border
 
429
  >
430
  <div className="lg:hidden p-4 border-b border-border flex justify-between items-center">
431
  <h3>Settings & Guide</h3>
432
+ <Button variant="ghost" size="icon" onClick={() => setLeftSidebarOpen(false)}>
 
 
 
 
433
  <X className="h-5 w-5" />
434
  </Button>
435
  </div>
436
+
437
  <LeftSidebar
438
  learningMode={learningMode}
439
  language={language}
 
460
  onClearConversation={handleClearConversation}
461
  onLearningModeChange={setLearningMode}
462
  spaceType={spaceType}
463
+ isTyping={isTyping}
464
  />
465
  </main>
466
 
467
  {/* Mobile Sidebar Toggle - Right */}
468
  {rightPanelOpen && (
469
+ <div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setRightPanelOpen(false)} />
 
 
 
470
  )}
471
 
472
  {/* Right Panel */}
473
  {rightPanelVisible && (
474
+ <aside
475
  className={`
476
  fixed lg:static inset-y-0 right-0 z-50
477
  w-80 bg-card border-l border-border
 
484
  >
485
  <div className="lg:hidden p-4 border-b border-border flex justify-between items-center">
486
  <h3>Account & Actions</h3>
487
+ <Button variant="ghost" size="icon" onClick={() => setRightPanelOpen(false)}>
 
 
 
 
488
  <X className="h-5 w-5" />
489
  </Button>
490
  </div>
491
+
492
  <RightPanel
493
  user={user}
494
+ onLogin={handleLogin}
495
+ onLogout={handleLogout}
496
  isLoggedIn={!!user}
497
  onClose={() => setRightPanelVisible(false)}
498
  exportResult={exportResult}
 
502
  />
503
  </aside>
504
  )}
505
+
506
  {/* Toggle Right Panel Button - Desktop only */}
507
  <Button
508
  variant="outline"
509
  size="icon"
510
  onClick={() => setRightPanelVisible(!rightPanelVisible)}
511
  className={`hidden lg:flex fixed top-20 z-[70] h-8 w-5 shadow-lg transition-all rounded-l-full rounded-r-none border-r-0 ${
512
+ rightPanelVisible ? 'right-[320px]' : 'right-0'
 
 
513
  }`}
514
  title={rightPanelVisible ? 'Close panel' : 'Open panel'}
515
  >
516
+ {rightPanelVisible ? <ChevronRight className="h-3 w-3" /> : <ChevronLeft className="h-3 w-3" />}
 
 
 
 
517
  </Button>
518
 
519
  {/* Floating Action Buttons - Desktop only, when panel is closed */}
 
532
  );
533
  }
534
 
535
+ export default App;