SarahXia0405 commited on
Commit
20499bc
·
verified ·
1 Parent(s): 0ddb203

Update web/src/components/LeftSidebar.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/LeftSidebar.tsx +97 -45
web/src/components/LeftSidebar.tsx CHANGED
@@ -3,16 +3,40 @@ import React, { useEffect, useRef, useState } from "react";
3
  import { LearningModeSelector } from "./LearningModeSelector";
4
  import { Label } from "./ui/label";
5
  import { Button } from "./ui/button";
6
- import { LogIn, Bookmark, Download, Copy, MessageSquare, Trash2, Edit2, Check, X as XIcon } from "lucide-react";
 
 
 
 
 
 
 
 
 
 
7
  import { Separator } from "./ui/separator";
8
  import { GroupMembers } from "./GroupMembers";
9
  import { Card } from "./ui/card";
10
  import { Input } from "./ui/input";
11
- import type { LearningMode, Language, SpaceType, GroupMember, User as UserType, SavedItem, SavedChat } from "../App";
 
 
 
 
 
 
 
 
12
  import { toast } from "sonner";
13
  import { Document, HeadingLevel, Packer, Paragraph, TextRun } from "docx";
14
  import { jsPDF } from "jspdf";
15
- import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "./ui/dialog";
 
 
 
 
 
 
16
  import type { CourseInfo } from "../App";
17
 
18
  // ================================
