SarahXia0405 commited on
Commit
0226509
·
verified ·
1 Parent(s): 496a450

Update web/src/App.tsx

Browse files
Files changed (1) hide show
  1. web/src/App.tsx +43 -65
web/src/App.tsx CHANGED
@@ -126,7 +126,6 @@ function App() {
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' },
@@ -140,18 +139,22 @@ function App() {
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]);
@@ -159,11 +162,8 @@ function App() {
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 }
@@ -176,7 +176,7 @@ function App() {
176
  timestamp: new Date(),
177
  sender,
178
  };
179
- setMessages(prev => [...prev, userMessage]);
180
 
181
  if (!shouldAIRespond) return;
182
 
@@ -190,7 +190,7 @@ function App() {
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,45 +199,45 @@ function App() {
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;
@@ -245,20 +245,15 @@ function App() {
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
 
@@ -269,11 +264,12 @@ function App() {
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.');
@@ -281,7 +277,7 @@ function App() {
281
  }
282
 
283
  for (const idx of pendingIdx) {
284
- // 顺序上传便于定位问题
285
  // eslint-disable-next-line no-await-in-loop
286
  await handleUploadSingle(idx);
287
  }
@@ -299,8 +295,6 @@ function App() {
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', {
@@ -330,7 +324,6 @@ function App() {
330
  }
331
  };
332
 
333
- // Quiz 你后端未必有接口,这里先保留 mock
334
  const handleQuiz = () => {
335
  const quiz = `# Micro-Quiz: Responsible AI
336
 
@@ -345,7 +338,6 @@ D) Cost reduction
345
  toast.success('Quiz generated!');
346
  };
347
 
348
- // Memoryline:轻量拉一下(失败不影响)
349
  useEffect(() => {
350
  const run = async () => {
351
  try {
@@ -353,9 +345,7 @@ D) Cost reduction
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
  }
@@ -375,15 +365,10 @@ D) Cost reduction
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
@@ -401,6 +386,7 @@ D) Cost reduction
401
  <X className="h-5 w-5" />
402
  </Button>
403
  </div>
 
404
  <LeftSidebar
405
  learningMode={learningMode}
406
  language={language}
@@ -412,7 +398,6 @@ D) Cost reduction
412
  />
413
  </aside>
414
 
415
- {/* Main Chat Area */}
416
  <main className="flex-1 flex flex-col min-w-0">
417
  <ChatArea
418
  messages={messages}
@@ -424,7 +409,7 @@ D) Cost reduction
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,15 +417,10 @@ D) Cost reduction
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={`
@@ -474,7 +454,6 @@ D) Cost reduction
474
  </aside>
475
  )}
476
 
477
- {/* Toggle Right Panel Button - Desktop only */}
478
  <Button
479
  variant="outline"
480
  size="icon"
@@ -487,7 +466,6 @@ D) Cost reduction
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}
 
126
  const [exportResult, setExportResult] = useState('');
127
  const [resultType, setResultType] = useState<'export' | 'quiz' | 'summary' | null>(null);
128
 
 
129
  const [groupMembers] = useState<GroupMember[]>([
130
  { id: 'clare', name: 'Clare AI', email: 'clare@ai.assistant', isAI: true },
131
  { id: '1', name: 'Sarah Johnson', email: 'sarah.j@university.edu' },
 
139
  }, [isDarkMode]);
140
 
141
  const userId = useMemo(() => {
142
+ // 登录时:email 作为 user_id;不登录也可跑通:0405
 
143
  return (user?.email || '0405').trim();
144
  }, [user]);
145
 
146
+ const isLoggedIn = useMemo(() => {
147
+ // 你如果希望“必须登录才能聊天/上传”,这里改回 !!user
148
+ // 目前为了便于调试后端,允许未登录也走通(user_id=0405)
149
+ return true;
150
+ }, []);
151
+
152
  const currentDocTypeForChat = useMemo(() => {
153
+ const hasSyllabus = uploadedFiles.some((f) => f.type === 'syllabus' && f.uploaded);
 
154
  if (hasSyllabus) return 'Syllabus';
155
+ const hasSlides = uploadedFiles.some((f) => f.type === 'lecture-slides' && f.uploaded);
156
  if (hasSlides) return 'Lecture Slides';
157
+ const hasLit = uploadedFiles.some((f) => f.type === 'literature-review' && f.uploaded);
158
  if (hasLit) return 'Literature Review / Paper';
159
  return 'Other';
160
  }, [uploadedFiles]);
 
162
  const handleSendMessage = async (content: string) => {
163
  if (!content.trim()) return;
164
 
165
+ const shouldAIRespond = spaceType === 'individual' || content.toLowerCase().includes('@clare');
 
 
166
 
 
167
  const sender: GroupMember | undefined =
168
  spaceType === 'group' && user
169
  ? { id: user.email, name: user.name, email: user.email }
 
176
  timestamp: new Date(),
177
  sender,
178
  };
179
+ setMessages((prev) => [...prev, userMessage]);
180
 
181
  if (!shouldAIRespond) return;
