SarahXia0405 commited on
Commit
1a5dd5e
·
verified ·
1 Parent(s): 56b7341

Update web/src/App.tsx

Browse files
Files changed (1) hide show
  1. web/src/App.tsx +226 -192
web/src/App.tsx CHANGED
@@ -1,4 +1,4 @@
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';
@@ -15,13 +15,12 @@ export interface Message {
15
  content: string;
16
  timestamp: Date;
17
  references?: string[];
18
- sender?: GroupMember;
19
  }
20
 
21
  export interface User {
22
  name: string;
23
  email: string;
24
- user_id: string; // IMPORTANT: server session key
25
  }
26
 
27
  export interface GroupMember {
@@ -46,35 +45,19 @@ export interface UploadedFile {
46
  export type LearningMode = 'concept' | 'socratic' | 'exam' | 'assignment' | 'summary';
47
  export type Language = 'auto' | 'en' | 'zh';
48
 
49
- // ----------------------------
50
- // API helpers (same-origin)
51
- // ----------------------------
52
- async function apiPostJson<T>(path: string, body: unknown): Promise<T> {
53
- const res = await fetch(path, {
54
- method: 'POST',
55
- headers: { 'Content-Type': 'application/json' },
56
- body: JSON.stringify(body),
57
- });
58
- if (!res.ok) {
59
- const txt = await res.text();
60
- throw new Error(`POST ${path} failed: ${res.status} ${txt}`);
61
- }
62
- return res.json();
63
- }
64
 
65
- async function apiPostForm<T>(path: string, formData: FormData): Promise<T> {
66
- const res = await fetch(path, {
67
- method: 'POST',
68
- body: formData,
69
- });
70
- if (!res.ok) {
71
- const txt = await res.text();
72
- throw new Error(`POST ${path} failed: ${res.status} ${txt}`);
73
- }
74
- return res.json();
75
- }
76
 
77
- // FE FileType -> BE doc_type mapping
78
  function mapFileTypeToDocType(t: FileType): string {
79
  switch (t) {
80
  case 'syllabus':
@@ -88,6 +71,28 @@ function mapFileTypeToDocType(t: FileType): string {
88
  }
89
  }
90
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
  function App() {
92
  const [isDarkMode, setIsDarkMode] = useState(() => {
93
  const saved = localStorage.getItem('theme');
@@ -101,15 +106,16 @@ function App() {
101
  id: '1',
102
  role: 'assistant',
103
  content:
104
- "Hi! I'm Clare, your AI teaching assistant for Module 10 – Responsible AI. Upload your syllabus/notes and ask me anything.",
105
  timestamp: new Date(),
106
  },
107
  ]);
108
 
109
  const [learningMode, setLearningMode] = useState<LearningMode>('concept');
110
  const [language, setLanguage] = useState<Language>('auto');
 
111
  const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
112
- const [memoryProgress, setMemoryProgress] = useState(36);
113
 
114
  const [leftSidebarOpen, setLeftSidebarOpen] = useState(false);
115
  const [rightPanelOpen, setRightPanelOpen] = useState(false);
@@ -120,6 +126,7 @@ function App() {
120
  const [exportResult, setExportResult] = useState('');
121
  const [resultType, setResultType] = useState<'export' | 'quiz' | 'summary' | null>(null);
122
 
 
123
  const [groupMembers] = useState<GroupMember[]>([
124
  { id: 'clare', name: 'Clare AI', email: 'clare@ai.assistant', isAI: true },
125
  { id: '1', name: 'Sarah Johnson', email: 'sarah.j@university.edu' },
@@ -132,130 +139,35 @@ function App() {
132
  localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
133
  }, [isDarkMode]);
134
 
135
- // ----------------------------
136
- // Login: call backend /api/login
137
- // ----------------------------
138
- const handleLogin = async (name: string, email: string) => {
139
- // Use email as stable user_id for MVP
140
- const user_id = email.trim().toLowerCase();
141
-
142
- try {
143
- const resp = await apiPostJson<{ ok: boolean; user: { name: string; user_id: string } }>(
144
- '/api/login',
145
- { name, user_id }
146
- );
147
-
148
- if (!resp.ok) throw new Error('login failed');
149
-
150
- setUser({ name: resp.user.name, email, user_id: resp.user.user_id });
151
- toast.success(`Welcome, ${resp.user.name}!`);
152
- } catch (e: any) {
153
- console.error(e);
154
- toast.error(`Login failed: ${e?.message || 'unknown error'}`);
155
- }
156
- };
157
 
158
- const handleLogout = () => {
159
- setUser(null);
160
- toast.success('Logged out successfully');
161
- };
162
-
163
- // ----------------------------
164
- // Upload: call backend /api/upload
165
- // ----------------------------
166
- const uploadOneFile = async (f: UploadedFile) => {
167
- if (!user) throw new Error('not logged in');
168
-
169
- const form = new FormData();
170
- form.append('user_id', user.user_id);
171
- form.append('doc_type', mapFileTypeToDocType(f.type));
172
- form.append('file', f.file);
173
-
174
- const resp = await apiPostForm<{ ok: boolean; added_chunks: number; status_md: string }>(
175
- '/api/upload',
176
- form
177
- );
178
-
179
- return resp;
180
- };
181
-
182
- // When FileUploadArea fires onFileUpload(files), we add them + immediately upload them (type default other).
183
- // If you want strict "must choose type before upload", then your FileUploadArea should call onFileTypeChange first.
184
- const handleFileUpload = async (files: File[]) => {
185
- if (!user) {
186
- toast.error('Please log in first');
187
- return;
188
- }
189
-
190
- const newFiles: UploadedFile[] = files.map((file) => ({
191
- file,
192
- type: 'other',
193
- uploaded: false,
194
- }));
195
-
196
- // Optimistic add
197
- setUploadedFiles((prev) => [...prev, ...newFiles]);
198
-
199
- // Upload sequentially to reduce memory spikes
200
- for (const uf of newFiles) {
201
- try {
202
- toast.message(`Uploading: ${uf.file.name}`);
203
- const r = await uploadOneFile(uf);
204
-
205
- setUploadedFiles((prev) =>
206
- prev.map((x) =>
207
- x.file === uf.file
208
- ? { ...x, uploaded: true, uploadedChunks: r.added_chunks }
209
- : x
210
- )
211
- );
212
- toast.success(`Uploaded ${uf.file.name} (+${r.added_chunks} chunks)`);
213
- } catch (e: any) {
214
- console.error(e);
215
- toast.error(`Upload failed: ${uf.file.name} — ${e?.message || 'unknown error'}`);
216
- }
217
- }
218
- };
219
-
220
- // If user changes file type AFTER selecting, re-upload with correct doc_type (important for syllabus)
221
- const handleFileTypeChange = async (index: number, type: FileType) => {
222
- setUploadedFiles((prev) =>
223
- prev.map((file, i) => (i === index ? { ...file, type } : file))
224
- );
225
-
226
- if (!user) return;
227
-
228
- // Re-upload this file with the new type (this is what fixes: "doc_type=Other" bug)
229
- const target = uploadedFiles[index];
230
- if (!target) return;
231
-
232
- try {
233
- toast.message(`Re-uploading as ${type}: ${target.file.name}`);
234
- const r = await uploadOneFile({ ...target, type });
235
- setUploadedFiles((prev) =>
236
- prev.map((x, i) =>
237
- i === index ? { ...x, uploaded: true, uploadedChunks: r.added_chunks } : x
238
- )
239
- );
240
- toast.success(`Updated type and uploaded (+${r.added_chunks} chunks)`);
241
- } catch (e: any) {
242
- console.error(e);
243
- toast.error(`Re-upload failed: ${e?.message || 'unknown error'}`);
244
- }
245
- };
246
-
247
- const handleRemoveFile = (index: number) => {
248
- setUploadedFiles((prev) => prev.filter((_, i) => i !== index));
249
- };
250
-
251
- // ----------------------------
252
- // Chat: call backend /api/chat
253
- // ----------------------------
254
  const handleSendMessage = async (content: string) => {
255
- if (!content.trim() || !user) return;
 
 
 
 
256
 
 
257
  const sender: GroupMember | undefined =
258
- spaceType === 'group' ? { id: user.email, name: user.name, email: user.email } : undefined;
 
 
259
 
260
  const userMessage: Message = {
261
  id: Date.now().toString(),
@@ -264,28 +176,21 @@ function App() {
264
  timestamp: new Date(),
265
  sender,
266
  };
 
267
 
268
- setMessages((prev) => [...prev, userMessage]);
269
-
270
- const shouldAIRespond = spaceType === 'individual' || content.toLowerCase().includes('@clare');
271
  if (!shouldAIRespond) return;
272
 
273
  try {
274
- const resp = await apiPostJson<{
275
- reply: string;
276
- session_status_md: string;
277
- refs: { source_file?: string; section?: string }[];
278
- latency_ms?: number;
279
- }>('/api/chat', {
280
- user_id: user.user_id,
281
  message: content,
282
  learning_mode: learningMode,
283
- language_preference: language === 'auto' ? 'Auto' : language === 'en' ? 'English' : 'Chinese',
284
- doc_type: 'Syllabus',
285
  });
286
 
287
  const refs = (resp.refs || [])
288
- .map((r) => [r.source_file, r.section].filter(Boolean).join(' '))
289
  .filter(Boolean);
290
 
291
  const assistantMessage: Message = {
@@ -294,13 +199,91 @@ function App() {
294
  content: resp.reply || '(empty reply)',
295
  timestamp: new Date(),
296
  references: refs.length ? refs : undefined,
297
- sender: spaceType === 'group' ? groupMembers.find((m) => m.isAI) : undefined,
298
  };
299
 
300
- setMessages((prev) => [...prev, assistantMessage]);
301
  } catch (e: any) {
302
  console.error(e);
303
  toast.error(`Chat failed: ${e?.message || 'unknown error'}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
304
  }
305
  };
306
 
@@ -310,39 +293,75 @@ function App() {
310
  id: '1',
311
  role: 'assistant',
312
  content:
313
- "Hi! I'm Clare, your AI teaching assistant for Module 10 – Responsible AI. Upload your syllabus/notes and ask me anything.",
314
  timestamp: new Date(),
315
  },
316
  ]);
317
  };
318
 
319
- // Your existing mock export/quiz/summary can stay. (Optional: wire them to /api/export /api/summary later.)
320
- const handleExport = () => {
321
- const result = `# Conversation Export
322
- Date: ${new Date().toLocaleDateString()}
323
- Student: ${user?.name || ''}
 
 
 
 
 
 
 
 
 
 
324
 
325
- (Placeholder export; wire to /api/export if needed.)`;
326
- setExportResult(result);
327
- setResultType('export');
328
- toast.success('Conversation exported!');
 
 
 
 
 
 
 
 
 
329
  };
330
 
 
331
  const handleQuiz = () => {
332
- const quiz = `# Micro-Quiz (Placeholder)
333
- (You can wire to backend later.)`;
 
 
 
 
 
 
334
  setExportResult(quiz);
335
  setResultType('quiz');
336
  toast.success('Quiz generated!');
337
  };
338
 
339
- const handleSummary = () => {
340
- const summary = `# Summary (Placeholder)
341
- (You can wire to backend /api/summary later.)`;
342
- setExportResult(summary);
343
- setResultType('summary');
344
- toast.success('Summary generated!');
345
- };
 
 
 
 
 
 
 
 
 
 
346
 
347
  return (
348
  <div className="min-h-screen bg-background flex flex-col">
@@ -356,10 +375,15 @@ Student: ${user?.name || ''}
356
  />
357
 
358
  <div className="flex-1 flex overflow-hidden">
 
359
  {leftSidebarOpen && (
360
- <div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setLeftSidebarOpen(false)} />
 
 
 
361
  )}
362
 
 
363
  <aside
364
  className={`
365
  fixed lg:static inset-y-0 left-0 z-50
@@ -388,6 +412,7 @@ Student: ${user?.name || ''}
388
  />
389
  </aside>
390
 
 
391
  <main className="flex-1 flex flex-col min-w-0">
392
  <ChatArea
393
  messages={messages}
@@ -396,8 +421,10 @@ Student: ${user?.name || ''}
396
  onFileUpload={handleFileUpload}
397
  onRemoveFile={handleRemoveFile}
398
  onFileTypeChange={handleFileTypeChange}
 
 
399
  memoryProgress={memoryProgress}
400
- isLoggedIn={!!user}
401
  learningMode={learningMode}
402
  onClearConversation={handleClearConversation}
403
  onLearningModeChange={setLearningMode}
@@ -405,10 +432,15 @@ Student: ${user?.name || ''}
405
  />
406
  </main>
407
 
 
408
  {rightPanelOpen && (
409
- <div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setRightPanelOpen(false)} />
 
 
 
410
  )}
411
 
 
412
  {rightPanelVisible && (
413
  <aside
414
  className={`
@@ -422,7 +454,7 @@ Student: ${user?.name || ''}
422
  `}
423
  >
424
  <div className="lg:hidden p-4 border-b border-border flex justify-between items-center">
425
- <h3>Account</h3>
426
  <Button variant="ghost" size="icon" onClick={() => setRightPanelOpen(false)}>
427
  <X className="h-5 w-5" />
428
  </Button>
@@ -430,8 +462,8 @@ Student: ${user?.name || ''}
430
 
431
  <RightPanel
432
  user={user}
433
- onLogin={handleLogin}
434
- onLogout={handleLogout}
435
  isLoggedIn={!!user}
436
  onClose={() => setRightPanelVisible(false)}
437
  exportResult={exportResult}
@@ -442,6 +474,7 @@ Student: ${user?.name || ''}
442
  </aside>
443
  )}
444
 
 
445
  <Button
446
  variant="outline"
447
  size="icon"
@@ -454,6 +487,7 @@ Student: ${user?.name || ''}
454
  {rightPanelVisible ? <ChevronRight className="h-3 w-3" /> : <ChevronLeft className="h-3 w-3" />}
455
  </Button>
456
 
 
457
  {!rightPanelVisible && (
458
  <FloatingActionButtons
459
  user={user}
 
1
+ import React, { useState, useEffect, useMemo } from 'react';
2
  import { Header } from './components/Header';
3
  import { LeftSidebar } from './components/LeftSidebar';
4
  import { ChatArea } from './components/ChatArea';
 
15
  content: string;
16
  timestamp: Date;
17
  references?: string[];
18
+ sender?: GroupMember; // For group chat
19
  }
20
 
21
  export interface User {
22
  name: string;
23
  email: string;
 
24
  }
25
 
26
  export interface GroupMember {
 
45
  export type LearningMode = 'concept' | 'socratic' | 'exam' | 'assignment' | 'summary';
46
  export type Language = 'auto' | 'en' | 'zh';
47
 
48
+ type ChatApiResp = {
49
+ reply: string;
50
+ session_status_md?: string;
51
+ refs?: Array<{ source_file?: string; section?: string }>;
52
+ latency_ms?: number;
53
+ };
 
 
 
 
 
 
 
 
 
54
 
55
+ type UploadApiResp = {
56
+ ok: boolean;
57
+ added_chunks: number;
58
+ status_md: string;
59
+ };
 
 
 
 
 
 
60
 
 
61
  function mapFileTypeToDocType(t: FileType): string {
62
  switch (t) {
63
  case 'syllabus':
 
71
  }
72
  }
73
 
74
+ async function apiPostJson<T>(path: string, payload: any): Promise<T> {
75
+ const res = await fetch(path, {
76
+ method: 'POST',
77
+ headers: { 'Content-Type': 'application/json' },
78
+ body: JSON.stringify(payload),
79
+ });
80
+ if (!res.ok) {
81
+ const txt = await res.text().catch(() => '');
82
+ throw new Error(`HTTP ${res.status}: ${txt || res.statusText}`);
83
+ }
84
+ return (await res.json()) as T;
85
+ }
86
+
87
+ async function apiPostForm<T>(path: string, form: FormData): Promise<T> {
88
+ const res = await fetch(path, { method: 'POST', body: form });
89
+ if (!res.ok) {
90
+ const txt = await res.text().catch(() => '');
91
+ throw new Error(`HTTP ${res.status}: ${txt || res.statusText}`);
92
+ }
93
+ return (await res.json()) as T;
94
+ }
95
+
96
  function App() {
97
  const [isDarkMode, setIsDarkMode] = useState(() => {
98
  const saved = localStorage.getItem('theme');
 
106
  id: '1',
107
  role: 'assistant',
108
  content:
109
+ "Hi! I'm Clare, your AI teaching assistant for Module 10 – Responsible AI. Upload your materials (e.g., syllabus/lecture slides) and ask me anything.",
110
  timestamp: new Date(),
111
  },
112
  ]);
113
 
114
  const [learningMode, setLearningMode] = useState<LearningMode>('concept');
115
  const [language, setLanguage] = useState<Language>('auto');
116
+
117
  const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([]);
118
+ const [memoryProgress, setMemoryProgress] = useState(40);
119
 
120
  const [leftSidebarOpen, setLeftSidebarOpen] = useState(false);
121
  const [rightPanelOpen, setRightPanelOpen] = useState(false);
 
126
  const [exportResult, setExportResult] = useState('');
127
  const [resultType, setResultType] = useState<'export' | 'quiz' | 'summary' | null>(null);
128
 
129
+ // Mock group members
130
  const [groupMembers] = useState<GroupMember[]>([
131
  { id: 'clare', name: 'Clare AI', email: 'clare@ai.assistant', isAI: true },
132
  { id: '1', name: 'Sarah Johnson', email: 'sarah.j@university.edu' },
 
139
  localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
140
  }, [isDarkMode]);
141
 
142
+ const userId = useMemo(() => {
143
+ // 兼容你现在 RightPanel User 结构:email 作为 user_id
144
+ // 没登录也能测试:兜底 0405
145
+ return (user?.email || '0405').trim();
146
+ }, [user]);
147
+
148
+ const currentDocTypeForChat = useMemo(() => {
149
+ // 给 /api/chat 传一个“更合理”的 doc_type(不影响 upload)
150
+ const hasSyllabus = uploadedFiles.some(f => f.type === 'syllabus' && f.uploaded);
151
+ if (hasSyllabus) return 'Syllabus';
152
+ const hasSlides = uploadedFiles.some(f => f.type === 'lecture-slides' && f.uploaded);
153
+ if (hasSlides) return 'Lecture Slides';
154
+ const hasLit = uploadedFiles.some(f => f.type === 'literature-review' && f.uploaded);
155
+ if (hasLit) return 'Literature Review / Paper';
156
+ return 'Other';
157
+ }, [uploadedFiles]);
 
 
 
 
 
 
158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  const handleSendMessage = async (content: string) => {
160
+ if (!content.trim()) return;
161
+
162
+ // Group mode: only respond if @clare mentioned
163
+ const shouldAIRespond =
164
+ spaceType === 'individual' || content.toLowerCase().includes('@clare');
165
 
166
+ // Sender info in group mode
167
  const sender: GroupMember | undefined =
168
+ spaceType === 'group' && user
169
+ ? { id: user.email, name: user.name, email: user.email }
170
+ : undefined;
171
 
172
  const userMessage: Message = {
173
  id: Date.now().toString(),
 
176
  timestamp: new Date(),
177
  sender,
178
  };
179
+ setMessages(prev => [...prev, userMessage]);
180
 
 
 
 
181
  if (!shouldAIRespond) return;
182
 
183
  try {
184
+ const resp = await apiPostJson<ChatApiResp>('/api/chat', {
185
+ user_id: userId,
 
 
 
 
 
186
  message: content,
187
  learning_mode: learningMode,
188
+ language_preference: language === 'auto' ? 'Auto' : language,
189
+ doc_type: currentDocTypeForChat,
190
  });
191
 
192
  const refs = (resp.refs || [])
193
+ .map(r => [r.source_file, r.section].filter(Boolean).join(' / '))
194
  .filter(Boolean);
195
 
196
  const assistantMessage: Message = {
 
199
  content: resp.reply || '(empty reply)',
200
  timestamp: new Date(),
201
  references: refs.length ? refs : undefined,
202
+ sender: spaceType === 'group' ? groupMembers.find(m => m.isAI) : undefined,
203
  };
204
 
205
+ setMessages(prev => [...prev, assistantMessage]);
206
  } catch (e: any) {
207
  console.error(e);
208
  toast.error(`Chat failed: ${e?.message || 'unknown error'}`);
209
+ const assistantMessage: Message = {
210
+ id: (Date.now() + 1).toString(),
211
+ role: 'assistant',
212
+ content: `Sorry — chat request failed. ${e?.message || ''}`,
213
+ timestamp: new Date(),
214
+ };
215
+ setMessages(prev => [...prev, assistantMessage]);
216
+ }
217
+ };
218
+
219
+ // ✅ 关键:选中文件时“只入库,不上传”
220
+ const handleFileUpload = (files: File[]) => {
221
+ const newFiles: UploadedFile[] = files.map(file => ({
222
+ file,
223
+ type: 'other',
224
+ uploaded: false,
225
+ }));
226
+ setUploadedFiles(prev => [...prev, ...newFiles]);
227
+ toast.message('Files added. Select a File Type, then click Upload.');
228
+ };
229
+
230
+ const handleRemoveFile = (index: number) => {
231
+ setUploadedFiles(prev => prev.filter((_, i) => i !== index));
232
+ };
233
+
234
+ const handleFileTypeChange = (index: number, type: FileType) => {
235
+ setUploadedFiles(prev =>
236
+ prev.map((f, i) => (i === index ? { ...f, type } : f))
237
+ );
238
+ };
239
+
240
+ // ✅ 真正上传:只在用户点 Upload/Upload All 时触发
241
+ const handleUploadSingle = async (index: number) => {
242
+ const target = uploadedFiles[index];
243
+ if (!target) return;
244
+
245
+ try {
246
+ const form = new FormData();
247
+ form.append('user_id', userId);
248
+ form.append('doc_type', mapFileTypeToDocType(target.type)); // Network 里会看到这里
249
+ form.append('file', target.file);
250
+
251
+ const r = await apiPostForm<UploadApiResp>('/api/upload', form);
252
+
253
+ if (!r.ok) {
254
+ throw new Error('Upload response ok=false');
255
+ }
256
+
257
+ setUploadedFiles(prev =>
258
+ prev.map((x, i) =>
259
+ i === index
260
+ ? { ...x, uploaded: true, uploadedChunks: r.added_chunks }
261
+ : x
262
+ )
263
+ );
264
+
265
+ toast.success(`Uploaded: ${target.file.name} (+${r.added_chunks} chunks)`);
266
+ } catch (e: any) {
267
+ console.error(e);
268
+ toast.error(`Upload failed: ${e?.message || 'unknown error'}`);
269
+ }
270
+ };
271
+
272
+ const handleUploadAllPending = async () => {
273
+ const pendingIdx = uploadedFiles
274
+ .map((f, i) => ({ f, i }))
275
+ .filter(x => !x.f.uploaded)
276
+ .map(x => x.i);
277
+
278
+ if (!pendingIdx.length) {
279
+ toast.message('No pending files to upload.');
280
+ return;
281
+ }
282
+
283
+ for (const idx of pendingIdx) {
284
+ // 顺序上传,便于定位问题
285
+ // eslint-disable-next-line no-await-in-loop
286
+ await handleUploadSingle(idx);
287
  }
288
  };
289
 
 
293
  id: '1',
294
  role: 'assistant',
295
  content:
296
+ "Hi! I'm Clare, your AI teaching assistant for Module 10 – Responsible AI. Upload your materials (e.g., syllabus/lecture slides) and ask me anything.",
297
  timestamp: new Date(),
298
  },
299
  ]);
300
  };
301
 
302
+ // 这些 action 你后端现在还没实现(/api/export /api/summary 你有)
303
+ // 这里先直接调后端,结果显示在 RightPanel
304
+ const handleExport = async () => {
305
+ try {
306
+ const r = await apiPostJson<{ markdown: string }>('/api/export', {
307
+ user_id: userId,
308
+ learning_mode: learningMode,
309
+ });
310
+ setExportResult(r.markdown || '');
311
+ setResultType('export');
312
+ toast.success('Conversation exported!');
313
+ } catch (e: any) {
314
+ toast.error(`Export failed: ${e?.message || 'unknown error'}`);
315
+ }
316
+ };
317
 
318
+ const handleSummary = async () => {
319
+ try {
320
+ const r = await apiPostJson<{ markdown: string }>('/api/summary', {
321
+ user_id: userId,
322
+ learning_mode: learningMode,
323
+ language_preference: language === 'auto' ? 'Auto' : language,
324
+ });
325
+ setExportResult(r.markdown || '');
326
+ setResultType('summary');
327
+ toast.success('Summary generated!');
328
+ } catch (e: any) {
329
+ toast.error(`Summary failed: ${e?.message || 'unknown error'}`);
330
+ }
331
  };
332
 
333
+ // Quiz 你后端未必有接口,这里先保留 mock
334
  const handleQuiz = () => {
335
+ const quiz = `# Micro-Quiz: Responsible AI
336
+
337
+ 1) Which is a key principle of Responsible AI?
338
+ A) Profit maximization
339
+ B) Transparency
340
+ C) Rapid deployment
341
+ D) Cost reduction
342
+ `;
343
  setExportResult(quiz);
344
  setResultType('quiz');
345
  toast.success('Quiz generated!');
346
  };
347
 
348
+ // Memoryline:轻量拉一下(失败不影响)
349
+ useEffect(() => {
350
+ const run = async () => {
351
+ try {
352
+ const res = await fetch(`/api/memoryline?user_id=${encodeURIComponent(userId)}`);
353
+ if (!res.ok) return;
354
+ const j = await res.json();
355
+ const pct = typeof j?.progress_pct === 'number' ? j.progress_pct : null;
356
+ if (pct !== null) {
357
+ setMemoryProgress(Math.round(pct * 100));
358
+ }
359
+ } catch {
360
+ // ignore
361
+ }
362
+ };
363
+ run();
364
+ }, [userId]);
365
 
366
  return (
367
  <div className="min-h-screen bg-background flex flex-col">
 
375
  />
376
 
377
  <div className="flex-1 flex overflow-hidden">
378
+ {/* Mobile Sidebar Toggle - Left */}
379
  {leftSidebarOpen && (
380
+ <div
381
+ className="fixed inset-0 bg-black/50 z-40 lg:hidden"
382
+ onClick={() => setLeftSidebarOpen(false)}
383
+ />
384
  )}
385
 
386
+ {/* Left Sidebar */}
387
  <aside
388
  className={`
389
  fixed lg:static inset-y-0 left-0 z-50
 
412
  />
413
  </aside>
414
 
415
+ {/* Main Chat Area */}
416
  <main className="flex-1 flex flex-col min-w-0">
417
  <ChatArea
418
  messages={messages}
 
421
  onFileUpload={handleFileUpload}
422
  onRemoveFile={handleRemoveFile}
423
  onFileTypeChange={handleFileTypeChange}
424
+ onUploadSingle={handleUploadSingle}
425
+ onUploadAllPending={handleUploadAllPending}
426
  memoryProgress={memoryProgress}
427
+ isLoggedIn={true} // 允许你不登录也跑通后端(user_id=0405)
428
  learningMode={learningMode}
429
  onClearConversation={handleClearConversation}
430
  onLearningModeChange={setLearningMode}
 
432
  />
433
  </main>
434
 
435
+ {/* Mobile Sidebar Toggle - Right */}
436
  {rightPanelOpen && (
437
+ <div
438
+ className="fixed inset-0 bg-black/50 z-40 lg:hidden"
439
+ onClick={() => setRightPanelOpen(false)}
440
+ />
441
  )}
442
 
443
+ {/* Right Panel */}
444
  {rightPanelVisible && (
445
  <aside
446
  className={`
 
454
  `}
455
  >
456
  <div className="lg:hidden p-4 border-b border-border flex justify-between items-center">
457
+ <h3>Account & Actions</h3>
458
  <Button variant="ghost" size="icon" onClick={() => setRightPanelOpen(false)}>
459
  <X className="h-5 w-5" />
460
  </Button>
 
462
 
463
  <RightPanel
464
  user={user}
465
+ onLogin={setUser}
466
+ onLogout={() => setUser(null)}
467
  isLoggedIn={!!user}
468
  onClose={() => setRightPanelVisible(false)}
469
  exportResult={exportResult}
 
474
  </aside>
475
  )}
476
 
477
+ {/* Toggle Right Panel Button - Desktop only */}
478
  <Button
479
  variant="outline"
480
  size="icon"
 
487
  {rightPanelVisible ? <ChevronRight className="h-3 w-3" /> : <ChevronLeft className="h-3 w-3" />}
488
  </Button>
489
 
490
+ {/* Floating Action Buttons - Desktop only, when panel is closed */}
491
  {!rightPanelVisible && (
492
  <FloatingActionButtons
493
  user={user}