SarahXia0405 commited on
Commit
8e91a82
·
verified ·
1 Parent(s): 3f3e53a

Update web/src/components/ChatArea.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/ChatArea.tsx +73 -51
web/src/components/ChatArea.tsx CHANGED
@@ -12,7 +12,7 @@ import {
12
  Share2,
13
  Upload,
14
  X,
15
- Trash2,
16
  File,
17
  FileText,
18
  Presentation,
@@ -66,6 +66,7 @@ interface ChatAreaProps {
66
  onFileUpload: (files: File[]) => void;
67
  onRemoveFile: (index: number) => void;
68
 
 
69
  onFileTypeChange: (index: number, type: FileType) => void;
70
  memoryProgress: number;
71
  isLoggedIn: boolean;
@@ -563,7 +564,7 @@ export function ChatArea({
563
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
564
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
565
  };
566
-
567
  const fileKey = (f: File) => `${f.name}::${f.size}::${f.lastModified}`;
568
 
569
  const removeUploadedByFile = (file: File) => {
@@ -572,13 +573,13 @@ export function ChatArea({
572
  };
573
 
574
 
575
- // useObjectUrlCache: for image thumbnails (uploaded + pending)
576
  const allThumbFiles = React.useMemo(() => {
577
  return [...uploadedFiles.map((u) => u.file), ...pendingFiles.map((p) => p.file)];
578
  }, [uploadedFiles, pendingFiles]);
579
  const { getOrCreate } = useObjectUrlCache(allThumbFiles);
580
 
581
- // NEW: a compact "chip" UI (the one with left X)
582
  const FileChip = ({
583
  file,
584
  index,
@@ -590,7 +591,7 @@ export function ChatArea({
590
  }) => {
591
  const ext = file.name.toLowerCase();
592
  const isImage = [".jpg", ".jpeg", ".png", ".gif", ".webp"].some((e) => ext.endsWith(e));
593
-
594
  const label = ext.endsWith(".pdf")
595
  ? "PDF"
596
  : ext.endsWith(".pptx") || ext.endsWith(".ppt")
@@ -600,59 +601,62 @@ export function ChatArea({
600
  : isImage
601
  ? "Image"
602
  : "File";
603
-
604
  const thumbUrl = isImage ? getOrCreate(file) : null;
605
-
606
  const handleRemove = () => {
607
  if (source === "uploaded") {
608
- removeUploadedByFile(file);
 
 
 
609
  } else {
610
  setPendingFiles((prev) => prev.filter((p) => fileKey(p.file) !== fileKey(file)));
611
  }
612
  };
613
-
614
-
615
-
616
  return (
617
  <div className="flex items-center gap-2 rounded-xl border border-border bg-card px-3 py-2 shadow-sm w-[320px] max-w-full">
618
- {/* 缩略图:右侧小图(上传图片时显示) */}
619
- {isImage ? (
620
- <div className="relative h-10 w-10 flex-shrink-0 rounded-lg overflow-hidden border border-border bg-muted">
621
- {thumbUrl ? (
622
- <img src={thumbUrl} alt={file.name} className="h-full w-full object-cover" draggable={false} />
623
- ) : (
624
- <div className="h-full w-full flex items-center justify-center">
625
- <ImageIcon className="h-4 w-4 text-muted-foreground" />
626
- </div>
627
- )}
628
- </div>
629
- ) : (
630
- <div className="relative h-10 w-10 flex-shrink-0 rounded-lg overflow-hidden border border-border bg-muted flex items-center justify-center">
631
- <File className="h-4 w-4 text-muted-foreground" />
632
- </div>
633
- )}
634
-
635
- {/* 文本区 */}
636
- <div className="min-w-0 flex-1">
637
- <div className="text-sm font-medium truncate" title={file.name}>
638
- {file.name}
639
- </div>
640
- <div className="text-xs text-muted-foreground">{label}</div>
641
- </div>
642
-
643
- {/* ✅ 删除按钮 */}
644
  <button
645
  type="button"
646
  onClick={(e) => {
647
  e.preventDefault();
648
  e.stopPropagation();
649
- handleRemove();
650
  }}
651
- className="inline-flex h-8 w-8 items-center justify-center rounded-md border border-border bg-card hover:bg-muted"
652
  title="Remove"
653
  >
654
  <Trash2 className="h-4 w-4" />
655
  </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
656
  </div>
657
  );
658
  };
@@ -924,17 +928,33 @@ export function ChatArea({
924
  {/* Uploaded Files Preview (chip UI) */}
925
  {(uploadedFiles.length > 0 || pendingFiles.length > 0) && (
926
  <div className="mb-2 flex flex-wrap gap-2 max-h-32 overflow-y-auto">
927
- {/* uploaded: 改为使用 FileChip,并显示缩略图 */}
928
- {uploadedFiles.map((uf, i) => (
929
- <FileChip
930
- key={`u-${uf.file.name}-${uf.file.size}-${uf.file.lastModified}`}
931
- file={uf.file}
932
- index={i}
933
- source="uploaded"
934
- />
935
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
936
 
937
- {/* pending */}
938
  {pendingFiles.map((p, idx) => (
939
  <FileChip key={`p-${p.file.name}-${p.file.size}-${p.file.lastModified}`} file={p.file} index={idx} source="pending" />
940
  ))}
@@ -1046,7 +1066,9 @@ export function ChatArea({
1046
  : "Ask Clare anything about the course or drag files here..."
1047
  }
1048
  disabled={!isLoggedIn || (chatMode === "quiz" && !quizState.waitingForAnswer)}
1049
- className={`min-h-[80px] pl-4 pr-20 resize-none bg-background border-2 ${isDragging ? "border-primary border-dashed" : "border-border"}`}
 
 
1050
  />
1051
 
1052
  <div className="absolute bottom-2 right-2 flex gap-1">
@@ -1304,4 +1326,4 @@ export function ChatArea({
1304
  )}
1305
  </div>
1306
  );
1307
- }
 
12
  Share2,
13
  Upload,
14
  X,
15
+ Trash2,
16
  File,
17
  FileText,
18
  Presentation,
 
66
  onFileUpload: (files: File[]) => void;
67
  onRemoveFile: (index: number) => void;
68
 
69
+
70
  onFileTypeChange: (index: number, type: FileType) => void;
71
  memoryProgress: number;
72
  isLoggedIn: boolean;
 
564
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
565
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
566
  };
567
+
568
  const fileKey = (f: File) => `${f.name}::${f.size}::${f.lastModified}`;
569
 
570
  const removeUploadedByFile = (file: File) => {
 
573
  };
574
 
575
 
576
+ // useObjectUrlCache: for image thumbnails (uploaded + pending)
577
  const allThumbFiles = React.useMemo(() => {
578
  return [...uploadedFiles.map((u) => u.file), ...pendingFiles.map((p) => p.file)];
579
  }, [uploadedFiles, pendingFiles]);
580
  const { getOrCreate } = useObjectUrlCache(allThumbFiles);
581
 
582
+ // NEW: a compact "chip" UI (the one with left X)
583
  const FileChip = ({
584
  file,
585
  index,
 
591
  }) => {
592
  const ext = file.name.toLowerCase();
593
  const isImage = [".jpg", ".jpeg", ".png", ".gif", ".webp"].some((e) => ext.endsWith(e));
594
+
595
  const label = ext.endsWith(".pdf")
596
  ? "PDF"
597
  : ext.endsWith(".pptx") || ext.endsWith(".ppt")
 
601
  : isImage
602
  ? "Image"
603
  : "File";
604
+
605
  const thumbUrl = isImage ? getOrCreate(file) : null;
606
+
607
  const handleRemove = () => {
608
  if (source === "uploaded") {
609
+ // 关键:仍然走 index 删除(或按 file 找 index)
610
+ onRemoveFile(index);
611
+ // 如果你更想稳一点(不依赖 index),用下面这一行替代上面那行:
612
+ // removeUploadedByFile(file);
613
  } else {
614
  setPendingFiles((prev) => prev.filter((p) => fileKey(p.file) !== fileKey(file)));
615
  }
616
  };
617
+
 
 
618
  return (
619
  <div className="flex items-center gap-2 rounded-xl border border-border bg-card px-3 py-2 shadow-sm w-[320px] max-w-full">
620
+ {/* 叉叉:只 stopPropagation,不要一堆 stopImmediatePropagation,避免某些浏览器/事件链出问题 */}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
621
  <button
622
  type="button"
623
  onClick={(e) => {
624
  e.preventDefault();
625
  e.stopPropagation();
626
+ onRemoveFile?.(index); // ✅ 用 index 删(前后端状态都会删掉)
627
  }}
628
+ className="ml-2 inline-flex h-7 w-7 items-center justify-center rounded-md border border-border bg-card hover:bg-muted"
629
  title="Remove"
630
  >
631
  <Trash2 className="h-4 w-4" />
632
  </button>
633
+
634
+
635
+ {/* 文本区 */}
636
+ <div className="min-w-0 flex-1">
637
+ <div className="text-sm font-medium truncate" title={file.name}>
638
+ {file.name}
639
+ </div>
640
+ <div className="text-xs text-muted-foreground">{label}</div>
641
+ </div>
642
+
643
+ {/* ✅ 缩略图:更小更干净 */}
644
+ {isImage ? (
645
+ <div className="relative h-10 w-10 flex-shrink-0 rounded-lg overflow-hidden border border-border bg-muted">
646
+ {thumbUrl ? (
647
+ <img
648
+ src={thumbUrl}
649
+ alt={file.name}
650
+ className="h-full w-full object-cover"
651
+ draggable={false}
652
+ />
653
+ ) : (
654
+ <div className="h-full w-full flex items-center justify-center">
655
+ <ImageIcon className="h-4 w-4 text-muted-foreground" />
656
+ </div>
657
+ )}
658
+ </div>
659
+ ) : null}
660
  </div>
661
  );
662
  };
 
928
  {/* Uploaded Files Preview (chip UI) */}
929
  {(uploadedFiles.length > 0 || pendingFiles.length > 0) && (
930
  <div className="mb-2 flex flex-wrap gap-2 max-h-32 overflow-y-auto">
931
+ {/* uploaded */}
932
+ {uploadedFiles.map((uf, i) => {
933
+ const key = `${uf.file.name}::${uf.file.size}::${uf.file.lastModified}`;
934
+
935
+ return (
936
+ <div key={key} className="flex items-center justify-between gap-2 rounded-md border px-3 py-2">
937
+ <div className="min-w-0">
938
+ <div className="truncate text-sm font-medium">{uf.file.name}</div>
939
+ <div className="text-xs text-muted-foreground">
940
+ {uf.type}
941
+ </div>
942
+ </div>
943
+
944
+ {/* 垃圾桶:直接删 props(UI + state 同步删) */}
945
+ <Button
946
+ variant="ghost"
947
+ size="icon"
948
+ onClick={() => onRemoveFile(i)} // 或者 onRemoveFile(uf) 也行(因为 App 现在兼容了)
949
+ title="Remove"
950
+ >
951
+ <Trash2 className="h-4 w-4" />
952
+ </Button>
953
+ </div>
954
+ );
955
+ })}
956
 
957
+ {/* pending (type dialog 之前也能显示的话) */}
958
  {pendingFiles.map((p, idx) => (
959
  <FileChip key={`p-${p.file.name}-${p.file.size}-${p.file.lastModified}`} file={p.file} index={idx} source="pending" />
960
  ))}
 
1066
  : "Ask Clare anything about the course or drag files here..."
1067
  }
1068
  disabled={!isLoggedIn || (chatMode === "quiz" && !quizState.waitingForAnswer)}
1069
+ className={`min-h-[80px] pl-4 pr-20 resize-none bg-background border-2 ${
1070
+ isDragging ? "border-primary border-dashed" : "border-border"
1071
+ }`}
1072
  />
1073
 
1074
  <div className="absolute bottom-2 right-2 flex gap-1">
 
1326
  )}
1327
  </div>
1328
  );
1329
+ }