182
 
 
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
+ setMessages((prev) => [
210
+ ...prev,
211
+ {
212
+ id: (Date.now() + 1).toString(),
213
+ role: 'assistant',
214
+ content: `Sorry — chat request failed. ${e?.message || ''}`,
215
+ timestamp: new Date(),
216
+ },
217
+ ]);
218
  }
219
  };
220
 
221
+ // ✅ 选文件只入库,不上传
222
  const handleFileUpload = (files: File[]) => {
223
+ const newFiles: UploadedFile[] = files.map((file) => ({
224
  file,
225
  type: 'other',
226
  uploaded: false,
227
  }));
228
+ setUploadedFiles((prev) => [...prev, ...newFiles]);
229
  toast.message('Files added. Select a File Type, then click Upload.');
230
  };
231
 
232
  const handleRemoveFile = (index: number) => {
233
+ setUploadedFiles((prev) => prev.filter((_, i) => i !== index));
234
  };
235
 
236
  const handleFileTypeChange = (index: number, type: FileType) => {
237
+ setUploadedFiles((prev) => prev.map((f, i) => (i === index ? { ...f, type } : f)));
 
 
238
  };
239
 
240
+ // ✅ 上传单个文件(关键读取“最新的 type”,写入 doc_type)
241
  const handleUploadSingle = async (index: number) => {
242
  const target = uploadedFiles[index];
243
  if (!target) return;
 
245
  try {
246
  const form = new FormData();
247
  form.append('user_id', userId);
248
+ form.append('doc_type', mapFileTypeToDocType(target.type));
249
  form.append('file', target.file);
250
 
251
  const r = await apiPostForm<UploadApiResp>('/api/upload', form);
252
+ if (!r.ok) throw new Error('Upload response ok=false');
253
 
254
+ setUploadedFiles((prev) =>
 
 
 
 
255
  prev.map((x, i) =>
256
+ i === index ? { ...x, uploaded: true, uploadedChunks: r.added_chunks } : x
 
 
257
  )
258
  );
259
 
 
264
  }
265
  };
266
 
267
+ // ✅ 上传所有未上传
268
  const handleUploadAllPending = async () => {
269
  const pendingIdx = uploadedFiles
270
  .map((f, i) => ({ f, i }))
271
+ .filter((x) => !x.f.uploaded)
272
+ .map((x) => x.i);
273
 
274
  if (!pendingIdx.length) {
275
  toast.message('No pending files to upload.');
 
277
  }
278
 
279
  for (const idx of pendingIdx) {
280
+ // 顺序上传便于 debug
281
  // eslint-disable-next-line no-await-in-loop
282
  await handleUploadSingle(idx);
283
  }
 
295
  ]);
296
  };
297
 
 
 
298
  const handleExport = async () => {
299
  try {
300
  const r = await apiPostJson<{ markdown: string }>('/api/export', {
 
324
  }
325
  };
326
 
 
327
  const handleQuiz = () => {
328
  const quiz = `# Micro-Quiz: Responsible AI
329
 
 
338
  toast.success('Quiz generated!');
339
  };
340
 
 
341
  useEffect(() => {
342
  const run = async () => {
343
  try {
 
345
  if (!res.ok) return;
346
  const j = await res.json();
347
  const pct = typeof j?.progress_pct === 'number' ? j.progress_pct : null;
348
+ if (pct !== null) setMemoryProgress(Math.round(pct * 100));
 
 
349
  } catch {
350
  // ignore
351
  }
 
365
  />
366
 
367
  <div className="flex-1 flex overflow-hidden">
 
368
  {leftSidebarOpen && (
369
+ <div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setLeftSidebarOpen(false)} />
 
 
 
370
  )}
371
 
 
372
  <aside
373
  className={`
374
  fixed lg:static inset-y-0 left-0 z-50
 
386
  <X className="h-5 w-5" />
387
  </Button>
388
  </div>
389
+
390
  <LeftSidebar
391
  learningMode={learningMode}
392
  language={language}
 
398
  />
399
  </aside>
400
 
 
401
  <main className="flex-1 flex flex-col min-w-0">
402
  <ChatArea
403
  messages={messages}
 
409
  onUploadSingle={handleUploadSingle}
410
  onUploadAllPending={handleUploadAllPending}
411
  memoryProgress={memoryProgress}
412
+ isLoggedIn={isLoggedIn}
413
  learningMode={learningMode}
414
  onClearConversation={handleClearConversation}
415
  onLearningModeChange={setLearningMode}
 
417
  />
418
  </main>
419
 
 
420
  {rightPanelOpen && (
421
+ <div className="fixed inset-0 bg-black/50 z-40 lg:hidden" onClick={() => setRightPanelOpen(false)} />
 
 
 
422
  )}
423
 
 
424
  {rightPanelVisible && (
425
  <aside
426
  className={`
 
454
  </aside>
455
  )}
456
 
 
457
  <Button
458
  variant="outline"
459
  size="icon"
 
466
  {rightPanelVisible ? <ChevronRight className="h-3 w-3" /> : <ChevronLeft className="h-3 w-3" />}
467
  </Button>
468
 
 
469
  {!rightPanelVisible && (
470
  <FloatingActionButtons
471
  user={user}