SarahXia0405 commited on
Commit
04abc16
·
verified ·
1 Parent(s): 3860a66

Update web/src/components/ChatArea.tsx

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