Spaces:
Sleeping
Sleeping
Update web/src/components/ChatArea.tsx
Browse files- web/src/components/ChatArea.tsx +31 -102
web/src/components/ChatArea.tsx
CHANGED
|
@@ -33,20 +33,8 @@ import type {
|
|
| 33 |
} from "../App";
|
| 34 |
import { toast } from "sonner";
|
| 35 |
import { jsPDF } from "jspdf";
|
| 36 |
-
import {
|
| 37 |
-
|
| 38 |
-
DropdownMenuContent,
|
| 39 |
-
DropdownMenuItem,
|
| 40 |
-
DropdownMenuTrigger,
|
| 41 |
-
} from "./ui/dropdown-menu";
|
| 42 |
-
import {
|
| 43 |
-
Dialog,
|
| 44 |
-
DialogContent,
|
| 45 |
-
DialogDescription,
|
| 46 |
-
DialogFooter,
|
| 47 |
-
DialogHeader,
|
| 48 |
-
DialogTitle,
|
| 49 |
-
} from "./ui/dialog";
|
| 50 |
import { Checkbox } from "./ui/checkbox";
|
| 51 |
import {
|
| 52 |
AlertDialog,
|
|
@@ -58,17 +46,11 @@ import {
|
|
| 58 |
AlertDialogHeader,
|
| 59 |
AlertDialogTitle,
|
| 60 |
} from "./ui/alert-dialog";
|
| 61 |
-
import {
|
| 62 |
-
Select,
|
| 63 |
-
SelectContent,
|
| 64 |
-
SelectItem,
|
| 65 |
-
SelectTrigger,
|
| 66 |
-
SelectValue,
|
| 67 |
-
} from "./ui/select";
|
| 68 |
import { SmartReview } from "./SmartReview";
|
| 69 |
import clareAvatar from "../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png";
|
| 70 |
|
| 71 |
-
// ✅ NEW:
|
| 72 |
import { useObjectUrlCache } from "../lib/useObjectUrlCache";
|
| 73 |
|
| 74 |
type ReviewEventType = "send_message" | "review_topic" | "review_all";
|
|
@@ -103,12 +85,7 @@ interface ChatAreaProps {
|
|
| 103 |
savedChats: SavedChat[];
|
| 104 |
workspaces: Workspace[];
|
| 105 |
currentWorkspaceId: string;
|
| 106 |
-
onSaveFile?: (
|
| 107 |
-
content: string,
|
| 108 |
-
type: "export" | "summary",
|
| 109 |
-
format?: "pdf" | "text",
|
| 110 |
-
workspaceId?: string
|
| 111 |
-
) => void;
|
| 112 |
leftPanelVisible?: boolean;
|
| 113 |
currentCourseId?: string;
|
| 114 |
onCourseChange?: (courseId: string) => void;
|
|
@@ -181,9 +158,6 @@ export function ChatArea({
|
|
| 181 |
const [shareLink, setShareLink] = useState("");
|
| 182 |
const [targetWorkspaceId, setTargetWorkspaceId] = useState<string>("");
|
| 183 |
|
| 184 |
-
// ✅ NEW: objectURL cache for image thumbnail rendering
|
| 185 |
-
const { getObjectUrl, revokeObjectUrl } = useObjectUrlCache();
|
| 186 |
-
|
| 187 |
const courses =
|
| 188 |
availableCourses.length > 0
|
| 189 |
? availableCourses
|
|
@@ -327,9 +301,7 @@ export function ChatArea({
|
|
| 327 |
|
| 328 |
const buildPreviewContent = () => {
|
| 329 |
if (!messages.length) return "";
|
| 330 |
-
return messages
|
| 331 |
-
.map((msg) => `${msg.role === "user" ? "You" : "Clare"}: ${msg.content}`)
|
| 332 |
-
.join("\n\n");
|
| 333 |
};
|
| 334 |
|
| 335 |
const buildSummaryContent = () => {
|
|
@@ -518,8 +490,8 @@ export function ChatArea({
|
|
| 518 |
|
| 519 |
const validFiles = files.filter((file) => {
|
| 520 |
const ext = file.name.toLowerCase();
|
| 521 |
-
return [".pdf", ".docx", ".pptx", ".jpg", ".jpeg", ".png", ".gif", ".webp", ".doc", ".ppt"].some(
|
| 522 |
-
|
| 523 |
);
|
| 524 |
});
|
| 525 |
|
|
@@ -536,8 +508,8 @@ export function ChatArea({
|
|
| 536 |
if (files.length > 0) {
|
| 537 |
const validFiles = files.filter((file) => {
|
| 538 |
const ext = file.name.toLowerCase();
|
| 539 |
-
return [".pdf", ".docx", ".pptx", ".jpg", ".jpeg", ".png", ".gif", ".webp", ".doc", ".ppt"].some(
|
| 540 |
-
|
| 541 |
);
|
| 542 |
});
|
| 543 |
|
|
@@ -591,7 +563,8 @@ export function ChatArea({
|
|
| 591 |
if (ext.endsWith(".pdf")) return { bgColor: "bg-red-500", type: "PDF" };
|
| 592 |
if (ext.endsWith(".docx") || ext.endsWith(".doc")) return { bgColor: "bg-blue-500", type: "Document" };
|
| 593 |
if (ext.endsWith(".pptx") || ext.endsWith(".ppt")) return { bgColor: "bg-orange-500", type: "Presentation" };
|
| 594 |
-
if ([".jpg", ".jpeg", ".png", ".gif", ".webp"].some((e) => ext.endsWith(e)))
|
|
|
|
| 595 |
return { bgColor: "bg-gray-500", type: "File" };
|
| 596 |
};
|
| 597 |
|
|
@@ -601,11 +574,9 @@ export function ChatArea({
|
|
| 601 |
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
| 602 |
};
|
| 603 |
|
| 604 |
-
// ✅ (display
|
| 605 |
-
const
|
| 606 |
-
|
| 607 |
-
return [".jpg", ".jpeg", ".png", ".gif", ".webp"].some((e) => ext.endsWith(e));
|
| 608 |
-
};
|
| 609 |
|
| 610 |
const FileThumbnail = ({
|
| 611 |
file,
|
|
@@ -622,10 +593,9 @@ export function ChatArea({
|
|
| 622 |
onPreview: () => void;
|
| 623 |
onRemove: (e: React.MouseEvent) => void;
|
| 624 |
}) => {
|
| 625 |
-
// ✅ Use cached objectURL for stable image preview
|
| 626 |
-
const src = isImage ? getObjectUrl(file) : null;
|
| 627 |
-
|
| 628 |
if (isImage) {
|
|
|
|
|
|
|
| 629 |
return (
|
| 630 |
<div
|
| 631 |
className="relative cursor-pointer w-16 h-16 flex-shrink-0"
|
|
@@ -659,7 +629,6 @@ export function ChatArea({
|
|
| 659 |
onRemove(e);
|
| 660 |
}}
|
| 661 |
style={{ zIndex: 100 }}
|
| 662 |
-
aria-label="Remove file"
|
| 663 |
>
|
| 664 |
<X className="h-2.5 w-2.5" style={{ color: "rgb(0, 0, 0)", strokeWidth: 2 }} />
|
| 665 |
</button>
|
|
@@ -690,7 +659,6 @@ export function ChatArea({
|
|
| 690 |
onRemove(e);
|
| 691 |
}}
|
| 692 |
style={{ zIndex: 10 }}
|
| 693 |
-
aria-label="Remove file"
|
| 694 |
>
|
| 695 |
<X className="h-2.5 w-2.5" style={{ color: "rgb(0, 0, 0)", strokeWidth: 2 }} />
|
| 696 |
</button>
|
|
@@ -765,11 +733,7 @@ export function ChatArea({
|
|
| 765 |
);
|
| 766 |
}
|
| 767 |
|
| 768 |
-
return
|
| 769 |
-
<div className="whitespace-pre-wrap text-sm font-mono p-4 bg-muted rounded-lg max-h-[60vh] overflow-y-auto">
|
| 770 |
-
{content}
|
| 771 |
-
</div>
|
| 772 |
-
);
|
| 773 |
};
|
| 774 |
|
| 775 |
// ✅ Reserve space for composer so last message is never hidden
|
|
@@ -802,10 +766,7 @@ export function ChatArea({
|
|
| 802 |
}
|
| 803 |
|
| 804 |
return (
|
| 805 |
-
<Select
|
| 806 |
-
value={currentCourseId || "course1"}
|
| 807 |
-
onValueChange={(val) => onCourseChange && onCourseChange(val)}
|
| 808 |
-
>
|
| 809 |
<SelectTrigger className="w-[200px] h-9 font-semibold">
|
| 810 |
<SelectValue placeholder="Select course" />
|
| 811 |
</SelectTrigger>
|
|
@@ -823,12 +784,7 @@ export function ChatArea({
|
|
| 823 |
|
| 824 |
{/* Chat Mode Tabs - Center */}
|
| 825 |
<div className="absolute left-1/2 -translate-x-1/2 flex-shrink-0">
|
| 826 |
-
<Tabs
|
| 827 |
-
value={chatMode}
|
| 828 |
-
onValueChange={(value) => onChatModeChange(value as ChatMode)}
|
| 829 |
-
className="w-auto"
|
| 830 |
-
orientation="horizontal"
|
| 831 |
-
>
|
| 832 |
<TabsList className="inline-flex h-8 items-center justify-center rounded-xl bg-muted p-1 text-muted-foreground">
|
| 833 |
<TabsTrigger value="ask" className="w-[140px] px-3 text-sm">
|
| 834 |
Ask
|
|
@@ -904,11 +860,7 @@ export function ChatArea({
|
|
| 904 |
{/* =========================
|
| 905 |
1) Scroll Container (ONLY this scrolls)
|
| 906 |
========================= */}
|
| 907 |
-
<div
|
| 908 |
-
ref={scrollContainerRef}
|
| 909 |
-
className="flex-1 min-h-0 overflow-y-auto overscroll-contain"
|
| 910 |
-
style={{ overscrollBehavior: "contain" }}
|
| 911 |
-
>
|
| 912 |
{/* Messages */}
|
| 913 |
<div className="py-6" style={{ paddingBottom: bottomPad }}>
|
| 914 |
<div className="w-full space-y-6 max-w-4xl mx-auto">
|
|
@@ -918,8 +870,7 @@ export function ChatArea({
|
|
| 918 |
message={message}
|
| 919 |
showSenderInfo={spaceType === "group"}
|
| 920 |
isFirstGreeting={
|
| 921 |
-
(message.id === "1" || message.id === "review-1" || message.id === "quiz-1") &&
|
| 922 |
-
message.role === "assistant"
|
| 923 |
}
|
| 924 |
showNextButton={message.showNextButton && !isAppTyping}
|
| 925 |
onNextQuestion={onNextQuestion}
|
|
@@ -957,18 +908,9 @@ export function ChatArea({
|
|
| 957 |
</div>
|
| 958 |
<div className="bg-muted rounded-2xl px-4 py-3">
|
| 959 |
<div className="flex gap-1">
|
| 960 |
-
<div
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
/>
|
| 964 |
-
<div
|
| 965 |
-
className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce"
|
| 966 |
-
style={{ animationDelay: "150ms" }}
|
| 967 |
-
/>
|
| 968 |
-
<div
|
| 969 |
-
className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce"
|
| 970 |
-
style={{ animationDelay: "300ms" }}
|
| 971 |
-
/>
|
| 972 |
</div>
|
| 973 |
</div>
|
| 974 |
</div>
|
|
@@ -1004,7 +946,9 @@ export function ChatArea({
|
|
| 1004 |
{uploadedFiles.map((uploadedFile, index) => {
|
| 1005 |
const Icon = getFileIcon(uploadedFile.file.name);
|
| 1006 |
const fileInfo = getFileTypeInfo(uploadedFile.file.name);
|
| 1007 |
-
const isImage =
|
|
|
|
|
|
|
| 1008 |
|
| 1009 |
return (
|
| 1010 |
<div key={index}>
|
|
@@ -1019,14 +963,6 @@ export function ChatArea({
|
|
| 1019 |
}}
|
| 1020 |
onRemove={(e) => {
|
| 1021 |
e.stopPropagation();
|
| 1022 |
-
// ✅ revoke objectURL for this image file (display-only change)
|
| 1023 |
-
if (isImage) {
|
| 1024 |
-
try {
|
| 1025 |
-
revokeObjectUrl(uploadedFile.file);
|
| 1026 |
-
} catch {
|
| 1027 |
-
// ignore
|
| 1028 |
-
}
|
| 1029 |
-
}
|
| 1030 |
setFileToDelete(index);
|
| 1031 |
setShowDeleteDialog(true);
|
| 1032 |
}}
|
|
@@ -1094,10 +1030,7 @@ export function ChatArea({
|
|
| 1094 |
</div>
|
| 1095 |
</DropdownMenuItem>
|
| 1096 |
|
| 1097 |
-
<DropdownMenuItem
|
| 1098 |
-
onClick={() => onLearningModeChange("exam")}
|
| 1099 |
-
className={learningMode === "exam" ? "bg-accent" : ""}
|
| 1100 |
-
>
|
| 1101 |
<div className="flex flex-col">
|
| 1102 |
<span className="font-medium">Exam Prep</span>
|
| 1103 |
<span className="text-xs text-muted-foreground">Practice with quiz questions</span>
|
|
@@ -1190,9 +1123,7 @@ export function ChatArea({
|
|
| 1190 |
<AlertDialogContent>
|
| 1191 |
<AlertDialogHeader>
|
| 1192 |
<AlertDialogTitle>Start New Conversation</AlertDialogTitle>
|
| 1193 |
-
<AlertDialogDescription>
|
| 1194 |
-
Would you like to save the current chat before starting a new conversation?
|
| 1195 |
-
</AlertDialogDescription>
|
| 1196 |
|
| 1197 |
<Button
|
| 1198 |
variant="ghost"
|
|
@@ -1373,9 +1304,7 @@ export function ChatArea({
|
|
| 1373 |
</DialogTitle>
|
| 1374 |
<DialogDescription>File size: {selectedFile ? formatFileSize(selectedFile.file.size) : ""}</DialogDescription>
|
| 1375 |
</DialogHeader>
|
| 1376 |
-
<div className="flex-1 min-h-0 overflow-y-auto mt-4">
|
| 1377 |
-
{selectedFile && <FileViewerContent file={selectedFile.file} />}
|
| 1378 |
-
</div>
|
| 1379 |
</DialogContent>
|
| 1380 |
</Dialog>
|
| 1381 |
|
|
|
|
| 33 |
} from "../App";
|
| 34 |
import { toast } from "sonner";
|
| 35 |
import { jsPDF } from "jspdf";
|
| 36 |
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu";
|
| 37 |
+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "./ui/dialog";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
import { Checkbox } from "./ui/checkbox";
|
| 39 |
import {
|
| 40 |
AlertDialog,
|
|
|
|
| 46 |
AlertDialogHeader,
|
| 47 |
AlertDialogTitle,
|
| 48 |
} from "./ui/alert-dialog";
|
| 49 |
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
import { SmartReview } from "./SmartReview";
|
| 51 |
import clareAvatar from "../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png";
|
| 52 |
|
| 53 |
+
// ✅ NEW: object URL cache for image thumbnails
|
| 54 |
import { useObjectUrlCache } from "../lib/useObjectUrlCache";
|
| 55 |
|
| 56 |
type ReviewEventType = "send_message" | "review_topic" | "review_all";
|
|
|
|
| 85 |
savedChats: SavedChat[];
|
| 86 |
workspaces: Workspace[];
|
| 87 |
currentWorkspaceId: string;
|
| 88 |
+
onSaveFile?: (content: string, type: "export" | "summary", format?: "pdf" | "text", workspaceId?: string) => void;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
leftPanelVisible?: boolean;
|
| 90 |
currentCourseId?: string;
|
| 91 |
onCourseChange?: (courseId: string) => void;
|
|
|
|
| 158 |
const [shareLink, setShareLink] = useState("");
|
| 159 |
const [targetWorkspaceId, setTargetWorkspaceId] = useState<string>("");
|
| 160 |
|
|
|
|
|
|
|
|
|
|
| 161 |
const courses =
|
| 162 |
availableCourses.length > 0
|
| 163 |
? availableCourses
|
|
|
|
| 301 |
|
| 302 |
const buildPreviewContent = () => {
|
| 303 |
if (!messages.length) return "";
|
| 304 |
+
return messages.map((msg) => `${msg.role === "user" ? "You" : "Clare"}: ${msg.content}`).join("\n\n");
|
|
|
|
|
|
|
| 305 |
};
|
| 306 |
|
| 307 |
const buildSummaryContent = () => {
|
|
|
|
| 490 |
|
| 491 |
const validFiles = files.filter((file) => {
|
| 492 |
const ext = file.name.toLowerCase();
|
| 493 |
+
return [".pdf", ".docx", ".pptx", ".jpg", ".jpeg", ".png", ".gif", ".webp", ".doc", ".ppt"].some((allowed) =>
|
| 494 |
+
ext.endsWith(allowed)
|
| 495 |
);
|
| 496 |
});
|
| 497 |
|
|
|
|
| 508 |
if (files.length > 0) {
|
| 509 |
const validFiles = files.filter((file) => {
|
| 510 |
const ext = file.name.toLowerCase();
|
| 511 |
+
return [".pdf", ".docx", ".pptx", ".jpg", ".jpeg", ".png", ".gif", ".webp", ".doc", ".ppt"].some((allowed) =>
|
| 512 |
+
ext.endsWith(allowed)
|
| 513 |
);
|
| 514 |
});
|
| 515 |
|
|
|
|
| 563 |
if (ext.endsWith(".pdf")) return { bgColor: "bg-red-500", type: "PDF" };
|
| 564 |
if (ext.endsWith(".docx") || ext.endsWith(".doc")) return { bgColor: "bg-blue-500", type: "Document" };
|
| 565 |
if (ext.endsWith(".pptx") || ext.endsWith(".ppt")) return { bgColor: "bg-orange-500", type: "Presentation" };
|
| 566 |
+
if ([".jpg", ".jpeg", ".png", ".gif", ".webp"].some((e) => ext.endsWith(e)))
|
| 567 |
+
return { bgColor: "bg-green-500", type: "Image" };
|
| 568 |
return { bgColor: "bg-gray-500", type: "File" };
|
| 569 |
};
|
| 570 |
|
|
|
|
| 574 |
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
| 575 |
};
|
| 576 |
|
| 577 |
+
// ✅ objectURL cache for image thumbnails (display only)
|
| 578 |
+
const cacheFiles = React.useMemo(() => uploadedFiles.map((u) => u.file), [uploadedFiles]);
|
| 579 |
+
const { getOrCreate } = useObjectUrlCache(cacheFiles);
|
|
|
|
|
|
|
| 580 |
|
| 581 |
const FileThumbnail = ({
|
| 582 |
file,
|
|
|
|
| 593 |
onPreview: () => void;
|
| 594 |
onRemove: (e: React.MouseEvent) => void;
|
| 595 |
}) => {
|
|
|
|
|
|
|
|
|
|
| 596 |
if (isImage) {
|
| 597 |
+
const src = getOrCreate(file);
|
| 598 |
+
|
| 599 |
return (
|
| 600 |
<div
|
| 601 |
className="relative cursor-pointer w-16 h-16 flex-shrink-0"
|
|
|
|
| 629 |
onRemove(e);
|
| 630 |
}}
|
| 631 |
style={{ zIndex: 100 }}
|
|
|
|
| 632 |
>
|
| 633 |
<X className="h-2.5 w-2.5" style={{ color: "rgb(0, 0, 0)", strokeWidth: 2 }} />
|
| 634 |
</button>
|
|
|
|
| 659 |
onRemove(e);
|
| 660 |
}}
|
| 661 |
style={{ zIndex: 10 }}
|
|
|
|
| 662 |
>
|
| 663 |
<X className="h-2.5 w-2.5" style={{ color: "rgb(0, 0, 0)", strokeWidth: 2 }} />
|
| 664 |
</button>
|
|
|
|
| 733 |
);
|
| 734 |
}
|
| 735 |
|
| 736 |
+
return <div className="whitespace-pre-wrap text-sm font-mono p-4 bg-muted rounded-lg max-h-[60vh] overflow-y-auto">{content}</div>;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 737 |
};
|
| 738 |
|
| 739 |
// ✅ Reserve space for composer so last message is never hidden
|
|
|
|
| 766 |
}
|
| 767 |
|
| 768 |
return (
|
| 769 |
+
<Select value={currentCourseId || "course1"} onValueChange={(val) => onCourseChange && onCourseChange(val)}>
|
|
|
|
|
|
|
|
|
|
| 770 |
<SelectTrigger className="w-[200px] h-9 font-semibold">
|
| 771 |
<SelectValue placeholder="Select course" />
|
| 772 |
</SelectTrigger>
|
|
|
|
| 784 |
|
| 785 |
{/* Chat Mode Tabs - Center */}
|
| 786 |
<div className="absolute left-1/2 -translate-x-1/2 flex-shrink-0">
|
| 787 |
+
<Tabs value={chatMode} onValueChange={(value) => onChatModeChange(value as ChatMode)} className="w-auto" orientation="horizontal">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 788 |
<TabsList className="inline-flex h-8 items-center justify-center rounded-xl bg-muted p-1 text-muted-foreground">
|
| 789 |
<TabsTrigger value="ask" className="w-[140px] px-3 text-sm">
|
| 790 |
Ask
|
|
|
|
| 860 |
{/* =========================
|
| 861 |
1) Scroll Container (ONLY this scrolls)
|
| 862 |
========================= */}
|
| 863 |
+
<div ref={scrollContainerRef} className="flex-1 min-h-0 overflow-y-auto overscroll-contain" style={{ overscrollBehavior: "contain" }}>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 864 |
{/* Messages */}
|
| 865 |
<div className="py-6" style={{ paddingBottom: bottomPad }}>
|
| 866 |
<div className="w-full space-y-6 max-w-4xl mx-auto">
|
|
|
|
| 870 |
message={message}
|
| 871 |
showSenderInfo={spaceType === "group"}
|
| 872 |
isFirstGreeting={
|
| 873 |
+
(message.id === "1" || message.id === "review-1" || message.id === "quiz-1") && message.role === "assistant"
|
|
|
|
| 874 |
}
|
| 875 |
showNextButton={message.showNextButton && !isAppTyping}
|
| 876 |
onNextQuestion={onNextQuestion}
|
|
|
|
| 908 |
</div>
|
| 909 |
<div className="bg-muted rounded-2xl px-4 py-3">
|
| 910 |
<div className="flex gap-1">
|
| 911 |
+
<div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: "0ms" }} />
|
| 912 |
+
<div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: "150ms" }} />
|
| 913 |
+
<div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: "300ms" }} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 914 |
</div>
|
| 915 |
</div>
|
| 916 |
</div>
|
|
|
|
| 946 |
{uploadedFiles.map((uploadedFile, index) => {
|
| 947 |
const Icon = getFileIcon(uploadedFile.file.name);
|
| 948 |
const fileInfo = getFileTypeInfo(uploadedFile.file.name);
|
| 949 |
+
const isImage = ["jpg", "jpeg", "png", "gif", "webp"].some((ext) =>
|
| 950 |
+
uploadedFile.file.name.toLowerCase().endsWith(`.${ext}`)
|
| 951 |
+
);
|
| 952 |
|
| 953 |
return (
|
| 954 |
<div key={index}>
|
|
|
|
| 963 |
}}
|
| 964 |
onRemove={(e) => {
|
| 965 |
e.stopPropagation();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 966 |
setFileToDelete(index);
|
| 967 |
setShowDeleteDialog(true);
|
| 968 |
}}
|
|
|
|
| 1030 |
</div>
|
| 1031 |
</DropdownMenuItem>
|
| 1032 |
|
| 1033 |
+
<DropdownMenuItem onClick={() => onLearningModeChange("exam")} className={learningMode === "exam" ? "bg-accent" : ""}>
|
|
|
|
|
|
|
|
|
|
| 1034 |
<div className="flex flex-col">
|
| 1035 |
<span className="font-medium">Exam Prep</span>
|
| 1036 |
<span className="text-xs text-muted-foreground">Practice with quiz questions</span>
|
|
|
|
| 1123 |
<AlertDialogContent>
|
| 1124 |
<AlertDialogHeader>
|
| 1125 |
<AlertDialogTitle>Start New Conversation</AlertDialogTitle>
|
| 1126 |
+
<AlertDialogDescription>Would you like to save the current chat before starting a new conversation?</AlertDialogDescription>
|
|
|
|
|
|
|
| 1127 |
|
| 1128 |
<Button
|
| 1129 |
variant="ghost"
|
|
|
|
| 1304 |
</DialogTitle>
|
| 1305 |
<DialogDescription>File size: {selectedFile ? formatFileSize(selectedFile.file.size) : ""}</DialogDescription>
|
| 1306 |
</DialogHeader>
|
| 1307 |
+
<div className="flex-1 min-h-0 overflow-y-auto mt-4">{selectedFile && <FileViewerContent file={selectedFile.file} />}</div>
|
|
|
|
|
|
|
| 1308 |
</DialogContent>
|
| 1309 |
</Dialog>
|
| 1310 |
|