@@ -71,7 +95,11 @@ function SavedChatItem({
71
 
72
  const handleInputBlur = (e: React.FocusEvent<HTMLInputElement>) => {
73
  const relatedTarget = e.relatedTarget as HTMLElement;
74
- if (relatedTarget && (cancelButtonRef.current?.contains(relatedTarget) || saveButtonRef.current?.contains(relatedTarget))) {
 
 
 
 
75
  return;
76
  }
77
  if (editTitle.trim() && editTitle !== originalTitle && onRenameSavedChat) {
@@ -97,7 +125,10 @@ function SavedChatItem({
97
  };
98
 
99
  return (
100
- <Card className="p-3 cursor-pointer hover:bg-muted/50 transition-all bg-muted/30" onClick={() => !isEditing && onLoadChat(chat)}>
 
 
 
101
  <div className="flex items-start gap-2">
102
  <MessageSquare className="h-3.5 w-3.5 mt-0.5 flex-shrink-0 text-muted-foreground" />
103
  <div className="flex-1 min-w-0">
@@ -193,7 +224,12 @@ function SavedChatItem({
193
  </div>
194
 
195
  <p className="text-xs text-muted-foreground mt-1">
196
- {chat.chatMode === "ask" ? "Ask" : chat.chatMode === "review" ? "Review" : "Quiz"} • {chat.timestamp.toLocaleDateString()}
 
 
 
 
 
197
  </p>
198
  <p className="text-xs text-muted-foreground/70 mt-1">
199
  {chat.messages.length} message{chat.messages.length !== 1 ? "s" : ""}
@@ -275,29 +311,8 @@ export function LeftSidebar({
275
  const [isDownloading, setIsDownloading] = useState(false);
276
  const [copied, setCopied] = useState(false);
277
 
278
- // ✅ NEW: ref for Saved scroll area
279
  const savedScrollRef = useRef<HTMLDivElement>(null);
280
 
281
- // ✅ NEW: hard-stop scroll chaining from Saved area to outer containers
282
- useEffect(() => {
283
- const el = savedScrollRef.current;
284
- if (!el) return;
285
-
286
- const onWheel = (e: WheelEvent) => {
287
- e.stopPropagation();
288
-
289
- const atTop = el.scrollTop <= 0;
290
- const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 1;
291
-
292
- if ((atTop && e.deltaY < 0) || (atBottom && e.deltaY > 0)) {
293
- e.preventDefault();
294
- }
295
- };
296
-
297
- el.addEventListener("wheel", onWheel, { passive: false });
298
- return () => el.removeEventListener("wheel", onWheel);
299
- }, []);
300
-
301
  const handleLogin = () => {
302
  if (!name.trim() || !email.trim()) {
303
  toast.error("Please fill in all fields");
@@ -335,7 +350,8 @@ export function LeftSidebar({
335
  };
336
 
337
  const getDefaultFilenameBase = (item: SavedItem) => {
338
- const kind = item.type === "export" ? "export" : item.type === "summary" ? "summary" : "quiz";
 
339
  return `clare-${kind}-${formatDateStamp(item.timestamp)}`;
340
  };
341
 
@@ -347,6 +363,7 @@ export function LeftSidebar({
347
  downloadBlob(blob, `${getDefaultFilenameBase(item)}.md`);
348
  toast.success("Downloaded .md");
349
  } catch (e) {
 
350
  console.error(e);
351
  toast.error("Failed to download .md");
352
  } finally {
@@ -362,9 +379,21 @@ export function LeftSidebar({
362
  const paragraphs: Paragraph[] = lines.map((line) => {
363
  const trimmed = line.trim();
364
  if (!trimmed) return new Paragraph({ text: "" });
365
- if (trimmed.startsWith("### ")) return new Paragraph({ text: trimmed.replace(/^###\s+/, ""), heading: HeadingLevel.HEADING_3 });
366
- if (trimmed.startsWith("## ")) return new Paragraph({ text: trimmed.replace(/^##\s+/, ""), heading: HeadingLevel.HEADING_2 });
367
- if (trimmed.startsWith("# ")) return new Paragraph({ text: trimmed.replace(/^#\s+/, ""), heading: HeadingLevel.HEADING_1 });
 
 
 
 
 
 
 
 
 
 
 
 
368
  return new Paragraph({ children: [new TextRun({ text: line })] });
369
  });
370
 
@@ -373,6 +402,7 @@ export function LeftSidebar({
373
  downloadBlob(blob, `${getDefaultFilenameBase(item)}.docx`);
374
  toast.success("Downloaded .docx");
375
  } catch (e) {
 
376
  console.error(e);
377
  toast.error("Failed to download .docx");
378
  } finally {
@@ -405,6 +435,7 @@ export function LeftSidebar({
405
  doc.save(`${getDefaultFilenameBase(item)}.pdf`);
406
  toast.success("Downloaded .pdf");
407
  } catch (e) {
 
408
  console.error(e);
409
  toast.error("Failed to download .pdf");
410
  } finally {
@@ -432,7 +463,9 @@ export function LeftSidebar({
432
 
433
  useEffect(() => {
434
  if (selectedItem && isDialogOpen) {
435
- const updatedItem = savedItems.find((item) => item.content === selectedItem.content && item.type === selectedItem.type);
 
 
436
  if (updatedItem && updatedItem.id !== selectedItem.id) setSelectedItem(updatedItem);
437
  }
438
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -444,7 +477,8 @@ export function LeftSidebar({
444
  if (isItemSaved) {
445
  const itemToUnsave = savedItems.find(
446
  (item) =>
447
- (selectedItem.id && item.id === selectedItem.id) || (item.content === selectedItem.content && item.type === selectedItem.type)
 
448
  );
449
  if (itemToUnsave) handleUnsaveItem(itemToUnsave.id);
450
  } else {
@@ -452,8 +486,6 @@ export function LeftSidebar({
452
  }
453
  };
454
 
455
- const filteredSavedItems = savedItems.filter((item) => item.workspaceId === currentWorkspaceId);
456
-
457
  const defaultCourses = [
458
  { id: "course1", name: "Introduction to AI" },
459
  { id: "course2", name: "Machine Learning" },
@@ -521,7 +553,7 @@ export function LeftSidebar({
521
  const courseDisplayInfo = getCourseDisplayInfo();
522
 
523
  return (
524
- // LeftSidebar itself never scrolls. Only the Saved list scrolls.
525
  <div className="h-full min-h-0 flex flex-col overflow-hidden">
526
  {/* Top fixed blocks */}
527
  {isLoggedIn && courseDisplayInfo && (
@@ -532,13 +564,19 @@ export function LeftSidebar({
532
  <div className="space-y-2 text-sm">
533
  <div>
534
  <span className="text-muted-foreground">Instructor: </span>
535
- <a href={`mailto:${courseDisplayInfo.instructor.email}`} className="text-primary hover:underline">
 
 
 
536
  {courseDisplayInfo.instructor.name}
537
  </a>
538
  </div>
539
  <div>
540
  <span className="text-muted-foreground">TA: </span>
541
- <a href={`mailto:${courseDisplayInfo.teachingAssistant.email}`} className="text-primary hover:underline">
 
 
 
542
  {courseDisplayInfo.teachingAssistant.name}
543
  </a>
544
  </div>
@@ -559,7 +597,10 @@ export function LeftSidebar({
559
  className="text-base font-semibold"
560
  />
561
  ) : (
562
- <h3 className="text-base font-semibold cursor-pointer hover:text-primary" onClick={() => setIsEditingTitle(true)}>
 
 
 
563
  {editableTitle}
564
  </h3>
565
  )}
@@ -593,7 +634,9 @@ export function LeftSidebar({
593
  className="w-20 h-20 rounded-full object-cover mb-4"
594
  />
595
  <h3 className="mb-2">Welcome to Clare!</h3>
596
- <p className="text-sm text-muted-foreground text-center mb-4">Log in to start your personalized learning journey</p>
 
 
597
  </div>
598
 
599
  {!showLoginForm ? (
@@ -605,7 +648,12 @@ export function LeftSidebar({
605
  <div className="space-y-3">
606
  <div className="space-y-2">
607
  <Label htmlFor="name">Name</Label>
608
- <Input id="name" value={name} onChange={(e) => setName(e.target.value)} placeholder="Enter your name" />
 
 
 
 
 
609
  </div>
610
  <div className="space-y-2">
611
  <Label htmlFor="email">Email / Student ID</Label>
@@ -638,7 +686,7 @@ export function LeftSidebar({
638
  </div>
639
  )}
640
 
641
- {/* Saved Chat: ONLY this list scrolls */}
642
  {isLoggedIn && (
643
  <div className="flex-1 min-h-0 flex flex-col overflow-hidden">
644
  <div className="p-4 border-b border-border flex-shrink-0">
@@ -673,14 +721,18 @@ export function LeftSidebar({
673
  </div>
674
  )}
675
 
676
- {/* Saved Item Dialog (keep your logic) */}
677
  <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
678
  <DialogContent className="max-w-4xl max-h-[85vh] flex flex-col">
679
  <DialogHeader>
680
  <DialogTitle>{selectedItem?.title}</DialogTitle>
681
  <DialogDescription>
682
- {selectedItem?.type === "export" ? "Export" : selectedItem?.type === "quiz" ? "Quiz" : "Summary"} •{" "}
683
- {selectedItem?.timestamp.toLocaleDateString()}
 
 
 
 
684
  </DialogDescription>
685
  </DialogHeader>
686
 
 
3
  import { LearningModeSelector } from "./LearningModeSelector";
4
  import { Label } from "./ui/label";
5
  import { Button } from "./ui/button";
6
+ import {
7
+ LogIn,
8
+ Bookmark,
9
+ Download,
10
+ Copy,
11
+ MessageSquare,
12
+ Trash2,
13
+ Edit2,
14
+ Check,
15
+ X as XIcon,
16
+ } from "lucide-react";
17
  import { Separator } from "./ui/separator";
18
  import { GroupMembers } from "./GroupMembers";
19
  import { Card } from "./ui/card";
20
  import { Input } from "./ui/input";
21
+ import type {
22
+ LearningMode,
23
+ Language,
24
+ SpaceType,
25
+ GroupMember,
26
+ User as UserType,
27
+ SavedItem,
28
+ SavedChat,
29
+ } from "../App";
30
  import { toast } from "sonner";
31
  import { Document, HeadingLevel, Packer, Paragraph, TextRun } from "docx";
32
  import { jsPDF } from "jspdf";
33
+ import {
34
+ Dialog,
35
+ DialogContent,
36
+ DialogDescription,
37
+ DialogHeader,
38
+ DialogTitle,
39
+ } from "./ui/dialog";
40
  import type { CourseInfo } from "../App";
41
 
42
  // ================================
 
95
 
96
  const handleInputBlur = (e: React.FocusEvent<HTMLInputElement>) => {
97
  const relatedTarget = e.relatedTarget as HTMLElement;
98
+ if (
99
+ relatedTarget &&
100
+ (cancelButtonRef.current?.contains(relatedTarget) ||
101
+ saveButtonRef.current?.contains(relatedTarget))
102
+ ) {
103
  return;
104
  }
105
  if (editTitle.trim() && editTitle !== originalTitle && onRenameSavedChat) {
 
125
  };
126
 
127
  return (
128
+ <Card
129
+ className="p-3 cursor-pointer hover:bg-muted/50 transition-all bg-muted/30"
130
+ onClick={() => !isEditing && onLoadChat(chat)}
131
+ >
132
  <div className="flex items-start gap-2">
133
  <MessageSquare className="h-3.5 w-3.5 mt-0.5 flex-shrink-0 text-muted-foreground" />
134
  <div className="flex-1 min-w-0">
 
224
  </div>
225
 
226
  <p className="text-xs text-muted-foreground mt-1">
227
+ {chat.chatMode === "ask"
228
+ ? "Ask"
229
+ : chat.chatMode === "review"
230
+ ? "Review"
231
+ : "Quiz"}{" "}
232
+ • {chat.timestamp.toLocaleDateString()}
233
  </p>
234
  <p className="text-xs text-muted-foreground/70 mt-1">
235
  {chat.messages.length} message{chat.messages.length !== 1 ? "s" : ""}
 
311
  const [isDownloading, setIsDownloading] = useState(false);
312
  const [copied, setCopied] = useState(false);
313
 
 
314
  const savedScrollRef = useRef<HTMLDivElement>(null);
315
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  const handleLogin = () => {
317
  if (!name.trim() || !email.trim()) {
318
  toast.error("Please fill in all fields");
 
350
  };
351
 
352
  const getDefaultFilenameBase = (item: SavedItem) => {
353
+ const kind =
354
+ item.type === "export" ? "export" : item.type === "summary" ? "summary" : "quiz";
355
  return `clare-${kind}-${formatDateStamp(item.timestamp)}`;
356
  };
357
 
 
363
  downloadBlob(blob, `${getDefaultFilenameBase(item)}.md`);
364
  toast.success("Downloaded .md");
365
  } catch (e) {
366
+ // eslint-disable-next-line no-console
367
  console.error(e);
368
  toast.error("Failed to download .md");
369
  } finally {
 
379
  const paragraphs: Paragraph[] = lines.map((line) => {
380
  const trimmed = line.trim();
381
  if (!trimmed) return new Paragraph({ text: "" });
382
+ if (trimmed.startsWith("### "))
383
+ return new Paragraph({
384
+ text: trimmed.replace(/^###\s+/, ""),
385
+ heading: HeadingLevel.HEADING_3,
386
+ });
387
+ if (trimmed.startsWith("## "))
388
+ return new Paragraph({
389
+ text: trimmed.replace(/^##\s+/, ""),
390
+ heading: HeadingLevel.HEADING_2,
391
+ });
392
+ if (trimmed.startsWith("# "))
393
+ return new Paragraph({
394
+ text: trimmed.replace(/^#\s+/, ""),
395
+ heading: HeadingLevel.HEADING_1,
396
+ });
397
  return new Paragraph({ children: [new TextRun({ text: line })] });
398
  });
399
 
 
402
  downloadBlob(blob, `${getDefaultFilenameBase(item)}.docx`);
403
  toast.success("Downloaded .docx");
404
  } catch (e) {
405
+ // eslint-disable-next-line no-console
406
  console.error(e);
407
  toast.error("Failed to download .docx");
408
  } finally {
 
435
  doc.save(`${getDefaultFilenameBase(item)}.pdf`);
436
  toast.success("Downloaded .pdf");
437
  } catch (e) {
438
+ // eslint-disable-next-line no-console
439
  console.error(e);
440
  toast.error("Failed to download .pdf");
441
  } finally {
 
463
 
464
  useEffect(() => {
465
  if (selectedItem && isDialogOpen) {
466
+ const updatedItem = savedItems.find(
467
+ (item) => item.content === selectedItem.content && item.type === selectedItem.type
468
+ );
469
  if (updatedItem && updatedItem.id !== selectedItem.id) setSelectedItem(updatedItem);
470
  }
471
  // eslint-disable-next-line react-hooks/exhaustive-deps
 
477
  if (isItemSaved) {
478
  const itemToUnsave = savedItems.find(
479
  (item) =>
480
+ (selectedItem.id && item.id === selectedItem.id) ||
481
+ (item.content === selectedItem.content && item.type === selectedItem.type)
482
  );
483
  if (itemToUnsave) handleUnsaveItem(itemToUnsave.id);
484
  } else {
 
486
  }
487
  };
488
 
 
 
489
  const defaultCourses = [
490
  { id: "course1", name: "Introduction to AI" },
491
  { id: "course2", name: "Machine Learning" },
 
553
  const courseDisplayInfo = getCourseDisplayInfo();
554
 
555
  return (
556
+ // LeftSidebar itself never scrolls; only Saved list scrolls.
557
  <div className="h-full min-h-0 flex flex-col overflow-hidden">
558
  {/* Top fixed blocks */}
559
  {isLoggedIn && courseDisplayInfo && (
 
564
  <div className="space-y-2 text-sm">
565
  <div>
566
  <span className="text-muted-foreground">Instructor: </span>
567
+ <a
568
+ href={`mailto:${courseDisplayInfo.instructor.email}`}
569
+ className="text-primary hover:underline"
570
+ >
571
  {courseDisplayInfo.instructor.name}
572
  </a>
573
  </div>
574
  <div>
575
  <span className="text-muted-foreground">TA: </span>
576
+ <a
577
+ href={`mailto:${courseDisplayInfo.teachingAssistant.email}`}
578
+ className="text-primary hover:underline"
579
+ >
580
  {courseDisplayInfo.teachingAssistant.name}
581
  </a>
582
  </div>
 
597
  className="text-base font-semibold"
598
  />
599
  ) : (
600
+ <h3
601
+ className="text-base font-semibold cursor-pointer hover:text-primary"
602
+ onClick={() => setIsEditingTitle(true)}
603
+ >
604
  {editableTitle}
605
  </h3>
606
  )}
 
634
  className="w-20 h-20 rounded-full object-cover mb-4"
635
  />
636
  <h3 className="mb-2">Welcome to Clare!</h3>
637
+ <p className="text-sm text-muted-foreground text-center mb-4">
638
+ Log in to start your personalized learning journey
639
+ </p>
640
  </div>
641
 
642
  {!showLoginForm ? (
 
648
  <div className="space-y-3">
649
  <div className="space-y-2">
650
  <Label htmlFor="name">Name</Label>
651
+ <Input
652
+ id="name"
653
+ value={name}
654
+ onChange={(e) => setName(e.target.value)}
655
+ placeholder="Enter your name"
656
+ />
657
  </div>
658
  <div className="space-y-2">
659
  <Label htmlFor="email">Email / Student ID</Label>
 
686
  </div>
687
  )}
688
 
689
+ {/* Saved Chat: ONLY this list scrolls */}
690
  {isLoggedIn && (
691
  <div className="flex-1 min-h-0 flex flex-col overflow-hidden">
692
  <div className="p-4 border-b border-border flex-shrink-0">
 
721
  </div>
722
  )}
723
 
724
+ {/* Saved Item Dialog */}
725
  <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
726
  <DialogContent className="max-w-4xl max-h-[85vh] flex flex-col">
727
  <DialogHeader>
728
  <DialogTitle>{selectedItem?.title}</DialogTitle>
729
  <DialogDescription>
730
+ {selectedItem?.type === "export"
731
+ ? "Export"
732
+ : selectedItem?.type === "quiz"
733
+ ? "Quiz"
734
+ : "Summary"}{" "}
735
+ • {selectedItem?.timestamp.toLocaleDateString()}
736
  </DialogDescription>
737
  </DialogHeader>
738