SarahXia0405 commited on
Commit
8c006a1
·
verified ·
1 Parent(s): 17a4c7b

Update web/src/App.tsx

Browse files
Files changed (1) hide show
  1. web/src/App.tsx +160 -223
web/src/App.tsx CHANGED
@@ -1,12 +1,14 @@
 
1
  import React, { useState, useEffect } from 'react';
2
  import { Header } from './components/Header';
3
  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 { 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,14 +16,13 @@ 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,7 +35,6 @@ 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 {
@@ -45,6 +45,8 @@ export interface UploadedFile {
45
  export type LearningMode = 'concept' | 'socratic' | 'exam' | 'assignment' | 'summary';
46
  export type Language = 'auto' | 'en' | 'zh';
47
 
 
 
48
  function App() {
49
  const [isDarkMode, setIsDarkMode] = useState(() => {
50
  const saved = localStorage.getItem('theme');
@@ -58,17 +60,16 @@ function App() {
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);
@@ -77,11 +78,8 @@ function App() {
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,97 +92,75 @@ function App() {
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
 
@@ -198,204 +174,165 @@ function App() {
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 (
@@ -410,12 +347,10 @@ Try asking Clare in Exam Prep mode to generate quiz questions from your uploaded
410
  />
411
 
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
@@ -433,7 +368,6 @@ Try asking Clare in Exam Prep mode to generate quiz questions from your uploaded
433
  <X className="h-5 w-5" />
434
  </Button>
435
  </div>
436
-
437
  <LeftSidebar
438
  learningMode={learningMode}
439
  language={language}
@@ -445,7 +379,6 @@ Try asking Clare in Exam Prep mode to generate quiz questions from your uploaded
445
  />
446
  </aside>
447
 
448
- {/* Main Chat Area */}
449
  <main className="flex-1 flex flex-col min-w-0">
450
  <ChatArea
451
  messages={messages}
@@ -460,16 +393,20 @@ Try asking Clare in Exam Prep mode to generate quiz questions from your uploaded
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={`
@@ -488,7 +425,6 @@ Try asking Clare in Exam Prep mode to generate quiz questions from your uploaded
488
  <X className="h-5 w-5" />
489
  </Button>
490
  </div>
491
-
492
  <RightPanel
493
  user={user}
494
  onLogin={handleLogin}
@@ -499,11 +435,13 @@ Try asking Clare in Exam Prep mode to generate quiz questions from your uploaded
499
  setExportResult={setExportResult}
500
  resultType={resultType}
501
  setResultType={setResultType}
 
 
 
502
  />
503
  </aside>
504
  )}
505
 
506
- {/* Toggle Right Panel Button - Desktop only */}
507
  <Button
508
  variant="outline"
509
  size="icon"
@@ -516,7 +454,6 @@ Try asking Clare in Exam Prep mode to generate quiz questions from your uploaded
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 */}
520
  {!rightPanelVisible && (
521
  <FloatingActionButtons
522
  user={user}
 
1
+ // web/src/App.tsx
2
  import React, { useState, useEffect } from 'react';
3
  import { Header } from './components/Header';
4
  import { LeftSidebar } from './components/LeftSidebar';
5
  import { ChatArea } from './components/ChatArea';
6
  import { RightPanel } from './components/RightPanel';
7
  import { FloatingActionButtons } from './components/FloatingActionButtons';
8
+ import { X } from 'lucide-react';
9
  import { Button } from './components/ui/button';
10
  import { Toaster } from './components/ui/sonner';
11
+ import { ChevronLeft, ChevronRight } from 'lucide-react';
12
  import { toast } from 'sonner';
13
 
14
  export interface Message {
 
16
  role: 'user' | 'assistant';
17
  content: string;
18
  timestamp: Date;
19
+ references?: string[];
20
+ sender?: GroupMember;
21
  }
22
 
23
  export interface User {
24
  name: string;
25
+ email: string; // we also use as user_id
 
26
  }
27
 
28
  export interface GroupMember {
 
35
 
36
  export type SpaceType = 'individual' | 'group';
37
 
 
38
  export type FileType = 'syllabus' | 'lecture-slides' | 'literature-review' | 'other';
39
 
40
  export interface UploadedFile {
 
45
  export type LearningMode = 'concept' | 'socratic' | 'exam' | 'assignment' | 'summary';
46
  export type Language = 'auto' | 'en' | 'zh';
47
 
48
+ type ResultType = 'export' | 'quiz' | 'summary' | null;
49
+
50
  function App() {
51
  const [isDarkMode, setIsDarkMode] = useState(() => {
52
  const saved = localStorage.getItem('theme');
 
60
  id: '1',
61
  role: 'assistant',
62
  content:
63
+ "Hi! I'm Clare, your AI teaching assistant for Module 10 – Responsible AI. Upload your course materials and ask me anything.",
64
  timestamp: new Date(),
65
  },
66
  ]);
67
 
68
  const [learningMode, setLearningMode] = useState<LearningMode>('concept');
69
  const [language, setLanguage] = useState<Language>('auto');
 
70
 
71
+ const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
72
+ const [memoryProgress, setMemoryProgress] = useState(40);
73
 
74
  const [leftSidebarOpen, setLeftSidebarOpen] = useState(false);
75
  const [rightPanelOpen, setRightPanelOpen] = useState(false);
 
78
  const [spaceType, setSpaceType] = useState<SpaceType>('individual');
79
 
80
  const [exportResult, setExportResult] = useState('');
81
+ const [resultType, setResultType] = useState<ResultType>(null);
82
 
 
 
 
83
  const [groupMembers] = useState<GroupMember[]>([
84
  { id: 'clare', name: 'Clare AI', email: 'clare@ai.assistant', isAI: true },
85
  { id: '1', name: 'Sarah Johnson', email: 'sarah.j@university.edu' },
 
92
  localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
93
  }, [isDarkMode]);
94
 
95
+ // -----------------------------
96
+ // API helpers
97
+ // -----------------------------
98
+ const apiPost = async <T,>(path: string, body: any): Promise<T> => {
99
+ const res = await fetch(path, {
100
+ method: 'POST',
101
+ headers: { 'Content-Type': 'application/json' },
102
+ body: JSON.stringify(body),
103
+ });
104
+ if (!res.ok) {
105
+ const text = await res.text().catch(() => '');
106
+ throw new Error(`${path} failed: ${res.status} ${text}`);
 
 
 
 
 
107
  }
108
+ return res.json();
109
  };
110
 
111
+ const apiGet = async <T,>(path: string): Promise<T> => {
112
+ const res = await fetch(path, { method: 'GET' });
113
+ if (!res.ok) {
114
+ const text = await res.text().catch(() => '');
115
+ throw new Error(`${path} failed: ${res.status} ${text}`);
116
+ }
117
+ return res.json();
 
 
 
118
  };
119
 
120
+ const fileTypeToDocType = (t: FileType): string => {
121
+ // backend normalizes too, but keep consistent
122
+ if (t === 'syllabus') return 'Syllabus';
123
+ if (t === 'lecture-slides') return 'Lecture Slides';
124
+ if (t === 'literature-review') return 'Literature Review / Paper';
125
+ return 'Other';
 
 
 
 
126
  };
127
 
128
+ // -----------------------------
129
+ // Auth
130
+ // -----------------------------
131
+ const handleLogin = async (u: User) => {
132
+ // use email as user_id
133
+ const user_id = u.email.trim();
134
+ const name = u.name.trim();
135
 
136
+ if (!user_id || !name) {
137
+ toast.error('Missing name/email');
138
+ return;
139
+ }
140
 
141
  try {
142
+ await apiPost<{ ok: boolean }>('/api/login', { user_id, name });
143
+ setUser({ name, email: user_id });
144
+ toast.success(`Welcome, ${name}`);
145
+ // sync memoryline
146
+ const ml = await apiGet<{ progress_pct: number }>('/api/memoryline?user_id=' + encodeURIComponent(user_id));
147
+ setMemoryProgress(Math.round((ml.progress_pct || 0) * 100));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
  } catch (e: any) {
149
+ toast.error(e.message || 'Login failed');
150
  }
151
  };
152
 
153
  const handleLogout = () => {
154
  setUser(null);
155
+ toast.success('Logged out');
156
  };
157
 
158
+ // -----------------------------
159
+ // Chat
160
+ // -----------------------------
161
  const handleSendMessage = async (content: string) => {
162
  if (!content.trim() || !user) return;
163
 
 
164
  const sender: GroupMember | undefined =
165
  spaceType === 'group' ? { id: user.email, name: user.name, email: user.email } : undefined;
166
 
 
174
 
175
  setMessages((prev) => [...prev, userMessage]);
176
 
 
177
  const shouldAIRespond = spaceType === 'individual' || content.toLowerCase().includes('@clare');
178
  if (!shouldAIRespond) return;
179
 
 
 
 
 
180
  try {
181
+ const resp = await apiPost<{
182
+ reply: string;
183
+ session_status_md: string;
184
+ refs: { source_file?: string; section?: string }[];
185
+ latency_ms?: number;
186
+ }>('/api/chat', {
187
+ user_id: user.email,
188
+ message: content,
189
+ learning_mode: learningMode,
190
+ language_preference: language === 'auto' ? 'Auto' : language === 'en' ? 'English' : 'Chinese',
191
+ doc_type: 'Syllabus',
192
  });
193
 
194
+ const refs = (resp.refs || []).map((r) => {
195
+ const a = r.source_file || 'source';
196
+ const b = r.section || 'section';
197
+ return `${a} — ${b}`;
198
+ });
 
 
 
 
199
 
200
  const assistantMessage: Message = {
201
  id: (Date.now() + 1).toString(),
202
  role: 'assistant',
203
+ content: resp.reply || '(empty response)',
204
  timestamp: new Date(),
205
  references: refs,
206
  sender: spaceType === 'group' ? groupMembers.find((m) => m.isAI) : undefined,
207
  };
208
 
209
  setMessages((prev) => [...prev, assistantMessage]);
 
 
 
210
  } catch (e: any) {
211
+ toast.error(e.message || 'Chat failed');
212
+ const assistantMessage: Message = {
213
+ id: (Date.now() + 1).toString(),
214
+ role: 'assistant',
215
+ content: 'Sorry — the server returned an error. Please retry.',
216
+ timestamp: new Date(),
217
+ };
218
+ setMessages((prev) => [...prev, assistantMessage]);
219
  }
220
  };
221
 
222
+ // -----------------------------
223
+ // Upload
224
+ // -----------------------------
225
+ const handleFileUpload = (files: File[]) => {
 
 
 
226
  const newFiles: UploadedFile[] = files.map((file) => ({
227
  file,
228
+ type: 'other',
229
  }));
230
  setUploadedFiles((prev) => [...prev, ...newFiles]);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
  };
232
 
233
  const handleRemoveFile = (index: number) => {
234
  setUploadedFiles((prev) => prev.filter((_, i) => i !== index));
235
  };
236
 
237
+ const handleFileTypeChange = (index: number, type: FileType) => {
238
+ setUploadedFiles((prev) => prev.map((f, i) => (i === index ? { ...f, type } : f)));
239
+ };
 
240
 
241
+ // Your FileUploadArea likely triggers an "Upload" confirm button.
242
+ // If not, you can call this after user confirms in the modal.
243
+ const uploadSingle = async (f: UploadedFile) => {
244
+ if (!user) throw new Error('Not logged in');
 
245
 
246
+ const form = new FormData();
247
+ form.append('user_id', user.email);
248
+ form.append('doc_type', fileTypeToDocType(f.type));
249
+ form.append('file', f.file);
250
+
251
+ const res = await fetch('/api/upload', { method: 'POST', body: form });
252
+ if (!res.ok) {
253
+ const text = await res.text().catch(() => '');
254
+ throw new Error(`/api/upload failed: ${res.status} ${text}`);
255
+ }
256
+ return res.json() as Promise<{ ok: boolean; added_chunks: number; status_md: string }>;
257
+ };
258
+
259
+ // If your UI already has an "Upload" button in the modal,
260
+ // wire it to this function via FileUploadArea; otherwise keep as-is.
261
+ const handleUploadAllPending = async () => {
262
+ if (!user) {
263
+ toast.error('Please log in first');
264
+ return;
265
+ }
266
  try {
267
+ for (const f of uploadedFiles) {
268
+ // naive: upload all; you can also only upload newly added ones
269
+ const out = await uploadSingle(f);
270
+ toast.success(`${f.file.name} uploaded (+${out.added_chunks} chunks)`);
 
 
 
 
 
 
271
  }
272
+ const ml = await apiGet<{ progress_pct: number }>('/api/memoryline?user_id=' + encodeURIComponent(user.email));
273
+ setMemoryProgress(Math.round((ml.progress_pct || 0) * 100));
 
 
274
  } catch (e: any) {
275
+ toast.error(e.message || 'Upload failed');
276
  }
277
  };
278
 
279
+ // -----------------------------
280
+ // Export / Summary / Quiz
281
+ // -----------------------------
282
  const handleClearConversation = () => {
283
  setMessages([
284
  {
285
  id: '1',
286
  role: 'assistant',
287
  content:
288
+ "Hi! I'm Clare, your AI teaching assistant for Module 10 – Responsible AI. Upload your course materials and ask me anything.",
289
  timestamp: new Date(),
290
  },
291
  ]);
 
292
  };
293
 
294
  const handleExport = async () => {
295
+ if (!user) return toast.error('Please log in');
 
296
  try {
297
+ const out = await apiPost<{ markdown: string }>('/api/export', {
298
+ user_id: user.email,
299
+ learning_mode: learningMode,
 
300
  });
301
+ setExportResult(out.markdown || '');
 
 
 
 
 
 
302
  setResultType('export');
303
+ toast.success('Export generated');
304
  } catch (e: any) {
305
+ toast.error(e.message || 'Export failed');
306
  }
307
  };
308
 
309
  const handleSummary = async () => {
310
+ if (!user) return toast.error('Please log in');
 
311
  try {
312
+ const out = await apiPost<{ markdown: string }>('/api/summary', {
313
+ user_id: user.email,
314
+ learning_mode: learningMode,
315
+ language_preference: language === 'auto' ? 'Auto' : language === 'en' ? 'English' : 'Chinese',
 
 
 
 
316
  });
317
+ setExportResult(out.markdown || '');
 
 
 
 
 
 
318
  setResultType('summary');
319
+ toast.success('Summary generated');
320
  } catch (e: any) {
321
+ toast.error(e.message || 'Summary failed');
322
  }
323
  };
324
 
325
+ const handleQuiz = () => {
326
+ // No backend endpoint yet; keep mock for now
 
327
  const quiz = `# Micro-Quiz (MVP)
 
328
 
329
+ 1) What is one core principle of Responsible AI?
330
+ 2) Give one example of fairness risk.
331
+ 3) Explain transparency in one sentence.
332
+ `;
333
  setExportResult(quiz);
334
  setResultType('quiz');
335
+ toast.success('Quiz generated (mock)');
336
  };
337
 
338
  return (
 
347
  />
348
 
349
  <div className="flex-1 flex overflow-hidden">
 
350
  {leftSidebarOpen && (
351
  <div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setLeftSidebarOpen(false)} />
352
  )}
353
 
 
354
  <aside
355
  className={`
356
  fixed lg:static inset-y-0 left-0 z-50
 
368
  <X className="h-5 w-5" />
369
  </Button>
370
  </div>
 
371
  <LeftSidebar
372
  learningMode={learningMode}
373
  language={language}
 
379
  />
380
  </aside>
381
 
 
382
  <main className="flex-1 flex flex-col min-w-0">
383
  <ChatArea
384
  messages={messages}
 
393
  onClearConversation={handleClearConversation}
394
  onLearningModeChange={setLearningMode}
395
  spaceType={spaceType}
 
396
  />
397
+
398
+ {/* Optional: if you want a manual "Upload All" button for MVP */}
399
+ {/* <div className="p-3 border-t">
400
+ <Button onClick={handleUploadAllPending} disabled={!user || uploadedFiles.length === 0}>
401
+ Upload all pending files
402
+ </Button>
403
+ </div> */}
404
  </main>
405
 
 
406
  {rightPanelOpen && (
407
  <div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setRightPanelOpen(false)} />
408
  )}
409
 
 
410
  {rightPanelVisible && (
411
  <aside
412
  className={`
 
425
  <X className="h-5 w-5" />
426
  </Button>
427
  </div>
 
428
  <RightPanel
429
  user={user}
430
  onLogin={handleLogin}
 
435
  setExportResult={setExportResult}
436
  resultType={resultType}
437
  setResultType={setResultType}
438
+ onExport={handleExport}
439
+ onQuiz={handleQuiz}
440
+ onSummary={handleSummary}
441
  />
442
  </aside>
443
  )}
444
 
 
445
  <Button
446
  variant="outline"
447
  size="icon"
 
454
  {rightPanelVisible ? <ChevronRight className="h-3 w-3" /> : <ChevronLeft className="h-3 w-3" />}
455
  </Button>
456
 
 
457
  {!rightPanelVisible && (
458
  <FloatingActionButtons
459
  user={user}