Spaces:
Sleeping
Sleeping
Update web/src/components/ChatArea.tsx
Browse files- web/src/components/ChatArea.tsx +28 -93
web/src/components/ChatArea.tsx
CHANGED
|
@@ -137,7 +137,7 @@ export function ChatArea({
|
|
| 137 |
workspaces,
|
| 138 |
currentWorkspaceId,
|
| 139 |
onSaveFile,
|
| 140 |
-
leftPanelVisible = false,
|
| 141 |
currentCourseId,
|
| 142 |
onCourseChange,
|
| 143 |
availableCourses = [],
|
|
@@ -166,7 +166,6 @@ export function ChatArea({
|
|
| 166 |
const [shareLink, setShareLink] = useState("");
|
| 167 |
const [targetWorkspaceId, setTargetWorkspaceId] = useState<string>("");
|
| 168 |
|
| 169 |
-
// Use availableCourses if provided, otherwise fallback
|
| 170 |
const courses =
|
| 171 |
availableCourses.length > 0
|
| 172 |
? availableCourses
|
|
@@ -194,7 +193,6 @@ export function ChatArea({
|
|
| 194 |
isInitialMount.current = false;
|
| 195 |
previousMessagesLength.current = messages.length;
|
| 196 |
|
| 197 |
-
// stay at top on initial load
|
| 198 |
if (scrollContainerRef.current) {
|
| 199 |
scrollContainerRef.current.scrollTop = 0;
|
| 200 |
}
|
|
@@ -227,26 +225,6 @@ export function ChatArea({
|
|
| 227 |
return () => container.removeEventListener("scroll", handleScroll);
|
| 228 |
}, [messages]);
|
| 229 |
|
| 230 |
-
// ✅ Prevent scroll chaining to left panel / outer containers
|
| 231 |
-
useEffect(() => {
|
| 232 |
-
const el = scrollContainerRef.current;
|
| 233 |
-
if (!el) return;
|
| 234 |
-
|
| 235 |
-
const onWheel = (e: WheelEvent) => {
|
| 236 |
-
e.stopPropagation();
|
| 237 |
-
|
| 238 |
-
const atTop = el.scrollTop <= 0;
|
| 239 |
-
const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 1;
|
| 240 |
-
|
| 241 |
-
if ((atTop && e.deltaY < 0) || (atBottom && e.deltaY > 0)) {
|
| 242 |
-
e.preventDefault();
|
| 243 |
-
}
|
| 244 |
-
};
|
| 245 |
-
|
| 246 |
-
el.addEventListener("wheel", onWheel, { passive: false });
|
| 247 |
-
return () => el.removeEventListener("wheel", onWheel);
|
| 248 |
-
}, []);
|
| 249 |
-
|
| 250 |
const handleSubmit = (e: React.FormEvent) => {
|
| 251 |
e.preventDefault();
|
| 252 |
if (!input.trim() || !isLoggedIn) return;
|
|
@@ -294,7 +272,9 @@ export function ChatArea({
|
|
| 294 |
|
| 295 |
const buildPreviewContent = () => {
|
| 296 |
if (!messages.length) return "";
|
| 297 |
-
return messages
|
|
|
|
|
|
|
| 298 |
};
|
| 299 |
|
| 300 |
const buildSummaryContent = () => {
|
|
@@ -441,7 +421,6 @@ export function ChatArea({
|
|
| 441 |
const saved = isCurrentChatSaved();
|
| 442 |
|
| 443 |
if (saved) {
|
| 444 |
-
// current logic: if saved, don't prompt — just start new
|
| 445 |
onConfirmClear(false);
|
| 446 |
return;
|
| 447 |
}
|
|
@@ -484,8 +463,8 @@ export function ChatArea({
|
|
| 484 |
|
| 485 |
const validFiles = files.filter((file) => {
|
| 486 |
const ext = file.name.toLowerCase();
|
| 487 |
-
return [".pdf", ".docx", ".pptx", ".jpg", ".jpeg", ".png", ".gif", ".webp", ".doc", ".ppt"].some(
|
| 488 |
-
ext.endsWith(allowed)
|
| 489 |
);
|
| 490 |
});
|
| 491 |
|
|
@@ -502,8 +481,8 @@ export function ChatArea({
|
|
| 502 |
if (files.length > 0) {
|
| 503 |
const validFiles = files.filter((file) => {
|
| 504 |
const ext = file.name.toLowerCase();
|
| 505 |
-
return [".pdf", ".docx", ".pptx", ".jpg", ".jpeg", ".png", ".gif", ".webp", ".doc", ".ppt"].some(
|
| 506 |
-
ext.endsWith(allowed)
|
| 507 |
);
|
| 508 |
});
|
| 509 |
|
|
@@ -521,7 +500,6 @@ export function ChatArea({
|
|
| 521 |
onFileUpload(pendingFiles.map((pf) => pf.file));
|
| 522 |
const startIndex = uploadedFiles.length;
|
| 523 |
|
| 524 |
-
// After uploadedFiles state grows (in parent), we call type change on next tick
|
| 525 |
pendingFiles.forEach((pf, idx) => {
|
| 526 |
setTimeout(() => {
|
| 527 |
onFileTypeChange(startIndex + idx, pf.type);
|
|
@@ -756,9 +734,10 @@ export function ChatArea({
|
|
| 756 |
};
|
| 757 |
|
| 758 |
return (
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
|
|
|
| 762 |
<div
|
| 763 |
ref={scrollContainerRef}
|
| 764 |
className="flex-1 min-h-0 overflow-y-auto"
|
|
@@ -787,10 +766,7 @@ export function ChatArea({
|
|
| 787 |
}
|
| 788 |
|
| 789 |
return (
|
| 790 |
-
<Select
|
| 791 |
-
value={currentCourseId || "course1"}
|
| 792 |
-
onValueChange={(val) => onCourseChange && onCourseChange(val)}
|
| 793 |
-
>
|
| 794 |
<SelectTrigger className="w-[200px] h-9 font-semibold">
|
| 795 |
<SelectValue placeholder="Select course" />
|
| 796 |
</SelectTrigger>
|
|
@@ -934,18 +910,9 @@ export function ChatArea({
|
|
| 934 |
</div>
|
| 935 |
<div className="bg-muted rounded-2xl px-4 py-3">
|
| 936 |
<div className="flex gap-1">
|
| 937 |
-
<div
|
| 938 |
-
|
| 939 |
-
|
| 940 |
-
/>
|
| 941 |
-
<div
|
| 942 |
-
className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce"
|
| 943 |
-
style={{ animationDelay: "150ms" }}
|
| 944 |
-
/>
|
| 945 |
-
<div
|
| 946 |
-
className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce"
|
| 947 |
-
style={{ animationDelay: "300ms" }}
|
| 948 |
-
/>
|
| 949 |
</div>
|
| 950 |
</div>
|
| 951 |
</div>
|
|
@@ -956,15 +923,11 @@ export function ChatArea({
|
|
| 956 |
</div>
|
| 957 |
</div>
|
| 958 |
|
| 959 |
-
{/* Scroll to Bottom Button */}
|
| 960 |
{showScrollButton && (
|
| 961 |
<div
|
| 962 |
-
className="
|
| 963 |
-
style={{
|
| 964 |
-
bottom: "120px",
|
| 965 |
-
left: leftPanelVisible ? "320px" : "0px",
|
| 966 |
-
right: "0px",
|
| 967 |
-
}}
|
| 968 |
>
|
| 969 |
<Button
|
| 970 |
variant="secondary"
|
|
@@ -978,14 +941,8 @@ export function ChatArea({
|
|
| 978 |
</div>
|
| 979 |
)}
|
| 980 |
|
| 981 |
-
{/* Floating Input Area */}
|
| 982 |
-
<div
|
| 983 |
-
className="fixed bottom-0 bg-background/95 backdrop-blur-sm z-10"
|
| 984 |
-
style={{
|
| 985 |
-
left: leftPanelVisible ? "320px" : "0px",
|
| 986 |
-
right: "0px",
|
| 987 |
-
}}
|
| 988 |
-
>
|
| 989 |
<div className="max-w-4xl mx-auto px-4 py-4">
|
| 990 |
{uploadedFiles.length > 0 && (
|
| 991 |
<div className="mb-2 flex flex-wrap gap-2 max-h-32 overflow-y-auto">
|
|
@@ -1052,9 +1009,7 @@ export function ChatArea({
|
|
| 1052 |
>
|
| 1053 |
<div className="flex flex-col">
|
| 1054 |
<span className="font-medium">General</span>
|
| 1055 |
-
<span className="text-xs text-muted-foreground">
|
| 1056 |
-
Answer various questions (context required)
|
| 1057 |
-
</span>
|
| 1058 |
</div>
|
| 1059 |
</DropdownMenuItem>
|
| 1060 |
|
|
@@ -1229,13 +1184,7 @@ export function ChatArea({
|
|
| 1229 |
<div className="border rounded-lg bg-muted/40 flex flex-col max-h-64">
|
| 1230 |
<div className="flex items-center justify-between p-4 sticky top-0 bg-muted/40 border-b z-10">
|
| 1231 |
<span className="text-sm font-medium">Preview</span>
|
| 1232 |
-
<Button
|
| 1233 |
-
variant="outline"
|
| 1234 |
-
size="sm"
|
| 1235 |
-
className="h-7 px-2 text-xs gap-1.5"
|
| 1236 |
-
onClick={handleCopyPreview}
|
| 1237 |
-
title="Copy preview"
|
| 1238 |
-
>
|
| 1239 |
<Copy className="h-3 w-3" />
|
| 1240 |
Copy
|
| 1241 |
</Button>
|
|
@@ -1310,9 +1259,7 @@ export function ChatArea({
|
|
| 1310 |
))}
|
| 1311 |
</SelectContent>
|
| 1312 |
</Select>
|
| 1313 |
-
<p className="text-xs text-muted-foreground">
|
| 1314 |
-
Sends this conversation to the selected workspace's Saved Files.
|
| 1315 |
-
</p>
|
| 1316 |
<Button onClick={handleShareSendToWorkspace} className="w-full">
|
| 1317 |
Send
|
| 1318 |
</Button>
|
|
@@ -1327,9 +1274,7 @@ export function ChatArea({
|
|
| 1327 |
<AlertDialogHeader>
|
| 1328 |
<AlertDialogTitle>Delete File</AlertDialogTitle>
|
| 1329 |
<AlertDialogDescription>
|
| 1330 |
-
Are you sure you want to delete "
|
| 1331 |
-
{fileToDelete !== null ? uploadedFiles[fileToDelete]?.file.name : ""}
|
| 1332 |
-
"? This action cannot be undone.
|
| 1333 |
</AlertDialogDescription>
|
| 1334 |
</AlertDialogHeader>
|
| 1335 |
<AlertDialogFooter>
|
|
@@ -1355,18 +1300,11 @@ export function ChatArea({
|
|
| 1355 |
<DialogHeader className="min-w-0 flex-shrink-0">
|
| 1356 |
<DialogTitle
|
| 1357 |
className="pr-8 break-words break-all overflow-wrap-anywhere leading-relaxed"
|
| 1358 |
-
style={{
|
| 1359 |
-
wordBreak: "break-all",
|
| 1360 |
-
overflowWrap: "anywhere",
|
| 1361 |
-
maxWidth: "100%",
|
| 1362 |
-
lineHeight: "1.6",
|
| 1363 |
-
}}
|
| 1364 |
>
|
| 1365 |
{selectedFile?.file.name}
|
| 1366 |
</DialogTitle>
|
| 1367 |
-
<DialogDescription>
|
| 1368 |
-
File size: {selectedFile ? formatFileSize(selectedFile.file.size) : ""}
|
| 1369 |
-
</DialogDescription>
|
| 1370 |
</DialogHeader>
|
| 1371 |
<div className="flex-1 min-h-0 overflow-y-auto mt-4">{selectedFile && <FileViewerContent file={selectedFile.file} />}</div>
|
| 1372 |
</DialogContent>
|
|
@@ -1396,10 +1334,7 @@ export function ChatArea({
|
|
| 1396 |
|
| 1397 |
<div className="space-y-1">
|
| 1398 |
<label className="text-xs text-muted-foreground">File Type</label>
|
| 1399 |
-
<Select
|
| 1400 |
-
value={pendingFile.type}
|
| 1401 |
-
onValueChange={(value) => handlePendingFileTypeChange(index, value as FileType)}
|
| 1402 |
-
>
|
| 1403 |
<SelectTrigger className="h-8 text-xs">
|
| 1404 |
<SelectValue />
|
| 1405 |
</SelectTrigger>
|
|
|
|
| 137 |
workspaces,
|
| 138 |
currentWorkspaceId,
|
| 139 |
onSaveFile,
|
| 140 |
+
leftPanelVisible = false, // kept for props compatibility; not used for positioning anymore
|
| 141 |
currentCourseId,
|
| 142 |
onCourseChange,
|
| 143 |
availableCourses = [],
|
|
|
|
| 166 |
const [shareLink, setShareLink] = useState("");
|
| 167 |
const [targetWorkspaceId, setTargetWorkspaceId] = useState<string>("");
|
| 168 |
|
|
|
|
| 169 |
const courses =
|
| 170 |
availableCourses.length > 0
|
| 171 |
? availableCourses
|
|
|
|
| 193 |
isInitialMount.current = false;
|
| 194 |
previousMessagesLength.current = messages.length;
|
| 195 |
|
|
|
|
| 196 |
if (scrollContainerRef.current) {
|
| 197 |
scrollContainerRef.current.scrollTop = 0;
|
| 198 |
}
|
|
|
|
| 225 |
return () => container.removeEventListener("scroll", handleScroll);
|
| 226 |
}, [messages]);
|
| 227 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
const handleSubmit = (e: React.FormEvent) => {
|
| 229 |
e.preventDefault();
|
| 230 |
if (!input.trim() || !isLoggedIn) return;
|
|
|
|
| 272 |
|
| 273 |
const buildPreviewContent = () => {
|
| 274 |
if (!messages.length) return "";
|
| 275 |
+
return messages
|
| 276 |
+
.map((msg) => `${msg.role === "user" ? "You" : "Clare"}: ${msg.content}`)
|
| 277 |
+
.join("\n\n");
|
| 278 |
};
|
| 279 |
|
| 280 |
const buildSummaryContent = () => {
|
|
|
|
| 421 |
const saved = isCurrentChatSaved();
|
| 422 |
|
| 423 |
if (saved) {
|
|
|
|
| 424 |
onConfirmClear(false);
|
| 425 |
return;
|
| 426 |
}
|
|
|
|
| 463 |
|
| 464 |
const validFiles = files.filter((file) => {
|
| 465 |
const ext = file.name.toLowerCase();
|
| 466 |
+
return [".pdf", ".docx", ".pptx", ".jpg", ".jpeg", ".png", ".gif", ".webp", ".doc", ".ppt"].some(
|
| 467 |
+
(allowed) => ext.endsWith(allowed)
|
| 468 |
);
|
| 469 |
});
|
| 470 |
|
|
|
|
| 481 |
if (files.length > 0) {
|
| 482 |
const validFiles = files.filter((file) => {
|
| 483 |
const ext = file.name.toLowerCase();
|
| 484 |
+
return [".pdf", ".docx", ".pptx", ".jpg", ".jpeg", ".png", ".gif", ".webp", ".doc", ".ppt"].some(
|
| 485 |
+
(allowed) => ext.endsWith(allowed)
|
| 486 |
);
|
| 487 |
});
|
| 488 |
|
|
|
|
| 500 |
onFileUpload(pendingFiles.map((pf) => pf.file));
|
| 501 |
const startIndex = uploadedFiles.length;
|
| 502 |
|
|
|
|
| 503 |
pendingFiles.forEach((pf, idx) => {
|
| 504 |
setTimeout(() => {
|
| 505 |
onFileTypeChange(startIndex + idx, pf.type);
|
|
|
|
| 734 |
};
|
| 735 |
|
| 736 |
return (
|
| 737 |
+
// IMPORTANT: relative enables internal absolute overlays (input + scroll button)
|
| 738 |
+
<div className="relative flex flex-col h-full min-h-0 overflow-hidden">
|
| 739 |
+
<div className="flex-1 relative min-h-0 flex flex-col overflow-hidden">
|
| 740 |
+
{/* Messages Area is the ONLY scroll container */}
|
| 741 |
<div
|
| 742 |
ref={scrollContainerRef}
|
| 743 |
className="flex-1 min-h-0 overflow-y-auto"
|
|
|
|
| 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>
|
|
|
|
| 910 |
</div>
|
| 911 |
<div className="bg-muted rounded-2xl px-4 py-3">
|
| 912 |
<div className="flex gap-1">
|
| 913 |
+
<div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: "0ms" }} />
|
| 914 |
+
<div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: "150ms" }} />
|
| 915 |
+
<div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: "300ms" }} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 916 |
</div>
|
| 917 |
</div>
|
| 918 |
</div>
|
|
|
|
| 923 |
</div>
|
| 924 |
</div>
|
| 925 |
|
| 926 |
+
{/* Scroll to Bottom Button (NOW absolute inside ChatArea) */}
|
| 927 |
{showScrollButton && (
|
| 928 |
<div
|
| 929 |
+
className="absolute z-30 left-0 right-0 flex justify-center pointer-events-none"
|
| 930 |
+
style={{ bottom: "120px" }}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 931 |
>
|
| 932 |
<Button
|
| 933 |
variant="secondary"
|
|
|
|
| 941 |
</div>
|
| 942 |
)}
|
| 943 |
|
| 944 |
+
{/* Floating Input Area (NOW absolute inside ChatArea) */}
|
| 945 |
+
<div className="absolute bottom-0 left-0 right-0 bg-background/95 backdrop-blur-sm z-10">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 946 |
<div className="max-w-4xl mx-auto px-4 py-4">
|
| 947 |
{uploadedFiles.length > 0 && (
|
| 948 |
<div className="mb-2 flex flex-wrap gap-2 max-h-32 overflow-y-auto">
|
|
|
|
| 1009 |
>
|
| 1010 |
<div className="flex flex-col">
|
| 1011 |
<span className="font-medium">General</span>
|
| 1012 |
+
<span className="text-xs text-muted-foreground">Answer various questions (context required)</span>
|
|
|
|
|
|
|
| 1013 |
</div>
|
| 1014 |
</DropdownMenuItem>
|
| 1015 |
|
|
|
|
| 1184 |
<div className="border rounded-lg bg-muted/40 flex flex-col max-h-64">
|
| 1185 |
<div className="flex items-center justify-between p-4 sticky top-0 bg-muted/40 border-b z-10">
|
| 1186 |
<span className="text-sm font-medium">Preview</span>
|
| 1187 |
+
<Button variant="outline" size="sm" className="h-7 px-2 text-xs gap-1.5" onClick={handleCopyPreview} title="Copy preview">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1188 |
<Copy className="h-3 w-3" />
|
| 1189 |
Copy
|
| 1190 |
</Button>
|
|
|
|
| 1259 |
))}
|
| 1260 |
</SelectContent>
|
| 1261 |
</Select>
|
| 1262 |
+
<p className="text-xs text-muted-foreground">Sends this conversation to the selected workspace's Saved Files.</p>
|
|
|
|
|
|
|
| 1263 |
<Button onClick={handleShareSendToWorkspace} className="w-full">
|
| 1264 |
Send
|
| 1265 |
</Button>
|
|
|
|
| 1274 |
<AlertDialogHeader>
|
| 1275 |
<AlertDialogTitle>Delete File</AlertDialogTitle>
|
| 1276 |
<AlertDialogDescription>
|
| 1277 |
+
Are you sure you want to delete "{fileToDelete !== null ? uploadedFiles[fileToDelete]?.file.name : ""}"? This action cannot be undone.
|
|
|
|
|
|
|
| 1278 |
</AlertDialogDescription>
|
| 1279 |
</AlertDialogHeader>
|
| 1280 |
<AlertDialogFooter>
|
|
|
|
| 1300 |
<DialogHeader className="min-w-0 flex-shrink-0">
|
| 1301 |
<DialogTitle
|
| 1302 |
className="pr-8 break-words break-all overflow-wrap-anywhere leading-relaxed"
|
| 1303 |
+
style={{ wordBreak: "break-all", overflowWrap: "anywhere", maxWidth: "100%", lineHeight: "1.6" }}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1304 |
>
|
| 1305 |
{selectedFile?.file.name}
|
| 1306 |
</DialogTitle>
|
| 1307 |
+
<DialogDescription>File size: {selectedFile ? formatFileSize(selectedFile.file.size) : ""}</DialogDescription>
|
|
|
|
|
|
|
| 1308 |
</DialogHeader>
|
| 1309 |
<div className="flex-1 min-h-0 overflow-y-auto mt-4">{selectedFile && <FileViewerContent file={selectedFile.file} />}</div>
|
| 1310 |
</DialogContent>
|
|
|
|
| 1334 |
|
| 1335 |
<div className="space-y-1">
|
| 1336 |
<label className="text-xs text-muted-foreground">File Type</label>
|
| 1337 |
+
<Select value={pendingFile.type} onValueChange={(value) => handlePendingFileTypeChange(index, value as FileType)}>
|
|
|
|
|
|
|
|
|
|
| 1338 |
<SelectTrigger className="h-8 text-xs">
|
| 1339 |
<SelectValue />
|
| 1340 |
</SelectTrigger>
|