Spaces:
Sleeping
Sleeping
Update web/src/components/ChatArea.tsx
Browse files- web/src/components/ChatArea.tsx +61 -37
web/src/components/ChatArea.tsx
CHANGED
|
@@ -50,6 +50,8 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from ".
|
|
| 50 |
import { SmartReview } from "./SmartReview";
|
| 51 |
import clareAvatar from "../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png";
|
| 52 |
|
|
|
|
|
|
|
| 53 |
interface ChatAreaProps {
|
| 54 |
messages: MessageType[];
|
| 55 |
onSendMessage: (content: string) => void;
|
|
@@ -86,6 +88,9 @@ interface ChatAreaProps {
|
|
| 86 |
onCourseChange?: (courseId: string) => void;
|
| 87 |
availableCourses?: Array<{ id: string; name: string }>;
|
| 88 |
showReviewBanner?: boolean;
|
|
|
|
|
|
|
|
|
|
| 89 |
}
|
| 90 |
|
| 91 |
interface PendingFile {
|
|
@@ -125,6 +130,7 @@ export function ChatArea({
|
|
| 125 |
onCourseChange,
|
| 126 |
availableCourses = [],
|
| 127 |
showReviewBanner = false,
|
|
|
|
| 128 |
}: ChatAreaProps) {
|
| 129 |
const [input, setInput] = useState("");
|
| 130 |
const [showScrollButton, setShowScrollButton] = useState(false);
|
|
@@ -240,6 +246,9 @@ export function ChatArea({
|
|
| 240 |
e.preventDefault();
|
| 241 |
if (!input.trim() || !isLoggedIn) return;
|
| 242 |
|
|
|
|
|
|
|
|
|
|
| 243 |
onSendMessage(input);
|
| 244 |
setInput("");
|
| 245 |
};
|
|
@@ -270,6 +279,9 @@ export function ChatArea({
|
|
| 270 |
weight: number;
|
| 271 |
lastReviewed: string;
|
| 272 |
}) => {
|
|
|
|
|
|
|
|
|
|
| 273 |
const userMessage = `Please help me review: ${item.title}`;
|
| 274 |
const reviewData = `REVIEW_TOPIC:${item.title}|${item.previousQuestion}|${item.memoryRetention}|${item.schedule}|${item.status}|${item.weight}|${item.lastReviewed}`;
|
| 275 |
(window as any).__lastReviewData = reviewData;
|
|
@@ -277,6 +289,9 @@ export function ChatArea({
|
|
| 277 |
};
|
| 278 |
|
| 279 |
const handleReviewAll = () => {
|
|
|
|
|
|
|
|
|
|
| 280 |
(window as any).__lastReviewData = "REVIEW_ALL";
|
| 281 |
onSendMessage("Please help me review all topics that need attention.");
|
| 282 |
};
|
|
@@ -593,7 +608,11 @@ export function ChatArea({
|
|
| 593 |
|
| 594 |
if (isImage) {
|
| 595 |
return (
|
| 596 |
-
<div
|
|
|
|
|
|
|
|
|
|
|
|
|
| 597 |
<div className="w-full h-full relative bg-card border border-border rounded-lg hover:border-primary/50 transition-colors">
|
| 598 |
<div className="w-full h-full overflow-hidden rounded-lg absolute inset-0">
|
| 599 |
{imageLoading ? (
|
|
@@ -729,11 +748,7 @@ export function ChatArea({
|
|
| 729 |
);
|
| 730 |
}
|
| 731 |
|
| 732 |
-
return
|
| 733 |
-
<div className="whitespace-pre-wrap text-sm font-mono p-4 bg-muted rounded-lg max-h-[60vh] overflow-y-auto">
|
| 734 |
-
{content}
|
| 735 |
-
</div>
|
| 736 |
-
);
|
| 737 |
};
|
| 738 |
|
| 739 |
// ✅ Reserve space for composer so last message is never hidden
|
|
@@ -784,12 +799,7 @@ export function ChatArea({
|
|
| 784 |
|
| 785 |
{/* Chat Mode Tabs - Center */}
|
| 786 |
<div className="absolute left-1/2 -translate-x-1/2 flex-shrink-0">
|
| 787 |
-
<Tabs
|
| 788 |
-
value={chatMode}
|
| 789 |
-
onValueChange={(value) => onChatModeChange(value as ChatMode)}
|
| 790 |
-
className="w-auto"
|
| 791 |
-
orientation="horizontal"
|
| 792 |
-
>
|
| 793 |
<TabsList className="inline-flex h-8 items-center justify-center rounded-xl bg-muted p-1 text-muted-foreground">
|
| 794 |
<TabsTrigger value="ask" className="w-[140px] px-3 text-sm">
|
| 795 |
Ask
|
|
@@ -865,11 +875,7 @@ export function ChatArea({
|
|
| 865 |
{/* =========================
|
| 866 |
1) Scroll Container (ONLY this scrolls)
|
| 867 |
========================= */}
|
| 868 |
-
<div
|
| 869 |
-
ref={scrollContainerRef}
|
| 870 |
-
className="flex-1 min-h-0 overflow-y-auto overscroll-contain"
|
| 871 |
-
style={{ overscrollBehavior: "contain" }}
|
| 872 |
-
>
|
| 873 |
{/* Messages */}
|
| 874 |
<div className="py-6" style={{ paddingBottom: bottomPad }}>
|
| 875 |
<div className="w-full space-y-6 max-w-4xl mx-auto">
|
|
@@ -879,8 +885,7 @@ export function ChatArea({
|
|
| 879 |
message={message}
|
| 880 |
showSenderInfo={spaceType === "group"}
|
| 881 |
isFirstGreeting={
|
| 882 |
-
(message.id === "1" || message.id === "review-1" || message.id === "quiz-1") &&
|
| 883 |
-
message.role === "assistant"
|
| 884 |
}
|
| 885 |
showNextButton={message.showNextButton && !isAppTyping}
|
| 886 |
onNextQuestion={onNextQuestion}
|
|
@@ -933,10 +938,7 @@ export function ChatArea({
|
|
| 933 |
2) Scroll-to-bottom button (positioned above composer)
|
| 934 |
========================= */}
|
| 935 |
{showScrollButton && (
|
| 936 |
-
<div
|
| 937 |
-
className="absolute z-30 left-0 right-0 flex justify-center pointer-events-none"
|
| 938 |
-
style={{ bottom: composerHeight + 16 }}
|
| 939 |
-
>
|
| 940 |
<Button
|
| 941 |
variant="secondary"
|
| 942 |
size="icon"
|
|
@@ -1013,21 +1015,30 @@ export function ChatArea({
|
|
| 1013 |
</Button>
|
| 1014 |
</DropdownMenuTrigger>
|
| 1015 |
<DropdownMenuContent align="start" className="w-56">
|
| 1016 |
-
<DropdownMenuItem
|
|
|
|
|
|
|
|
|
|
| 1017 |
<div className="flex flex-col">
|
| 1018 |
<span className="font-medium">General</span>
|
| 1019 |
<span className="text-xs text-muted-foreground">Answer various questions (context required)</span>
|
| 1020 |
</div>
|
| 1021 |
</DropdownMenuItem>
|
| 1022 |
|
| 1023 |
-
<DropdownMenuItem
|
|
|
|
|
|
|
|
|
|
| 1024 |
<div className="flex flex-col">
|
| 1025 |
<span className="font-medium">Concept Explainer</span>
|
| 1026 |
<span className="text-xs text-muted-foreground">Get detailed explanations of concepts</span>
|
| 1027 |
</div>
|
| 1028 |
</DropdownMenuItem>
|
| 1029 |
|
| 1030 |
-
<DropdownMenuItem
|
|
|
|
|
|
|
|
|
|
| 1031 |
<div className="flex flex-col">
|
| 1032 |
<span className="font-medium">Socratic Tutor</span>
|
| 1033 |
<span className="text-xs text-muted-foreground">Learn through guided questions</span>
|
|
@@ -1041,14 +1052,20 @@ export function ChatArea({
|
|
| 1041 |
</div>
|
| 1042 |
</DropdownMenuItem>
|
| 1043 |
|
| 1044 |
-
<DropdownMenuItem
|
|
|
|
|
|
|
|
|
|
| 1045 |
<div className="flex flex-col">
|
| 1046 |
<span className="font-medium">Assignment Helper</span>
|
| 1047 |
<span className="text-xs text-muted-foreground">Get help with assignments</span>
|
| 1048 |
</div>
|
| 1049 |
</DropdownMenuItem>
|
| 1050 |
|
| 1051 |
-
<DropdownMenuItem
|
|
|
|
|
|
|
|
|
|
| 1052 |
<div className="flex flex-col">
|
| 1053 |
<span className="font-medium">Quick Summary</span>
|
| 1054 |
<span className="text-xs text-muted-foreground">Get concise summaries</span>
|
|
@@ -1091,7 +1108,9 @@ export function ChatArea({
|
|
| 1091 |
: "Ask Clare anything about the course or drag files here..."
|
| 1092 |
}
|
| 1093 |
disabled={!isLoggedIn || (chatMode === "quiz" && !quizState.waitingForAnswer)}
|
| 1094 |
-
className={`min-h-[80px] pl-4 pr-20 resize-none bg-background border-2 ${
|
|
|
|
|
|
|
| 1095 |
/>
|
| 1096 |
|
| 1097 |
<div className="absolute bottom-2 right-2 flex gap-1">
|
|
@@ -1119,9 +1138,7 @@ export function ChatArea({
|
|
| 1119 |
<AlertDialogContent>
|
| 1120 |
<AlertDialogHeader>
|
| 1121 |
<AlertDialogTitle>Start New Conversation</AlertDialogTitle>
|
| 1122 |
-
<AlertDialogDescription>
|
| 1123 |
-
Would you like to save the current chat before starting a new conversation?
|
| 1124 |
-
</AlertDialogDescription>
|
| 1125 |
|
| 1126 |
<Button
|
| 1127 |
variant="ghost"
|
|
@@ -1173,7 +1190,13 @@ export function ChatArea({
|
|
| 1173 |
<div className="border rounded-lg bg-muted/40 flex flex-col max-h-64">
|
| 1174 |
<div className="flex items-center justify-between p-4 sticky top-0 bg-muted/40 border-b z-10">
|
| 1175 |
<span className="text-sm font-medium">Preview</span>
|
| 1176 |
-
<Button
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1177 |
<Copy className="h-3 w-3" />
|
| 1178 |
Copy
|
| 1179 |
</Button>
|
|
@@ -1248,9 +1271,7 @@ export function ChatArea({
|
|
| 1248 |
))}
|
| 1249 |
</SelectContent>
|
| 1250 |
</Select>
|
| 1251 |
-
<p className="text-xs text-muted-foreground">
|
| 1252 |
-
Sends this conversation to the selected workspace's Saved Files.
|
| 1253 |
-
</p>
|
| 1254 |
<Button onClick={handleShareSendToWorkspace} className="w-full">
|
| 1255 |
Send
|
| 1256 |
</Button>
|
|
@@ -1326,7 +1347,10 @@ export function ChatArea({
|
|
| 1326 |
|
| 1327 |
<div className="space-y-1">
|
| 1328 |
<label className="text-xs text-muted-foreground">File Type</label>
|
| 1329 |
-
<Select
|
|
|
|
|
|
|
|
|
|
| 1330 |
<SelectTrigger className="h-8 text-xs">
|
| 1331 |
<SelectValue />
|
| 1332 |
</SelectTrigger>
|
|
|
|
| 50 |
import { SmartReview } from "./SmartReview";
|
| 51 |
import clareAvatar from "../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png";
|
| 52 |
|
| 53 |
+
type ReviewEventType = "send_message" | "review_topic" | "review_all";
|
| 54 |
+
|
| 55 |
interface ChatAreaProps {
|
| 56 |
messages: MessageType[];
|
| 57 |
onSendMessage: (content: string) => void;
|
|
|
|
| 88 |
onCourseChange?: (courseId: string) => void;
|
| 89 |
availableCourses?: Array<{ id: string; name: string }>;
|
| 90 |
showReviewBanner?: boolean;
|
| 91 |
+
|
| 92 |
+
// ✅ NEW: for Review star brightness (daily)
|
| 93 |
+
onReviewActivity?: (event: ReviewEventType) => void;
|
| 94 |
}
|
| 95 |
|
| 96 |
interface PendingFile {
|
|
|
|
| 130 |
onCourseChange,
|
| 131 |
availableCourses = [],
|
| 132 |
showReviewBanner = false,
|
| 133 |
+
onReviewActivity,
|
| 134 |
}: ChatAreaProps) {
|
| 135 |
const [input, setInput] = useState("");
|
| 136 |
const [showScrollButton, setShowScrollButton] = useState(false);
|
|
|
|
| 246 |
e.preventDefault();
|
| 247 |
if (!input.trim() || !isLoggedIn) return;
|
| 248 |
|
| 249 |
+
// ✅ Review activity: user sent a message in Review tab
|
| 250 |
+
if (chatMode === "review") onReviewActivity?.("send_message");
|
| 251 |
+
|
| 252 |
onSendMessage(input);
|
| 253 |
setInput("");
|
| 254 |
};
|
|
|
|
| 279 |
weight: number;
|
| 280 |
lastReviewed: string;
|
| 281 |
}) => {
|
| 282 |
+
// ✅ Review activity: clicked Review this topic
|
| 283 |
+
onReviewActivity?.("review_topic");
|
| 284 |
+
|
| 285 |
const userMessage = `Please help me review: ${item.title}`;
|
| 286 |
const reviewData = `REVIEW_TOPIC:${item.title}|${item.previousQuestion}|${item.memoryRetention}|${item.schedule}|${item.status}|${item.weight}|${item.lastReviewed}`;
|
| 287 |
(window as any).__lastReviewData = reviewData;
|
|
|
|
| 289 |
};
|
| 290 |
|
| 291 |
const handleReviewAll = () => {
|
| 292 |
+
// ✅ Review activity: clicked Review all
|
| 293 |
+
onReviewActivity?.("review_all");
|
| 294 |
+
|
| 295 |
(window as any).__lastReviewData = "REVIEW_ALL";
|
| 296 |
onSendMessage("Please help me review all topics that need attention.");
|
| 297 |
};
|
|
|
|
| 608 |
|
| 609 |
if (isImage) {
|
| 610 |
return (
|
| 611 |
+
<div
|
| 612 |
+
className="relative cursor-pointer w-16 h-16 flex-shrink-0"
|
| 613 |
+
onClick={onPreview}
|
| 614 |
+
style={{ width: 64, height: 64 }}
|
| 615 |
+
>
|
| 616 |
<div className="w-full h-full relative bg-card border border-border rounded-lg hover:border-primary/50 transition-colors">
|
| 617 |
<div className="w-full h-full overflow-hidden rounded-lg absolute inset-0">
|
| 618 |
{imageLoading ? (
|
|
|
|
| 748 |
);
|
| 749 |
}
|
| 750 |
|
| 751 |
+
return <div className="whitespace-pre-wrap text-sm font-mono p-4 bg-muted rounded-lg max-h-[60vh] overflow-y-auto">{content}</div>;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 752 |
};
|
| 753 |
|
| 754 |
// ✅ Reserve space for composer so last message is never hidden
|
|
|
|
| 799 |
|
| 800 |
{/* Chat Mode Tabs - Center */}
|
| 801 |
<div className="absolute left-1/2 -translate-x-1/2 flex-shrink-0">
|
| 802 |
+
<Tabs value={chatMode} onValueChange={(value) => onChatModeChange(value as ChatMode)} className="w-auto" orientation="horizontal">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 803 |
<TabsList className="inline-flex h-8 items-center justify-center rounded-xl bg-muted p-1 text-muted-foreground">
|
| 804 |
<TabsTrigger value="ask" className="w-[140px] px-3 text-sm">
|
| 805 |
Ask
|
|
|
|
| 875 |
{/* =========================
|
| 876 |
1) Scroll Container (ONLY this scrolls)
|
| 877 |
========================= */}
|
| 878 |
+
<div ref={scrollContainerRef} className="flex-1 min-h-0 overflow-y-auto overscroll-contain" style={{ overscrollBehavior: "contain" }}>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 879 |
{/* Messages */}
|
| 880 |
<div className="py-6" style={{ paddingBottom: bottomPad }}>
|
| 881 |
<div className="w-full space-y-6 max-w-4xl mx-auto">
|
|
|
|
| 885 |
message={message}
|
| 886 |
showSenderInfo={spaceType === "group"}
|
| 887 |
isFirstGreeting={
|
| 888 |
+
(message.id === "1" || message.id === "review-1" || message.id === "quiz-1") && message.role === "assistant"
|
|
|
|
| 889 |
}
|
| 890 |
showNextButton={message.showNextButton && !isAppTyping}
|
| 891 |
onNextQuestion={onNextQuestion}
|
|
|
|
| 938 |
2) Scroll-to-bottom button (positioned above composer)
|
| 939 |
========================= */}
|
| 940 |
{showScrollButton && (
|
| 941 |
+
<div className="absolute z-30 left-0 right-0 flex justify-center pointer-events-none" style={{ bottom: composerHeight + 16 }}>
|
|
|
|
|
|
|
|
|
|
| 942 |
<Button
|
| 943 |
variant="secondary"
|
| 944 |
size="icon"
|
|
|
|
| 1015 |
</Button>
|
| 1016 |
</DropdownMenuTrigger>
|
| 1017 |
<DropdownMenuContent align="start" className="w-56">
|
| 1018 |
+
<DropdownMenuItem
|
| 1019 |
+
onClick={() => onLearningModeChange("general")}
|
| 1020 |
+
className={learningMode === "general" ? "bg-accent" : ""}
|
| 1021 |
+
>
|
| 1022 |
<div className="flex flex-col">
|
| 1023 |
<span className="font-medium">General</span>
|
| 1024 |
<span className="text-xs text-muted-foreground">Answer various questions (context required)</span>
|
| 1025 |
</div>
|
| 1026 |
</DropdownMenuItem>
|
| 1027 |
|
| 1028 |
+
<DropdownMenuItem
|
| 1029 |
+
onClick={() => onLearningModeChange("concept")}
|
| 1030 |
+
className={learningMode === "concept" ? "bg-accent" : ""}
|
| 1031 |
+
>
|
| 1032 |
<div className="flex flex-col">
|
| 1033 |
<span className="font-medium">Concept Explainer</span>
|
| 1034 |
<span className="text-xs text-muted-foreground">Get detailed explanations of concepts</span>
|
| 1035 |
</div>
|
| 1036 |
</DropdownMenuItem>
|
| 1037 |
|
| 1038 |
+
<DropdownMenuItem
|
| 1039 |
+
onClick={() => onLearningModeChange("socratic")}
|
| 1040 |
+
className={learningMode === "socratic" ? "bg-accent" : ""}
|
| 1041 |
+
>
|
| 1042 |
<div className="flex flex-col">
|
| 1043 |
<span className="font-medium">Socratic Tutor</span>
|
| 1044 |
<span className="text-xs text-muted-foreground">Learn through guided questions</span>
|
|
|
|
| 1052 |
</div>
|
| 1053 |
</DropdownMenuItem>
|
| 1054 |
|
| 1055 |
+
<DropdownMenuItem
|
| 1056 |
+
onClick={() => onLearningModeChange("assignment")}
|
| 1057 |
+
className={learningMode === "assignment" ? "bg-accent" : ""}
|
| 1058 |
+
>
|
| 1059 |
<div className="flex flex-col">
|
| 1060 |
<span className="font-medium">Assignment Helper</span>
|
| 1061 |
<span className="text-xs text-muted-foreground">Get help with assignments</span>
|
| 1062 |
</div>
|
| 1063 |
</DropdownMenuItem>
|
| 1064 |
|
| 1065 |
+
<DropdownMenuItem
|
| 1066 |
+
onClick={() => onLearningModeChange("summary")}
|
| 1067 |
+
className={learningMode === "summary" ? "bg-accent" : ""}
|
| 1068 |
+
>
|
| 1069 |
<div className="flex flex-col">
|
| 1070 |
<span className="font-medium">Quick Summary</span>
|
| 1071 |
<span className="text-xs text-muted-foreground">Get concise summaries</span>
|
|
|
|
| 1108 |
: "Ask Clare anything about the course or drag files here..."
|
| 1109 |
}
|
| 1110 |
disabled={!isLoggedIn || (chatMode === "quiz" && !quizState.waitingForAnswer)}
|
| 1111 |
+
className={`min-h-[80px] pl-4 pr-20 resize-none bg-background border-2 ${
|
| 1112 |
+
isDragging ? "border-primary border-dashed" : "border-border"
|
| 1113 |
+
}`}
|
| 1114 |
/>
|
| 1115 |
|
| 1116 |
<div className="absolute bottom-2 right-2 flex gap-1">
|
|
|
|
| 1138 |
<AlertDialogContent>
|
| 1139 |
<AlertDialogHeader>
|
| 1140 |
<AlertDialogTitle>Start New Conversation</AlertDialogTitle>
|
| 1141 |
+
<AlertDialogDescription>Would you like to save the current chat before starting a new conversation?</AlertDialogDescription>
|
|
|
|
|
|
|
| 1142 |
|
| 1143 |
<Button
|
| 1144 |
variant="ghost"
|
|
|
|
| 1190 |
<div className="border rounded-lg bg-muted/40 flex flex-col max-h-64">
|
| 1191 |
<div className="flex items-center justify-between p-4 sticky top-0 bg-muted/40 border-b z-10">
|
| 1192 |
<span className="text-sm font-medium">Preview</span>
|
| 1193 |
+
<Button
|
| 1194 |
+
variant="outline"
|
| 1195 |
+
size="sm"
|
| 1196 |
+
className="h-7 px-2 text-xs gap-1.5"
|
| 1197 |
+
onClick={handleCopyPreview}
|
| 1198 |
+
title="Copy preview"
|
| 1199 |
+
>
|
| 1200 |
<Copy className="h-3 w-3" />
|
| 1201 |
Copy
|
| 1202 |
</Button>
|
|
|
|
| 1271 |
))}
|
| 1272 |
</SelectContent>
|
| 1273 |
</Select>
|
| 1274 |
+
<p className="text-xs text-muted-foreground">Sends this conversation to the selected workspace's Saved Files.</p>
|
|
|
|
|
|
|
| 1275 |
<Button onClick={handleShareSendToWorkspace} className="w-full">
|
| 1276 |
Send
|
| 1277 |
</Button>
|
|
|
|
| 1347 |
|
| 1348 |
<div className="space-y-1">
|
| 1349 |
<label className="text-xs text-muted-foreground">File Type</label>
|
| 1350 |
+
<Select
|
| 1351 |
+
value={pendingFile.type}
|
| 1352 |
+
onValueChange={(value) => handlePendingFileTypeChange(index, value as FileType)}
|
| 1353 |
+
>
|
| 1354 |
<SelectTrigger className="h-8 text-xs">
|
| 1355 |
<SelectValue />
|
| 1356 |
</SelectTrigger>
|