SarahXia0405 commited on
Commit
f36fc08
·
verified ·
1 Parent(s): cb11663

Update web/src/components/ChatArea.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/ChatArea.tsx +400 -325
web/src/components/ChatArea.tsx CHANGED
@@ -1,13 +1,12 @@
1
  // web/src/components/ChatArea.tsx
2
- import React, { useState, useRef, useEffect } from 'react';
3
- import { Button } from './ui/button';
4
- import { Textarea } from './ui/textarea';
5
- import { Input } from './ui/input';
6
- import { Label } from './ui/label';
7
  import {
8
  Send,
9
  ArrowDown,
10
- Trash2,
11
  Share2,
12
  Upload,
13
  X,
@@ -18,22 +17,37 @@ import {
18
  Bookmark,
19
  Plus,
20
  Download,
21
- Copy
22
- } from 'lucide-react';
23
- import { Message } from './Message';
24
- import { Badge } from './ui/badge';
25
- import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
26
- import type { Message as MessageType, LearningMode, UploadedFile, FileType, SpaceType, ChatMode, SavedChat, Workspace } from '../App';
27
- import { toast } from 'sonner';
28
- import { jsPDF } from 'jspdf';
 
 
 
 
 
 
 
 
29
  import {
30
  DropdownMenu,
31
  DropdownMenuContent,
32
  DropdownMenuItem,
33
  DropdownMenuTrigger,
34
- } from './ui/dropdown-menu';
35
- import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from './ui/dialog';
36
- import { Checkbox } from './ui/checkbox';
 
 
 
 
 
 
 
37
  import {
38
  AlertDialog,
39
  AlertDialogAction,
@@ -43,10 +57,10 @@ import {
43
  AlertDialogFooter,
44
  AlertDialogHeader,
45
  AlertDialogTitle,
46
- } from './ui/alert-dialog';
47
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
48
- import { SmartReview } from './SmartReview';
49
- import clareAvatar from '../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png';
50
 
51
  interface ChatAreaProps {
52
  messages: MessageType[];
@@ -78,7 +92,12 @@ interface ChatAreaProps {
78
  savedChats: SavedChat[];
79
  workspaces: Workspace[];
80
  currentWorkspaceId: string;
81
- onSaveFile?: (content: string, type: 'export' | 'summary', format?: 'pdf' | 'text', workspaceId?: string) => void;
 
 
 
 
 
82
  leftPanelVisible?: boolean;
83
  currentCourseId?: string;
84
  onCourseChange?: (courseId: string) => void;
@@ -124,80 +143,88 @@ export function ChatArea({
124
  availableCourses = [],
125
  showReviewBanner = false,
126
  }: ChatAreaProps) {
127
- const [input, setInput] = useState('');
128
  const [showScrollButton, setShowScrollButton] = useState(false);
129
  const [showTopBorder, setShowTopBorder] = useState(false);
130
  const [isDragging, setIsDragging] = useState(false);
 
131
  const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);
132
  const [showTypeDialog, setShowTypeDialog] = useState(false);
 
133
  const [showDeleteDialog, setShowDeleteDialog] = useState(false);
134
  const [fileToDelete, setFileToDelete] = useState<number | null>(null);
 
135
  const [selectedFile, setSelectedFile] = useState<{ file: File; index: number } | null>(null);
136
  const [showFileViewer, setShowFileViewer] = useState(false);
 
137
  const [showDownloadDialog, setShowDownloadDialog] = useState(false);
138
- const [downloadPreview, setDownloadPreview] = useState('');
139
- const [downloadTab, setDownloadTab] = useState<'chat' | 'summary'>('chat');
140
  const [downloadOptions, setDownloadOptions] = useState({ chat: true, summary: false });
141
- const [showShareDialog, setShowShareDialog] = useState(false);
142
- const [shareLink, setShareLink] = useState('');
143
- const [targetWorkspaceId, setTargetWorkspaceId] = useState<string>('');
144
 
145
- // Use availableCourses if provided, otherwise fallback to default
146
- const courses = availableCourses.length > 0 ? availableCourses : [
147
- { id: 'course1', name: 'Introduction to AI' },
148
- { id: 'course2', name: 'Machine Learning' },
149
- { id: 'course3', name: 'Data Structures' },
150
- { id: 'course4', name: 'Web Development' },
151
- ];
 
 
 
 
 
 
 
152
 
153
  const messagesEndRef = useRef<HTMLDivElement>(null);
154
  const scrollContainerRef = useRef<HTMLDivElement>(null);
155
  const fileInputRef = useRef<HTMLInputElement>(null);
 
156
  const isInitialMount = useRef(true);
157
  const previousMessagesLength = useRef(messages.length);
158
 
159
  const scrollToBottom = () => {
160
- messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
161
  };
162
 
163
  // Only auto-scroll when new messages are added (not on initial load)
164
  useEffect(() => {
165
- // Skip auto-scroll on initial mount
166
  if (isInitialMount.current) {
167
  isInitialMount.current = false;
168
  previousMessagesLength.current = messages.length;
169
- // Ensure we stay at top on initial load
 
170
  if (scrollContainerRef.current) {
171
  scrollContainerRef.current.scrollTop = 0;
172
  }
173
  return;
174
  }
175
 
176
- // Only scroll if new messages were added (length increased)
177
  if (messages.length > previousMessagesLength.current) {
178
  scrollToBottom();
179
  }
180
  previousMessagesLength.current = messages.length;
181
  }, [messages]);
182
 
 
183
  useEffect(() => {
184
  const handleScroll = () => {
185
- if (scrollContainerRef.current) {
186
- const { scrollTop, scrollHeight, clientHeight } = scrollContainerRef.current;
187
- const isAtBottom = scrollHeight - scrollTop - clientHeight < 100;
188
- setShowScrollButton(!isAtBottom);
189
- // Show border when content is scrolled below the fixed bar
190
- setShowTopBorder(scrollTop > 0);
191
- }
192
  };
193
 
194
  const container = scrollContainerRef.current;
195
- if (container) {
196
- // Check initial state
197
- handleScroll();
198
- container.addEventListener('scroll', handleScroll);
199
- return () => container.removeEventListener('scroll', handleScroll);
200
- }
201
  }, [messages]);
202
 
203
  // ✅ Prevent scroll chaining to left panel / outer containers
@@ -206,10 +233,8 @@ export function ChatArea({
206
  if (!el) return;
207
 
208
  const onWheel = (e: WheelEvent) => {
209
- // Always stop bubbling so left panel won't react
210
  e.stopPropagation();
211
 
212
- // Only preventDefault at boundaries to stop scroll chaining
213
  const atTop = el.scrollTop <= 0;
214
  const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 1;
215
 
@@ -227,68 +252,56 @@ export function ChatArea({
227
  if (!input.trim() || !isLoggedIn) return;
228
 
229
  onSendMessage(input);
230
- setInput('');
231
  };
232
 
233
  const handleKeyDown = (e: React.KeyboardEvent) => {
234
- if (e.key === 'Enter' && !e.shiftKey) {
235
  e.preventDefault();
236
  handleSubmit(e);
237
  }
238
  };
239
 
240
  const modeLabels: Record<LearningMode, string> = {
241
- general: 'General',
242
- concept: 'Concept Explainer',
243
- socratic: 'Socratic Tutor',
244
- exam: 'Exam Prep',
245
- assignment: 'Assignment Helper',
246
- summary: 'Quick Summary',
247
  };
248
 
249
- // Handle review topic button click
250
- const handleReviewTopic = (item: { title: string; previousQuestion: string; memoryRetention: number; schedule: string; status: string; weight: number; lastReviewed: string }) => {
 
 
 
 
 
 
 
 
251
  const userMessage = `Please help me review: ${item.title}`;
252
  const reviewData = `REVIEW_TOPIC:${item.title}|${item.previousQuestion}|${item.memoryRetention}|${item.schedule}|${item.status}|${item.weight}|${item.lastReviewed}`;
253
  (window as any).__lastReviewData = reviewData;
254
  onSendMessage(userMessage);
255
  };
256
 
257
- // Handle review all button click
258
  const handleReviewAll = () => {
259
- (window as any).__lastReviewData = 'REVIEW_ALL';
260
- onSendMessage('Please help me review all topics that need attention.');
261
- };
262
-
263
- const handleClearClick = () => {
264
- const isSavedChat = isCurrentChatSaved();
265
-
266
- if (isSavedChat) {
267
- onConfirmClear(false);
268
- return;
269
- }
270
-
271
- const hasUserMessages = messages.some(msg => msg.role === 'user');
272
- if (!hasUserMessages) {
273
- onClearConversation();
274
- return;
275
- }
276
-
277
- onClearConversation();
278
  };
279
 
280
  const buildPreviewContent = () => {
281
- if (!messages.length) return '';
282
- return messages
283
- .map((msg) => `${msg.role === 'user' ? 'You' : 'Clare'}: ${msg.content}`)
284
- .join('\n\n');
285
  };
286
 
287
  const buildSummaryContent = () => {
288
- if (!messages.length) return 'No messages to summarize.';
289
 
290
- const userMessages = messages.filter(msg => msg.role === 'user');
291
- const assistantMessages = messages.filter(msg => msg.role === 'assistant');
292
 
293
  let summary = `Chat Summary\n================\n\n`;
294
  summary += `Total Messages: ${messages.length}\n`;
@@ -298,14 +311,14 @@ export function ChatArea({
298
  summary += `Key Points:\n`;
299
  userMessages.slice(0, 3).forEach((msg, idx) => {
300
  const preview = msg.content.substring(0, 80);
301
- summary += `${idx + 1}. ${preview}${msg.content.length > 80 ? '...' : ''}\n`;
302
  });
303
 
304
  return summary;
305
  };
306
 
307
  const handleOpenDownloadDialog = () => {
308
- setDownloadTab('chat');
309
  setDownloadOptions({ chat: true, summary: false });
310
  setDownloadPreview(buildPreviewContent());
311
  setShowDownloadDialog(true);
@@ -314,66 +327,57 @@ export function ChatArea({
314
  const handleCopyPreview = async () => {
315
  try {
316
  await navigator.clipboard.writeText(downloadPreview);
317
- toast.success('Copied preview');
318
- } catch (e) {
319
- toast.error('Copy failed');
320
  }
321
  };
322
 
323
  const handleDownloadFile = async () => {
324
  try {
325
- let contentToPdf = '';
326
 
327
- if (downloadOptions.chat) {
328
- contentToPdf += buildPreviewContent();
329
- }
330
 
331
  if (downloadOptions.summary) {
332
- if (downloadOptions.chat) {
333
- contentToPdf += '\n\n================\n\n';
334
- }
335
  contentToPdf += buildSummaryContent();
336
  }
337
 
338
  if (!contentToPdf.trim()) {
339
- toast.error('Please select at least one option');
340
  return;
341
  }
342
 
343
- const pdf = new jsPDF({
344
- orientation: 'portrait',
345
- unit: 'mm',
346
- format: 'a4'
347
- });
348
 
349
- pdf.setFontSize(11);
350
  pdf.setFontSize(14);
351
- pdf.text('Chat Export', 10, 10);
352
  pdf.setFontSize(11);
353
 
354
  const pageHeight = pdf.internal.pageSize.getHeight();
355
  const margin = 10;
356
  const maxWidth = 190;
357
  const lineHeight = 5;
358
- let yPosition = 20;
359
 
360
  const lines = pdf.splitTextToSize(contentToPdf, maxWidth);
361
-
362
  lines.forEach((line: string) => {
363
- if (yPosition > pageHeight - margin) {
364
  pdf.addPage();
365
- yPosition = margin;
366
  }
367
- pdf.text(line, margin, yPosition);
368
- yPosition += lineHeight;
369
  });
370
 
371
- pdf.save('chat-export.pdf');
372
  setShowDownloadDialog(false);
373
- toast.success('PDF downloaded successfully');
374
  } catch (error) {
375
- console.error('PDF generation error:', error);
376
- toast.error('Failed to generate PDF');
 
377
  }
378
  };
379
 
@@ -381,12 +385,12 @@ export function ChatArea({
381
  const isCurrentChatSaved = (): boolean => {
382
  if (messages.length <= 1) return false;
383
 
384
- return savedChats.some(chat => {
385
  if (chat.chatMode !== chatMode) return false;
386
  if (chat.messages.length !== messages.length) return false;
387
 
388
- return chat.messages.every((savedMsg, index) => {
389
- const currentMsg = messages[index];
390
  return (
391
  savedMsg.id === currentMsg.id &&
392
  savedMsg.role === currentMsg.role &&
@@ -398,20 +402,19 @@ export function ChatArea({
398
 
399
  const handleSaveClick = () => {
400
  if (messages.length <= 1) {
401
- toast.info('No conversation to save');
402
  return;
403
  }
404
-
405
  onSaveChat();
406
  };
407
 
408
  const handleShareClick = () => {
409
  if (messages.length <= 1) {
410
- toast.info('No conversation to share');
411
  return;
412
  }
413
  const conversationText = buildPreviewContent();
414
- const blob = new Blob([conversationText], { type: 'text/plain' });
415
  const url = URL.createObjectURL(blob);
416
  setShareLink(url);
417
  setTargetWorkspaceId(currentWorkspaceId);
@@ -421,19 +424,38 @@ export function ChatArea({
421
  const handleCopyShareLink = async () => {
422
  try {
423
  await navigator.clipboard.writeText(shareLink);
424
- toast.success('Link copied');
425
  } catch {
426
- toast.error('Failed to copy link');
427
  }
428
  };
429
 
430
  const handleShareSendToWorkspace = () => {
431
  const content = buildPreviewContent();
432
- onSaveFile?.(content, 'export', 'text', targetWorkspaceId);
433
  setShowShareDialog(false);
434
- toast.success('Sent to workspace Saved Files');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
  };
436
 
 
437
  const handleDragOver = (e: React.DragEvent) => {
438
  e.preventDefault();
439
  e.stopPropagation();
@@ -456,24 +478,22 @@ export function ChatArea({
456
  const fileList = e.dataTransfer.files;
457
  const files: File[] = [];
458
  for (let i = 0; i < fileList.length; i++) {
459
- const file = fileList.item(i);
460
- if (file) {
461
- files.push(file);
462
- }
463
  }
464
 
465
  const validFiles = files.filter((file) => {
466
  const ext = file.name.toLowerCase();
467
- return ['.pdf', '.docx', '.pptx', '.jpg', '.jpeg', '.png', '.gif', '.webp', '.doc', '.ppt'].some((allowedExt) =>
468
- ext.endsWith(allowedExt)
469
  );
470
  });
471
 
472
  if (validFiles.length > 0) {
473
- setPendingFiles(validFiles.map(file => ({ file, type: 'other' as FileType })));
474
  setShowTypeDialog(true);
475
  } else {
476
- toast.error('Please upload .pdf, .docx, .pptx, or image files');
477
  }
478
  };
479
 
@@ -482,31 +502,36 @@ export function ChatArea({
482
  if (files.length > 0) {
483
  const validFiles = files.filter((file) => {
484
  const ext = file.name.toLowerCase();
485
- return ['.pdf', '.docx', '.pptx', '.jpg', '.jpeg', '.png', '.gif', '.webp', '.doc', '.ppt'].some((allowedExt) =>
486
- ext.endsWith(allowedExt)
487
  );
488
  });
 
489
  if (validFiles.length > 0) {
490
- setPendingFiles(validFiles.map(file => ({ file, type: 'other' as FileType })));
491
  setShowTypeDialog(true);
492
  } else {
493
- toast.error('Please upload .pdf, .docx, .pptx, or image files');
494
  }
495
  }
496
- e.target.value = '';
497
  };
498
 
499
  const handleConfirmUpload = () => {
500
- onFileUpload(pendingFiles.map(pf => pf.file));
501
  const startIndex = uploadedFiles.length;
 
 
502
  pendingFiles.forEach((pf, idx) => {
503
  setTimeout(() => {
504
  onFileTypeChange(startIndex + idx, pf.type);
505
  }, 0);
506
  });
 
 
507
  setPendingFiles([]);
508
  setShowTypeDialog(false);
509
- toast.success(`${pendingFiles.length} file(s) uploaded successfully`);
510
  };
511
 
512
  const handleCancelUpload = () => {
@@ -515,31 +540,33 @@ export function ChatArea({
515
  };
516
 
517
  const handlePendingFileTypeChange = (index: number, type: FileType) => {
518
- setPendingFiles(prev => prev.map((pf, i) => (i === index ? { ...pf, type } : pf)));
519
  };
520
 
 
521
  const getFileIcon = (filename: string) => {
522
  const ext = filename.toLowerCase();
523
- if (ext.endsWith('.pdf')) return FileText;
524
- if (ext.endsWith('.docx') || ext.endsWith('.doc')) return File;
525
- if (ext.endsWith('.pptx') || ext.endsWith('.ppt')) return Presentation;
526
- if (['.jpg', '.jpeg', '.png', '.gif', '.webp'].some(e => ext.endsWith(e))) return ImageIcon;
527
  return File;
528
  };
529
 
530
  const getFileTypeInfo = (filename: string) => {
531
  const ext = filename.toLowerCase();
532
- if (ext.endsWith('.pdf')) return { bgColor: 'bg-red-500', type: 'PDF' };
533
- if (ext.endsWith('.docx') || ext.endsWith('.doc')) return { bgColor: 'bg-blue-500', type: 'Document' };
534
- if (ext.endsWith('.pptx') || ext.endsWith('.ppt')) return { bgColor: 'bg-orange-500', type: 'Presentation' };
535
- if (['.jpg', '.jpeg', '.png', '.gif', '.webp'].some(e => ext.endsWith(e))) return { bgColor: 'bg-green-500', type: 'Image' };
536
- return { bgColor: 'bg-gray-500', type: 'File' };
 
537
  };
538
 
539
  const formatFileSize = (bytes: number) => {
540
- if (bytes < 1024) return bytes + ' B';
541
- if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
542
- return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
543
  };
544
 
545
  const FileThumbnail = ({
@@ -548,7 +575,7 @@ export function ChatArea({
548
  fileInfo,
549
  isImage,
550
  onPreview,
551
- onRemove
552
  }: {
553
  file: File;
554
  Icon: React.ComponentType<{ className?: string }>;
@@ -561,21 +588,22 @@ export function ChatArea({
561
  const [imageLoading, setImageLoading] = useState(true);
562
 
563
  useEffect(() => {
564
- if (isImage) {
565
- setImageLoading(true);
566
- const reader = new FileReader();
567
- reader.onload = (e) => {
568
- setImagePreview(e.target?.result as string);
569
- setImageLoading(false);
570
- };
571
- reader.onerror = () => {
572
- setImageLoading(false);
573
- };
574
- reader.readAsDataURL(file);
575
- } else {
576
  setImagePreview(null);
577
  setImageLoading(false);
 
578
  }
 
 
 
 
 
 
 
 
 
 
 
579
  }, [file, isImage]);
580
 
581
  if (isImage) {
@@ -583,7 +611,7 @@ export function ChatArea({
583
  <div
584
  className="relative cursor-pointer w-16 h-16 flex-shrink-0"
585
  onClick={onPreview}
586
- style={{ width: '64px', height: '64px', flexShrink: 0 }}
587
  >
588
  <div className="w-full h-full relative bg-card border border-border rounded-lg hover:border-primary/50 transition-colors">
589
  <div className="w-full h-full overflow-hidden rounded-lg absolute inset-0">
@@ -597,7 +625,7 @@ export function ChatArea({
597
  alt={file.name}
598
  className="w-full h-full object-cover"
599
  onError={(e) => {
600
- e.currentTarget.style.display = 'none';
601
  setImageLoading(false);
602
  }}
603
  />
@@ -607,6 +635,7 @@ export function ChatArea({
607
  </div>
608
  )}
609
  </div>
 
610
  <button
611
  type="button"
612
  className="absolute top-1 right-1 h-4 w-4 rounded-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow-sm flex items-center justify-center cursor-pointer"
@@ -614,15 +643,9 @@ export function ChatArea({
614
  e.stopPropagation();
615
  onRemove(e);
616
  }}
617
- style={{ zIndex: 100, position: 'absolute', top: '4px', right: '4px' }}
618
  >
619
- <X
620
- className="h-2.5 w-2.5"
621
- style={{
622
- color: 'rgb(0, 0, 0)',
623
- strokeWidth: 2
624
- }}
625
- />
626
  </button>
627
  </div>
628
  </div>
@@ -630,11 +653,7 @@ export function ChatArea({
630
  }
631
 
632
  return (
633
- <div
634
- className="relative cursor-pointer"
635
- onClick={onPreview}
636
- style={{ width: '240px', flexShrink: 0 }}
637
- >
638
  <div className="h-16 w-full relative flex items-center px-3 bg-card border border-border rounded-lg hover:border-primary/50 transition-colors">
639
  <div className={`${fileInfo.bgColor} flex items-center justify-center w-10 h-10 rounded shrink-0`}>
640
  <Icon className="h-5 w-5 text-white" />
@@ -644,10 +663,9 @@ export function ChatArea({
644
  <p className="text-xs text-foreground truncate" title={file.name}>
645
  {file.name}
646
  </p>
647
- <p className="text-[10px] text-muted-foreground mt-0.5 truncate">
648
- {fileInfo.type}
649
- </p>
650
  </div>
 
651
  <button
652
  type="button"
653
  className="absolute top-1 right-1 h-4 w-4 rounded-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow-sm flex items-center justify-center cursor-pointer z-10"
@@ -655,15 +673,9 @@ export function ChatArea({
655
  e.stopPropagation();
656
  onRemove(e);
657
  }}
658
- style={{ position: 'absolute', top: '4px', right: '4px', zIndex: 10 }}
659
  >
660
- <X
661
- className="h-2.5 w-2.5"
662
- style={{
663
- color: 'rgb(0, 0, 0)',
664
- strokeWidth: 2
665
- }}
666
- />
667
  </button>
668
  </div>
669
  </div>
@@ -671,7 +683,7 @@ export function ChatArea({
671
  };
672
 
673
  const FileViewerContent = ({ file }: { file: File }) => {
674
- const [content, setContent] = useState<string>('');
675
  const [loading, setLoading] = useState(true);
676
  const [error, setError] = useState<string | null>(null);
677
 
@@ -683,34 +695,38 @@ export function ChatArea({
683
 
684
  const ext = file.name.toLowerCase();
685
 
686
- if (['.jpg', '.jpeg', '.png', '.gif', '.webp'].some(e => ext.endsWith(e))) {
687
  const reader = new FileReader();
688
  reader.onload = (e) => {
689
  setContent(e.target?.result as string);
690
  setLoading(false);
691
  };
692
  reader.onerror = () => {
693
- setError('Failed to load image');
694
  setLoading(false);
695
  };
696
  reader.readAsDataURL(file);
697
- } else if (ext.endsWith('.pdf')) {
698
- setContent('PDF files cannot be previewed directly. Please download the file to view it.');
 
 
 
699
  setLoading(false);
700
- } else {
701
- const reader = new FileReader();
702
- reader.onload = (e) => {
703
- setContent(e.target?.result as string);
704
- setLoading(false);
705
- };
706
- reader.onerror = () => {
707
- setError('Failed to load file');
708
- setLoading(false);
709
- };
710
- reader.readAsText(file);
711
  }
712
- } catch (err) {
713
- setError('Failed to load file');
 
 
 
 
 
 
 
 
 
 
 
714
  setLoading(false);
715
  }
716
  };
@@ -722,7 +738,7 @@ export function ChatArea({
722
  if (error) return <div className="text-center py-8 text-destructive">{error}</div>;
723
 
724
  const ext = file.name.toLowerCase();
725
- const isImage = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].some(e => ext.endsWith(e));
726
 
727
  if (isImage) {
728
  return (
@@ -740,32 +756,27 @@ export function ChatArea({
740
  };
741
 
742
  return (
743
- <div className="flex flex-col h-full min-h-0 overflow-hidden" style={{ overscrollBehavior: 'none' }}>
744
- <div className="flex-1 relative min-h-0 flex flex-col overflow-hidden" style={{ overscrollBehavior: 'none' }}>
745
  {/* ✅ Messages Area is the ONLY scroll container */}
746
  <div
747
  ref={scrollContainerRef}
748
  className="flex-1 min-h-0 overflow-y-auto"
749
- style={{
750
- overscrollBehavior: 'contain',
751
- }}
752
  >
753
  {/* Top Bar - Sticky */}
754
  <div
755
- className={`sticky top-0 flex items-center justify-between px-4 z-20 bg-card ${showTopBorder ? 'border-b border-border' : ''}`}
756
- style={{
757
- height: '4.5rem',
758
- margin: 0,
759
- padding: '1rem 1rem',
760
- boxSizing: 'border-box',
761
- }}
762
  >
763
  {/* Course Selector - Left */}
764
  <div className="flex-shrink-0">
765
  {(() => {
766
- const current = workspaces.find(w => w.id === currentWorkspaceId);
767
- if (current?.type === 'group') {
768
- if (current.category === 'course' && current.courseName) {
769
  return (
770
  <div className="h-9 px-3 inline-flex items-center rounded-md border font-semibold">
771
  {current.courseName}
@@ -774,8 +785,12 @@ export function ChatArea({
774
  }
775
  return null;
776
  }
 
777
  return (
778
- <Select value={currentCourseId || 'course1'} onValueChange={(val) => onCourseChange && onCourseChange(val)}>
 
 
 
779
  <SelectTrigger className="w-[200px] h-9 font-semibold">
780
  <SelectValue placeholder="Select course" />
781
  </SelectTrigger>
@@ -800,21 +815,25 @@ export function ChatArea({
800
  orientation="horizontal"
801
  >
802
  <TabsList className="inline-flex h-8 items-center justify-center rounded-xl bg-muted p-1 text-muted-foreground">
803
- <TabsTrigger value="ask" className="w-[140px] px-3 text-sm">Ask</TabsTrigger>
 
 
804
  <TabsTrigger value="review" className="w-[140px] px-3 text-sm relative">
805
  Review
806
  <span
807
  className="absolute top-0 right-0 bg-red-500 rounded-full border-2"
808
  style={{
809
- width: '10px',
810
- height: '10px',
811
- transform: 'translate(25%, -25%)',
812
  zIndex: 10,
813
- borderColor: 'var(--muted)'
814
  }}
815
  />
816
  </TabsTrigger>
817
- <TabsTrigger value="quiz" className="w-[140px] px-3 text-sm">Quiz</TabsTrigger>
 
 
818
  </TabsList>
819
  </Tabs>
820
  </div>
@@ -826,11 +845,12 @@ export function ChatArea({
826
  size="icon"
827
  onClick={handleSaveClick}
828
  disabled={!isLoggedIn}
829
- className={`h-8 w-8 rounded-md hover:bg-muted/50 ${isCurrentChatSaved() ? 'text-primary' : ''}`}
830
- title={isCurrentChatSaved() ? 'Unsave' : 'Save'}
831
  >
832
- <Bookmark className={`h-4 w-4 ${isCurrentChatSaved() ? 'fill-primary text-primary' : ''}`} />
833
  </Button>
 
834
  <Button
835
  variant="ghost"
836
  size="icon"
@@ -841,6 +861,7 @@ export function ChatArea({
841
  >
842
  <Download className="h-4 w-4" />
843
  </Button>
 
844
  <Button
845
  variant="ghost"
846
  size="icon"
@@ -851,6 +872,7 @@ export function ChatArea({
851
  >
852
  <Share2 className="h-4 w-4" />
853
  </Button>
 
854
  <Button
855
  variant="outline"
856
  onClick={handleClearClick}
@@ -865,41 +887,34 @@ export function ChatArea({
865
  </div>
866
 
867
  {/* Messages Content */}
868
- <div
869
- className="py-6"
870
- style={{
871
- paddingBottom: '12rem'
872
- }}
873
- >
874
  <div className="w-full space-y-6 max-w-4xl mx-auto">
875
  {messages.map((message) => (
876
  <React.Fragment key={message.id}>
877
  <Message
878
  message={message}
879
- showSenderInfo={spaceType === 'group'}
880
  isFirstGreeting={
881
- (message.id === '1' || message.id === 'review-1' || message.id === 'quiz-1') &&
882
- message.role === 'assistant'
883
  }
884
  showNextButton={message.showNextButton && !isAppTyping}
885
  onNextQuestion={onNextQuestion}
886
  chatMode={chatMode}
887
  />
888
 
889
- {chatMode === 'review' &&
890
- message.id === 'review-1' &&
891
- message.role === 'assistant' && (
892
- <div className="flex gap-2 justify-start px-4">
893
- <div className="w-10 h-10 flex-shrink-0"></div>
894
- <div className="w-full" style={{ maxWidth: 'min(770px, calc(100% - 2rem))' }}>
895
- <SmartReview onReviewTopic={handleReviewTopic} onReviewAll={handleReviewAll} />
896
- </div>
897
  </div>
898
- )}
 
899
 
900
- {chatMode === 'quiz' &&
901
- message.id === 'quiz-1' &&
902
- message.role === 'assistant' &&
903
  quizState.currentQuestion === 0 &&
904
  !quizState.waitingForAnswer &&
905
  !isAppTyping && (
@@ -919,9 +934,18 @@ export function ChatArea({
919
  </div>
920
  <div className="bg-muted rounded-2xl px-4 py-3">
921
  <div className="flex gap-1">
922
- <div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: '0ms' }} />
923
- <div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: '150ms' }} />
924
- <div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: '300ms' }} />
 
 
 
 
 
 
 
 
 
925
  </div>
926
  </div>
927
  </div>
@@ -937,9 +961,9 @@ export function ChatArea({
937
  <div
938
  className="fixed z-30 flex justify-center pointer-events-none"
939
  style={{
940
- bottom: '120px',
941
- left: leftPanelVisible ? '320px' : '0px',
942
- right: '0px'
943
  }}
944
  >
945
  <Button
@@ -958,8 +982,8 @@ export function ChatArea({
958
  <div
959
  className="fixed bottom-0 bg-background/95 backdrop-blur-sm z-10"
960
  style={{
961
- left: leftPanelVisible ? '320px' : '0px',
962
- right: '0px'
963
  }}
964
  >
965
  <div className="max-w-4xl mx-auto px-4 py-4">
@@ -968,7 +992,7 @@ export function ChatArea({
968
  {uploadedFiles.map((uploadedFile, index) => {
969
  const Icon = getFileIcon(uploadedFile.file.name);
970
  const fileInfo = getFileTypeInfo(uploadedFile.file.name);
971
- const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp'].some(ext =>
972
  uploadedFile.file.name.toLowerCase().endsWith(`.${ext}`)
973
  );
974
 
@@ -1000,12 +1024,12 @@ export function ChatArea({
1000
  onDragOver={handleDragOver}
1001
  onDragLeave={handleDragLeave}
1002
  onDrop={handleDrop}
1003
- className={isDragging ? 'opacity-75' : ''}
1004
  >
1005
  <div className="relative">
1006
- {/* Mode Selector and Upload Button */}
1007
  <div className="absolute bottom-3 left-2 flex items-center gap-1 z-10">
1008
- {chatMode === 'ask' && (
1009
  <DropdownMenu>
1010
  <DropdownMenuTrigger asChild>
1011
  <Button
@@ -1022,37 +1046,62 @@ export function ChatArea({
1022
  </Button>
1023
  </DropdownMenuTrigger>
1024
  <DropdownMenuContent align="start" className="w-56">
1025
- <DropdownMenuItem onClick={() => onLearningModeChange('general')} className={learningMode === 'general' ? 'bg-accent' : ''}>
 
 
 
1026
  <div className="flex flex-col">
1027
  <span className="font-medium">General</span>
1028
- <span className="text-xs text-muted-foreground">Answer various questions (context required)</span>
 
 
1029
  </div>
1030
  </DropdownMenuItem>
1031
- <DropdownMenuItem onClick={() => onLearningModeChange('concept')} className={learningMode === 'concept' ? 'bg-accent' : ''}>
 
 
 
 
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
- <DropdownMenuItem onClick={() => onLearningModeChange('socratic')} className={learningMode === 'socratic' ? 'bg-accent' : ''}>
 
 
 
 
1038
  <div className="flex flex-col">
1039
  <span className="font-medium">Socratic Tutor</span>
1040
  <span className="text-xs text-muted-foreground">Learn through guided questions</span>
1041
  </div>
1042
  </DropdownMenuItem>
1043
- <DropdownMenuItem onClick={() => onLearningModeChange('exam')} className={learningMode === 'exam' ? 'bg-accent' : ''}>
 
 
 
 
1044
  <div className="flex flex-col">
1045
  <span className="font-medium">Exam Prep</span>
1046
  <span className="text-xs text-muted-foreground">Practice with quiz questions</span>
1047
  </div>
1048
  </DropdownMenuItem>
1049
- <DropdownMenuItem onClick={() => onLearningModeChange('assignment')} className={learningMode === 'assignment' ? 'bg-accent' : ''}>
 
 
 
 
1050
  <div className="flex flex-col">
1051
  <span className="font-medium">Assignment Helper</span>
1052
  <span className="text-xs text-muted-foreground">Get help with assignments</span>
1053
  </div>
1054
  </DropdownMenuItem>
1055
- <DropdownMenuItem onClick={() => onLearningModeChange('summary')} className={learningMode === 'summary' ? 'bg-accent' : ''}>
 
 
 
 
1056
  <div className="flex flex-col">
1057
  <span className="font-medium">Quick Summary</span>
1058
  <span className="text-xs text-muted-foreground">Get concise summaries</span>
@@ -1066,7 +1115,7 @@ export function ChatArea({
1066
  type="button"
1067
  size="icon"
1068
  variant="ghost"
1069
- disabled={!isLoggedIn || (chatMode === 'quiz' && !quizState.waitingForAnswer)}
1070
  className="h-8 w-8 hover:bg-muted/50"
1071
  onClick={() => fileInputRef.current?.click()}
1072
  title="Upload files"
@@ -1082,29 +1131,26 @@ export function ChatArea({
1082
  placeholder={
1083
  !isLoggedIn
1084
  ? "Please log in on the right to start chatting..."
1085
- : chatMode === 'quiz'
1086
- ? quizState.waitingForAnswer
1087
- ? "Type your answer here..."
1088
- : quizState.currentQuestion > 0
1089
- ? "Click 'Next Question' to continue..."
1090
- : "Click 'Start Quiz' to begin..."
1091
- : spaceType === 'group'
1092
- ? "Type a message or drag files here... (mention @Clare to get AI assistance)"
1093
- : learningMode === 'general'
1094
- ? "Ask me anything! Please provide context about your question..."
1095
- : "Ask Clare anything about the course or drag files here..."
1096
  }
1097
- disabled={!isLoggedIn || (chatMode === 'quiz' && !quizState.waitingForAnswer)}
1098
- className={`min-h-[80px] pl-4 pr-20 resize-none bg-background border-2 ${isDragging ? 'border-primary border-dashed' : 'border-border'}`}
 
 
1099
  />
1100
 
1101
  <div className="absolute bottom-2 right-2 flex gap-1">
1102
- <Button
1103
- type="submit"
1104
- size="icon"
1105
- disabled={!input.trim() || !isLoggedIn}
1106
- className="h-8 w-8 rounded-full"
1107
- >
1108
  <Send className="h-4 w-4" />
1109
  </Button>
1110
  </div>
@@ -1132,6 +1178,7 @@ export function ChatArea({
1132
  <AlertDialogDescription>
1133
  Would you like to save the current chat before starting a new conversation?
1134
  </AlertDialogDescription>
 
1135
  <Button
1136
  variant="ghost"
1137
  size="icon"
@@ -1142,6 +1189,7 @@ export function ChatArea({
1142
  <span className="sr-only">Close</span>
1143
  </Button>
1144
  </AlertDialogHeader>
 
1145
  <AlertDialogFooter className="flex-col sm:flex-row gap-2 sm:justify-end">
1146
  <Button variant="outline" onClick={() => onConfirmClear(false)} className="sm:flex-1 sm:max-w-[200px]">
1147
  Start New (Don't Save)
@@ -1164,9 +1212,10 @@ export function ChatArea({
1164
  <Tabs
1165
  value={downloadTab}
1166
  onValueChange={(value) => {
1167
- setDownloadTab(value as 'chat' | 'summary');
1168
- setDownloadPreview(value === 'chat' ? buildPreviewContent() : buildSummaryContent());
1169
- if (value === 'summary') setDownloadOptions({ chat: false, summary: true });
 
1170
  else setDownloadOptions({ chat: true, summary: false });
1171
  }}
1172
  className="w-full"
@@ -1180,7 +1229,13 @@ export function ChatArea({
1180
  <div className="border rounded-lg bg-muted/40 flex flex-col max-h-64">
1181
  <div className="flex items-center justify-between p-4 sticky top-0 bg-muted/40 border-b z-10">
1182
  <span className="text-sm font-medium">Preview</span>
1183
- <Button variant="outline" size="sm" className="h-7 px-2 text-xs gap-1.5" onClick={handleCopyPreview} title="Copy preview">
 
 
 
 
 
 
1184
  <Copy className="h-3 w-3" />
1185
  Copy
1186
  </Button>
@@ -1197,7 +1252,9 @@ export function ChatArea({
1197
  checked={downloadOptions.chat}
1198
  onCheckedChange={(checked) => setDownloadOptions({ ...downloadOptions, chat: checked === true })}
1199
  />
1200
- <label htmlFor="download-chat" className="text-sm font-medium cursor-pointer">Download chat</label>
 
 
1201
  </div>
1202
  <div className="flex items-center space-x-2">
1203
  <Checkbox
@@ -1205,12 +1262,16 @@ export function ChatArea({
1205
  checked={downloadOptions.summary}
1206
  onCheckedChange={(checked) => setDownloadOptions({ ...downloadOptions, summary: checked === true })}
1207
  />
1208
- <label htmlFor="download-summary" className="text-sm font-medium cursor-pointer">Download summary</label>
 
 
1209
  </div>
1210
  </div>
1211
 
1212
  <DialogFooter>
1213
- <Button variant="outline" onClick={() => setShowDownloadDialog(false)}>Cancel</Button>
 
 
1214
  <Button onClick={handleDownloadFile}>Download</Button>
1215
  </DialogFooter>
1216
  </DialogContent>
@@ -1228,10 +1289,13 @@ export function ChatArea({
1228
  <Label>Copy Link</Label>
1229
  <div className="flex gap-2 items-center">
1230
  <Input value={shareLink} readOnly className="flex-1" />
1231
- <Button variant="secondary" onClick={handleCopyShareLink}>Copy</Button>
 
 
1232
  </div>
1233
  <p className="text-xs text-muted-foreground">Temporary link valid for this session.</p>
1234
  </div>
 
1235
  <div className="space-y-2">
1236
  <Label>Send to Workspace</Label>
1237
  <Select value={targetWorkspaceId} onValueChange={setTargetWorkspaceId}>
@@ -1239,13 +1303,19 @@ export function ChatArea({
1239
  <SelectValue placeholder="Choose a workspace" />
1240
  </SelectTrigger>
1241
  <SelectContent>
1242
- {workspaces.map(w => (
1243
- <SelectItem key={w.id} value={w.id}>{w.name}</SelectItem>
 
 
1244
  ))}
1245
  </SelectContent>
1246
  </Select>
1247
- <p className="text-xs text-muted-foreground">Sends this conversation to the selected workspace's Saved Files.</p>
1248
- <Button onClick={handleShareSendToWorkspace} className="w-full">Send</Button>
 
 
 
 
1249
  </div>
1250
  </div>
1251
  </DialogContent>
@@ -1257,7 +1327,9 @@ export function ChatArea({
1257
  <AlertDialogHeader>
1258
  <AlertDialogTitle>Delete File</AlertDialogTitle>
1259
  <AlertDialogDescription>
1260
- Are you sure you want to delete "{fileToDelete !== null ? uploadedFiles[fileToDelete]?.file.name : ''}"? This action cannot be undone.
 
 
1261
  </AlertDialogDescription>
1262
  </AlertDialogHeader>
1263
  <AlertDialogFooter>
@@ -1284,21 +1356,19 @@ export function ChatArea({
1284
  <DialogTitle
1285
  className="pr-8 break-words break-all overflow-wrap-anywhere leading-relaxed"
1286
  style={{
1287
- wordBreak: 'break-all',
1288
- overflowWrap: 'anywhere',
1289
- maxWidth: '100%',
1290
- lineHeight: '1.6'
1291
  }}
1292
  >
1293
  {selectedFile?.file.name}
1294
  </DialogTitle>
1295
  <DialogDescription>
1296
- File size: {selectedFile ? formatFileSize(selectedFile.file.size) : ''}
1297
  </DialogDescription>
1298
  </DialogHeader>
1299
- <div className="flex-1 min-h-0 overflow-y-auto mt-4">
1300
- {selectedFile && <FileViewerContent file={selectedFile.file} />}
1301
- </div>
1302
  </DialogContent>
1303
  </Dialog>
1304
 
@@ -1310,6 +1380,7 @@ export function ChatArea({
1310
  <DialogTitle>Select File Types</DialogTitle>
1311
  <DialogDescription>Please select the type for each file you are uploading.</DialogDescription>
1312
  </DialogHeader>
 
1313
  <div className="space-y-3 max-h-64 overflow-y-auto">
1314
  {pendingFiles.map((pendingFile, index) => {
1315
  const Icon = getFileIcon(pendingFile.file.name);
@@ -1322,6 +1393,7 @@ export function ChatArea({
1322
  <p className="text-xs text-muted-foreground">{formatFileSize(pendingFile.file.size)}</p>
1323
  </div>
1324
  </div>
 
1325
  <div className="space-y-1">
1326
  <label className="text-xs text-muted-foreground">File Type</label>
1327
  <Select
@@ -1343,8 +1415,11 @@ export function ChatArea({
1343
  );
1344
  })}
1345
  </div>
 
1346
  <DialogFooter>
1347
- <Button variant="outline" onClick={handleCancelUpload}>Cancel</Button>
 
 
1348
  <Button onClick={handleConfirmUpload}>Upload</Button>
1349
  </DialogFooter>
1350
  </DialogContent>
 
1
  // web/src/components/ChatArea.tsx
2
+ import React, { useState, useRef, useEffect } from "react";
3
+ import { Button } from "./ui/button";
4
+ import { Textarea } from "./ui/textarea";
5
+ import { Input } from "./ui/input";
6
+ import { Label } from "./ui/label";
7
  import {
8
  Send,
9
  ArrowDown,
 
10
  Share2,
11
  Upload,
12
  X,
 
17
  Bookmark,
18
  Plus,
19
  Download,
20
+ Copy,
21
+ } from "lucide-react";
22
+ import { Message } from "./Message";
23
+ import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
24
+ import type {
25
+ Message as MessageType,
26
+ LearningMode,
27
+ UploadedFile,
28
+ FileType,
29
+ SpaceType,
30
+ ChatMode,
31
+ SavedChat,
32
+ Workspace,
33
+ } from "../App";
34
+ import { toast } from "sonner";
35
+ import { jsPDF } from "jspdf";
36
  import {
37
  DropdownMenu,
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,
53
  AlertDialogAction,
 
57
  AlertDialogFooter,
58
  AlertDialogHeader,
59
  AlertDialogTitle,
60
+ } from "./ui/alert-dialog";
61
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
62
+ import { SmartReview } from "./SmartReview";
63
+ import clareAvatar from "../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png";
64
 
65
  interface ChatAreaProps {
66
  messages: MessageType[];
 
92
  savedChats: SavedChat[];
93
  workspaces: Workspace[];
94
  currentWorkspaceId: string;
95
+ onSaveFile?: (
96
+ content: string,
97
+ type: "export" | "summary",
98
+ format?: "pdf" | "text",
99
+ workspaceId?: string
100
+ ) => void;
101
  leftPanelVisible?: boolean;
102
  currentCourseId?: string;
103
  onCourseChange?: (courseId: string) => void;
 
143
  availableCourses = [],
144
  showReviewBanner = false,
145
  }: ChatAreaProps) {
146
+ const [input, setInput] = useState("");
147
  const [showScrollButton, setShowScrollButton] = useState(false);
148
  const [showTopBorder, setShowTopBorder] = useState(false);
149
  const [isDragging, setIsDragging] = useState(false);
150
+
151
  const [pendingFiles, setPendingFiles] = useState<PendingFile[]>([]);
152
  const [showTypeDialog, setShowTypeDialog] = useState(false);
153
+
154
  const [showDeleteDialog, setShowDeleteDialog] = useState(false);
155
  const [fileToDelete, setFileToDelete] = useState<number | null>(null);
156
+
157
  const [selectedFile, setSelectedFile] = useState<{ file: File; index: number } | null>(null);
158
  const [showFileViewer, setShowFileViewer] = useState(false);
159
+
160
  const [showDownloadDialog, setShowDownloadDialog] = useState(false);
161
+ const [downloadPreview, setDownloadPreview] = useState("");
162
+ const [downloadTab, setDownloadTab] = useState<"chat" | "summary">("chat");
163
  const [downloadOptions, setDownloadOptions] = useState({ chat: true, summary: false });
 
 
 
164
 
165
+ const [showShareDialog, setShowShareDialog] = useState(false);
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
173
+ : [
174
+ { id: "course1", name: "Introduction to AI" },
175
+ { id: "course2", name: "Machine Learning" },
176
+ { id: "course3", name: "Data Structures" },
177
+ { id: "course4", name: "Web Development" },
178
+ ];
179
 
180
  const messagesEndRef = useRef<HTMLDivElement>(null);
181
  const scrollContainerRef = useRef<HTMLDivElement>(null);
182
  const fileInputRef = useRef<HTMLInputElement>(null);
183
+
184
  const isInitialMount = useRef(true);
185
  const previousMessagesLength = useRef(messages.length);
186
 
187
  const scrollToBottom = () => {
188
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
189
  };
190
 
191
  // Only auto-scroll when new messages are added (not on initial load)
192
  useEffect(() => {
 
193
  if (isInitialMount.current) {
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
  }
201
  return;
202
  }
203
 
 
204
  if (messages.length > previousMessagesLength.current) {
205
  scrollToBottom();
206
  }
207
  previousMessagesLength.current = messages.length;
208
  }, [messages]);
209
 
210
+ // Scroll button + top border
211
  useEffect(() => {
212
  const handleScroll = () => {
213
+ const el = scrollContainerRef.current;
214
+ if (!el) return;
215
+
216
+ const { scrollTop, scrollHeight, clientHeight } = el;
217
+ const isAtBottom = scrollHeight - scrollTop - clientHeight < 100;
218
+ setShowScrollButton(!isAtBottom);
219
+ setShowTopBorder(scrollTop > 0);
220
  };
221
 
222
  const container = scrollContainerRef.current;
223
+ if (!container) return;
224
+
225
+ handleScroll();
226
+ container.addEventListener("scroll", handleScroll);
227
+ return () => container.removeEventListener("scroll", handleScroll);
 
228
  }, [messages]);
229
 
230
  // ✅ Prevent scroll chaining to left panel / outer containers
 
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
 
 
252
  if (!input.trim() || !isLoggedIn) return;
253
 
254
  onSendMessage(input);
255
+ setInput("");
256
  };
257
 
258
  const handleKeyDown = (e: React.KeyboardEvent) => {
259
+ if (e.key === "Enter" && !e.shiftKey) {
260
  e.preventDefault();
261
  handleSubmit(e);
262
  }
263
  };
264
 
265
  const modeLabels: Record<LearningMode, string> = {
266
+ general: "General",
267
+ concept: "Concept Explainer",
268
+ socratic: "Socratic Tutor",
269
+ exam: "Exam Prep",
270
+ assignment: "Assignment Helper",
271
+ summary: "Quick Summary",
272
  };
273
 
274
+ // Review topic click
275
+ const handleReviewTopic = (item: {
276
+ title: string;
277
+ previousQuestion: string;
278
+ memoryRetention: number;
279
+ schedule: string;
280
+ status: string;
281
+ weight: number;
282
+ lastReviewed: string;
283
+ }) => {
284
  const userMessage = `Please help me review: ${item.title}`;
285
  const reviewData = `REVIEW_TOPIC:${item.title}|${item.previousQuestion}|${item.memoryRetention}|${item.schedule}|${item.status}|${item.weight}|${item.lastReviewed}`;
286
  (window as any).__lastReviewData = reviewData;
287
  onSendMessage(userMessage);
288
  };
289
 
 
290
  const handleReviewAll = () => {
291
+ (window as any).__lastReviewData = "REVIEW_ALL";
292
+ onSendMessage("Please help me review all topics that need attention.");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  };
294
 
295
  const buildPreviewContent = () => {
296
+ if (!messages.length) return "";
297
+ return messages.map((msg) => `${msg.role === "user" ? "You" : "Clare"}: ${msg.content}`).join("\n\n");
 
 
298
  };
299
 
300
  const buildSummaryContent = () => {
301
+ if (!messages.length) return "No messages to summarize.";
302
 
303
+ const userMessages = messages.filter((msg) => msg.role === "user");
304
+ const assistantMessages = messages.filter((msg) => msg.role === "assistant");
305
 
306
  let summary = `Chat Summary\n================\n\n`;
307
  summary += `Total Messages: ${messages.length}\n`;
 
311
  summary += `Key Points:\n`;
312
  userMessages.slice(0, 3).forEach((msg, idx) => {
313
  const preview = msg.content.substring(0, 80);
314
+ summary += `${idx + 1}. ${preview}${msg.content.length > 80 ? "..." : ""}\n`;
315
  });
316
 
317
  return summary;
318
  };
319
 
320
  const handleOpenDownloadDialog = () => {
321
+ setDownloadTab("chat");
322
  setDownloadOptions({ chat: true, summary: false });
323
  setDownloadPreview(buildPreviewContent());
324
  setShowDownloadDialog(true);
 
327
  const handleCopyPreview = async () => {
328
  try {
329
  await navigator.clipboard.writeText(downloadPreview);
330
+ toast.success("Copied preview");
331
+ } catch {
332
+ toast.error("Copy failed");
333
  }
334
  };
335
 
336
  const handleDownloadFile = async () => {
337
  try {
338
+ let contentToPdf = "";
339
 
340
+ if (downloadOptions.chat) contentToPdf += buildPreviewContent();
 
 
341
 
342
  if (downloadOptions.summary) {
343
+ if (downloadOptions.chat) contentToPdf += "\n\n================\n\n";
 
 
344
  contentToPdf += buildSummaryContent();
345
  }
346
 
347
  if (!contentToPdf.trim()) {
348
+ toast.error("Please select at least one option");
349
  return;
350
  }
351
 
352
+ const pdf = new jsPDF({ orientation: "portrait", unit: "mm", format: "a4" });
 
 
 
 
353
 
 
354
  pdf.setFontSize(14);
355
+ pdf.text("Chat Export", 10, 10);
356
  pdf.setFontSize(11);
357
 
358
  const pageHeight = pdf.internal.pageSize.getHeight();
359
  const margin = 10;
360
  const maxWidth = 190;
361
  const lineHeight = 5;
362
+ let y = 20;
363
 
364
  const lines = pdf.splitTextToSize(contentToPdf, maxWidth);
 
365
  lines.forEach((line: string) => {
366
+ if (y > pageHeight - margin) {
367
  pdf.addPage();
368
+ y = margin;
369
  }
370
+ pdf.text(line, margin, y);
371
+ y += lineHeight;
372
  });
373
 
374
+ pdf.save("chat-export.pdf");
375
  setShowDownloadDialog(false);
376
+ toast.success("PDF downloaded successfully");
377
  } catch (error) {
378
+ // eslint-disable-next-line no-console
379
+ console.error("PDF generation error:", error);
380
+ toast.error("Failed to generate PDF");
381
  }
382
  };
383
 
 
385
  const isCurrentChatSaved = (): boolean => {
386
  if (messages.length <= 1) return false;
387
 
388
+ return savedChats.some((chat) => {
389
  if (chat.chatMode !== chatMode) return false;
390
  if (chat.messages.length !== messages.length) return false;
391
 
392
+ return chat.messages.every((savedMsg, idx) => {
393
+ const currentMsg = messages[idx];
394
  return (
395
  savedMsg.id === currentMsg.id &&
396
  savedMsg.role === currentMsg.role &&
 
402
 
403
  const handleSaveClick = () => {
404
  if (messages.length <= 1) {
405
+ toast.info("No conversation to save");
406
  return;
407
  }
 
408
  onSaveChat();
409
  };
410
 
411
  const handleShareClick = () => {
412
  if (messages.length <= 1) {
413
+ toast.info("No conversation to share");
414
  return;
415
  }
416
  const conversationText = buildPreviewContent();
417
+ const blob = new Blob([conversationText], { type: "text/plain" });
418
  const url = URL.createObjectURL(blob);
419
  setShareLink(url);
420
  setTargetWorkspaceId(currentWorkspaceId);
 
424
  const handleCopyShareLink = async () => {
425
  try {
426
  await navigator.clipboard.writeText(shareLink);
427
+ toast.success("Link copied");
428
  } catch {
429
+ toast.error("Failed to copy link");
430
  }
431
  };
432
 
433
  const handleShareSendToWorkspace = () => {
434
  const content = buildPreviewContent();
435
+ onSaveFile?.(content, "export", "text", targetWorkspaceId);
436
  setShowShareDialog(false);
437
+ toast.success("Sent to workspace Saved Files");
438
+ };
439
+
440
+ const handleClearClick = () => {
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
+ }
448
+
449
+ const hasUserMessages = messages.some((m) => m.role === "user");
450
+ if (!hasUserMessages) {
451
+ onClearConversation();
452
+ return;
453
+ }
454
+
455
+ onClearConversation();
456
  };
457
 
458
+ // DnD
459
  const handleDragOver = (e: React.DragEvent) => {
460
  e.preventDefault();
461
  e.stopPropagation();
 
478
  const fileList = e.dataTransfer.files;
479
  const files: File[] = [];
480
  for (let i = 0; i < fileList.length; i++) {
481
+ const f = fileList.item(i);
482
+ if (f) files.push(f);
 
 
483
  }
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((allowed) =>
488
+ ext.endsWith(allowed)
489
  );
490
  });
491
 
492
  if (validFiles.length > 0) {
493
+ setPendingFiles(validFiles.map((file) => ({ file, type: "other" as FileType })));
494
  setShowTypeDialog(true);
495
  } else {
496
+ toast.error("Please upload .pdf, .docx, .pptx, or image files");
497
  }
498
  };
499
 
 
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((allowed) =>
506
+ ext.endsWith(allowed)
507
  );
508
  });
509
+
510
  if (validFiles.length > 0) {
511
+ setPendingFiles(validFiles.map((file) => ({ file, type: "other" as FileType })));
512
  setShowTypeDialog(true);
513
  } else {
514
+ toast.error("Please upload .pdf, .docx, .pptx, or image files");
515
  }
516
  }
517
+ e.target.value = "";
518
  };
519
 
520
  const handleConfirmUpload = () => {
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);
528
  }, 0);
529
  });
530
+
531
+ const count = pendingFiles.length;
532
  setPendingFiles([]);
533
  setShowTypeDialog(false);
534
+ toast.success(`${count} file(s) uploaded successfully`);
535
  };
536
 
537
  const handleCancelUpload = () => {
 
540
  };
541
 
542
  const handlePendingFileTypeChange = (index: number, type: FileType) => {
543
+ setPendingFiles((prev) => prev.map((pf, i) => (i === index ? { ...pf, type } : pf)));
544
  };
545
 
546
+ // File helpers
547
  const getFileIcon = (filename: string) => {
548
  const ext = filename.toLowerCase();
549
+ if (ext.endsWith(".pdf")) return FileText;
550
+ if (ext.endsWith(".docx") || ext.endsWith(".doc")) return File;
551
+ if (ext.endsWith(".pptx") || ext.endsWith(".ppt")) return Presentation;
552
+ if ([".jpg", ".jpeg", ".png", ".gif", ".webp"].some((e) => ext.endsWith(e))) return ImageIcon;
553
  return File;
554
  };
555
 
556
  const getFileTypeInfo = (filename: string) => {
557
  const ext = filename.toLowerCase();
558
+ if (ext.endsWith(".pdf")) return { bgColor: "bg-red-500", type: "PDF" };
559
+ if (ext.endsWith(".docx") || ext.endsWith(".doc")) return { bgColor: "bg-blue-500", type: "Document" };
560
+ if (ext.endsWith(".pptx") || ext.endsWith(".ppt")) return { bgColor: "bg-orange-500", type: "Presentation" };
561
+ if ([".jpg", ".jpeg", ".png", ".gif", ".webp"].some((e) => ext.endsWith(e)))
562
+ return { bgColor: "bg-green-500", type: "Image" };
563
+ return { bgColor: "bg-gray-500", type: "File" };
564
  };
565
 
566
  const formatFileSize = (bytes: number) => {
567
+ if (bytes < 1024) return `${bytes} B`;
568
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
569
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
570
  };
571
 
572
  const FileThumbnail = ({
 
575
  fileInfo,
576
  isImage,
577
  onPreview,
578
+ onRemove,
579
  }: {
580
  file: File;
581
  Icon: React.ComponentType<{ className?: string }>;
 
588
  const [imageLoading, setImageLoading] = useState(true);
589
 
590
  useEffect(() => {
591
+ if (!isImage) {
 
 
 
 
 
 
 
 
 
 
 
592
  setImagePreview(null);
593
  setImageLoading(false);
594
+ return;
595
  }
596
+
597
+ setImageLoading(true);
598
+ const reader = new FileReader();
599
+ reader.onload = (e) => {
600
+ setImagePreview(e.target?.result as string);
601
+ setImageLoading(false);
602
+ };
603
+ reader.onerror = () => {
604
+ setImageLoading(false);
605
+ };
606
+ reader.readAsDataURL(file);
607
  }, [file, isImage]);
608
 
609
  if (isImage) {
 
611
  <div
612
  className="relative cursor-pointer w-16 h-16 flex-shrink-0"
613
  onClick={onPreview}
614
+ style={{ width: "64px", height: "64px", flexShrink: 0 }}
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">
 
625
  alt={file.name}
626
  className="w-full h-full object-cover"
627
  onError={(e) => {
628
+ e.currentTarget.style.display = "none";
629
  setImageLoading(false);
630
  }}
631
  />
 
635
  </div>
636
  )}
637
  </div>
638
+
639
  <button
640
  type="button"
641
  className="absolute top-1 right-1 h-4 w-4 rounded-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow-sm flex items-center justify-center cursor-pointer"
 
643
  e.stopPropagation();
644
  onRemove(e);
645
  }}
646
+ style={{ zIndex: 100, position: "absolute", top: "4px", right: "4px" }}
647
  >
648
+ <X className="h-2.5 w-2.5" style={{ color: "rgb(0, 0, 0)", strokeWidth: 2 }} />
 
 
 
 
 
 
649
  </button>
650
  </div>
651
  </div>
 
653
  }
654
 
655
  return (
656
+ <div className="relative cursor-pointer" onClick={onPreview} style={{ width: "240px", flexShrink: 0 }}>
 
 
 
 
657
  <div className="h-16 w-full relative flex items-center px-3 bg-card border border-border rounded-lg hover:border-primary/50 transition-colors">
658
  <div className={`${fileInfo.bgColor} flex items-center justify-center w-10 h-10 rounded shrink-0`}>
659
  <Icon className="h-5 w-5 text-white" />
 
663
  <p className="text-xs text-foreground truncate" title={file.name}>
664
  {file.name}
665
  </p>
666
+ <p className="text-[10px] text-muted-foreground mt-0.5 truncate">{fileInfo.type}</p>
 
 
667
  </div>
668
+
669
  <button
670
  type="button"
671
  className="absolute top-1 right-1 h-4 w-4 rounded-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 shadow-sm flex items-center justify-center cursor-pointer z-10"
 
673
  e.stopPropagation();
674
  onRemove(e);
675
  }}
676
+ style={{ position: "absolute", top: "4px", right: "4px", zIndex: 10 }}
677
  >
678
+ <X className="h-2.5 w-2.5" style={{ color: "rgb(0, 0, 0)", strokeWidth: 2 }} />
 
 
 
 
 
 
679
  </button>
680
  </div>
681
  </div>
 
683
  };
684
 
685
  const FileViewerContent = ({ file }: { file: File }) => {
686
+ const [content, setContent] = useState<string>("");
687
  const [loading, setLoading] = useState(true);
688
  const [error, setError] = useState<string | null>(null);
689
 
 
695
 
696
  const ext = file.name.toLowerCase();
697
 
698
+ if ([".jpg", ".jpeg", ".png", ".gif", ".webp"].some((e) => ext.endsWith(e))) {
699
  const reader = new FileReader();
700
  reader.onload = (e) => {
701
  setContent(e.target?.result as string);
702
  setLoading(false);
703
  };
704
  reader.onerror = () => {
705
+ setError("Failed to load image");
706
  setLoading(false);
707
  };
708
  reader.readAsDataURL(file);
709
+ return;
710
+ }
711
+
712
+ if (ext.endsWith(".pdf")) {
713
+ setContent("PDF files cannot be previewed directly. Please download the file to view it.");
714
  setLoading(false);
715
+ return;
 
 
 
 
 
 
 
 
 
 
716
  }
717
+
718
+ const reader = new FileReader();
719
+ reader.onload = (e) => {
720
+ setContent(String(e.target?.result ?? ""));
721
+ setLoading(false);
722
+ };
723
+ reader.onerror = () => {
724
+ setError("Failed to load file");
725
+ setLoading(false);
726
+ };
727
+ reader.readAsText(file);
728
+ } catch {
729
+ setError("Failed to load file");
730
  setLoading(false);
731
  }
732
  };
 
738
  if (error) return <div className="text-center py-8 text-destructive">{error}</div>;
739
 
740
  const ext = file.name.toLowerCase();
741
+ const isImage = [".jpg", ".jpeg", ".png", ".gif", ".webp"].some((e) => ext.endsWith(e));
742
 
743
  if (isImage) {
744
  return (
 
756
  };
757
 
758
  return (
759
+ <div className="flex flex-col h-full min-h-0 overflow-hidden" style={{ overscrollBehavior: "none" }}>
760
+ <div className="flex-1 relative min-h-0 flex flex-col overflow-hidden" style={{ overscrollBehavior: "none" }}>
761
  {/* ✅ Messages Area is the ONLY scroll container */}
762
  <div
763
  ref={scrollContainerRef}
764
  className="flex-1 min-h-0 overflow-y-auto"
765
+ style={{ overscrollBehavior: "contain" }}
 
 
766
  >
767
  {/* Top Bar - Sticky */}
768
  <div
769
+ className={`sticky top-0 flex items-center justify-between px-4 z-20 bg-card ${
770
+ showTopBorder ? "border-b border-border" : ""
771
+ }`}
772
+ style={{ height: "4.5rem", margin: 0, padding: "1rem 1rem", boxSizing: "border-box" }}
 
 
 
773
  >
774
  {/* Course Selector - Left */}
775
  <div className="flex-shrink-0">
776
  {(() => {
777
+ const current = workspaces.find((w) => w.id === currentWorkspaceId);
778
+ if (current?.type === "group") {
779
+ if (current.category === "course" && current.courseName) {
780
  return (
781
  <div className="h-9 px-3 inline-flex items-center rounded-md border font-semibold">
782
  {current.courseName}
 
785
  }
786
  return null;
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>
 
815
  orientation="horizontal"
816
  >
817
  <TabsList className="inline-flex h-8 items-center justify-center rounded-xl bg-muted p-1 text-muted-foreground">
818
+ <TabsTrigger value="ask" className="w-[140px] px-3 text-sm">
819
+ Ask
820
+ </TabsTrigger>
821
  <TabsTrigger value="review" className="w-[140px] px-3 text-sm relative">
822
  Review
823
  <span
824
  className="absolute top-0 right-0 bg-red-500 rounded-full border-2"
825
  style={{
826
+ width: "10px",
827
+ height: "10px",
828
+ transform: "translate(25%, -25%)",
829
  zIndex: 10,
830
+ borderColor: "var(--muted)",
831
  }}
832
  />
833
  </TabsTrigger>
834
+ <TabsTrigger value="quiz" className="w-[140px] px-3 text-sm">
835
+ Quiz
836
+ </TabsTrigger>
837
  </TabsList>
838
  </Tabs>
839
  </div>
 
845
  size="icon"
846
  onClick={handleSaveClick}
847
  disabled={!isLoggedIn}
848
+ className={`h-8 w-8 rounded-md hover:bg-muted/50 ${isCurrentChatSaved() ? "text-primary" : ""}`}
849
+ title={isCurrentChatSaved() ? "Unsave" : "Save"}
850
  >
851
+ <Bookmark className={`h-4 w-4 ${isCurrentChatSaved() ? "fill-primary text-primary" : ""}`} />
852
  </Button>
853
+
854
  <Button
855
  variant="ghost"
856
  size="icon"
 
861
  >
862
  <Download className="h-4 w-4" />
863
  </Button>
864
+
865
  <Button
866
  variant="ghost"
867
  size="icon"
 
872
  >
873
  <Share2 className="h-4 w-4" />
874
  </Button>
875
+
876
  <Button
877
  variant="outline"
878
  onClick={handleClearClick}
 
887
  </div>
888
 
889
  {/* Messages Content */}
890
+ <div className="py-6" style={{ paddingBottom: "12rem" }}>
 
 
 
 
 
891
  <div className="w-full space-y-6 max-w-4xl mx-auto">
892
  {messages.map((message) => (
893
  <React.Fragment key={message.id}>
894
  <Message
895
  message={message}
896
+ showSenderInfo={spaceType === "group"}
897
  isFirstGreeting={
898
+ (message.id === "1" || message.id === "review-1" || message.id === "quiz-1") &&
899
+ message.role === "assistant"
900
  }
901
  showNextButton={message.showNextButton && !isAppTyping}
902
  onNextQuestion={onNextQuestion}
903
  chatMode={chatMode}
904
  />
905
 
906
+ {chatMode === "review" && message.id === "review-1" && message.role === "assistant" && (
907
+ <div className="flex gap-2 justify-start px-4">
908
+ <div className="w-10 h-10 flex-shrink-0" />
909
+ <div className="w-full" style={{ maxWidth: "min(770px, calc(100% - 2rem))" }}>
910
+ <SmartReview onReviewTopic={handleReviewTopic} onReviewAll={handleReviewAll} />
 
 
 
911
  </div>
912
+ </div>
913
+ )}
914
 
915
+ {chatMode === "quiz" &&
916
+ message.id === "quiz-1" &&
917
+ message.role === "assistant" &&
918
  quizState.currentQuestion === 0 &&
919
  !quizState.waitingForAnswer &&
920
  !isAppTyping && (
 
934
  </div>
935
  <div className="bg-muted rounded-2xl px-4 py-3">
936
  <div className="flex gap-1">
937
+ <div
938
+ className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce"
939
+ style={{ animationDelay: "0ms" }}
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>
 
961
  <div
962
  className="fixed z-30 flex justify-center pointer-events-none"
963
  style={{
964
+ bottom: "120px",
965
+ left: leftPanelVisible ? "320px" : "0px",
966
+ right: "0px",
967
  }}
968
  >
969
  <Button
 
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">
 
992
  {uploadedFiles.map((uploadedFile, index) => {
993
  const Icon = getFileIcon(uploadedFile.file.name);
994
  const fileInfo = getFileTypeInfo(uploadedFile.file.name);
995
+ const isImage = ["jpg", "jpeg", "png", "gif", "webp"].some((ext) =>
996
  uploadedFile.file.name.toLowerCase().endsWith(`.${ext}`)
997
  );
998
 
 
1024
  onDragOver={handleDragOver}
1025
  onDragLeave={handleDragLeave}
1026
  onDrop={handleDrop}
1027
+ className={isDragging ? "opacity-75" : ""}
1028
  >
1029
  <div className="relative">
1030
+ {/* Mode Selector + Upload */}
1031
  <div className="absolute bottom-3 left-2 flex items-center gap-1 z-10">
1032
+ {chatMode === "ask" && (
1033
  <DropdownMenu>
1034
  <DropdownMenuTrigger asChild>
1035
  <Button
 
1046
  </Button>
1047
  </DropdownMenuTrigger>
1048
  <DropdownMenuContent align="start" className="w-56">
1049
+ <DropdownMenuItem
1050
+ onClick={() => onLearningModeChange("general")}
1051
+ className={learningMode === "general" ? "bg-accent" : ""}
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
+
1061
+ <DropdownMenuItem
1062
+ onClick={() => onLearningModeChange("concept")}
1063
+ className={learningMode === "concept" ? "bg-accent" : ""}
1064
+ >
1065
  <div className="flex flex-col">
1066
  <span className="font-medium">Concept Explainer</span>
1067
  <span className="text-xs text-muted-foreground">Get detailed explanations of concepts</span>
1068
  </div>
1069
  </DropdownMenuItem>
1070
+
1071
+ <DropdownMenuItem
1072
+ onClick={() => onLearningModeChange("socratic")}
1073
+ className={learningMode === "socratic" ? "bg-accent" : ""}
1074
+ >
1075
  <div className="flex flex-col">
1076
  <span className="font-medium">Socratic Tutor</span>
1077
  <span className="text-xs text-muted-foreground">Learn through guided questions</span>
1078
  </div>
1079
  </DropdownMenuItem>
1080
+
1081
+ <DropdownMenuItem
1082
+ onClick={() => onLearningModeChange("exam")}
1083
+ className={learningMode === "exam" ? "bg-accent" : ""}
1084
+ >
1085
  <div className="flex flex-col">
1086
  <span className="font-medium">Exam Prep</span>
1087
  <span className="text-xs text-muted-foreground">Practice with quiz questions</span>
1088
  </div>
1089
  </DropdownMenuItem>
1090
+
1091
+ <DropdownMenuItem
1092
+ onClick={() => onLearningModeChange("assignment")}
1093
+ className={learningMode === "assignment" ? "bg-accent" : ""}
1094
+ >
1095
  <div className="flex flex-col">
1096
  <span className="font-medium">Assignment Helper</span>
1097
  <span className="text-xs text-muted-foreground">Get help with assignments</span>
1098
  </div>
1099
  </DropdownMenuItem>
1100
+
1101
+ <DropdownMenuItem
1102
+ onClick={() => onLearningModeChange("summary")}
1103
+ className={learningMode === "summary" ? "bg-accent" : ""}
1104
+ >
1105
  <div className="flex flex-col">
1106
  <span className="font-medium">Quick Summary</span>
1107
  <span className="text-xs text-muted-foreground">Get concise summaries</span>
 
1115
  type="button"
1116
  size="icon"
1117
  variant="ghost"
1118
+ disabled={!isLoggedIn || (chatMode === "quiz" && !quizState.waitingForAnswer)}
1119
  className="h-8 w-8 hover:bg-muted/50"
1120
  onClick={() => fileInputRef.current?.click()}
1121
  title="Upload files"
 
1131
  placeholder={
1132
  !isLoggedIn
1133
  ? "Please log in on the right to start chatting..."
1134
+ : chatMode === "quiz"
1135
+ ? quizState.waitingForAnswer
1136
+ ? "Type your answer here..."
1137
+ : quizState.currentQuestion > 0
1138
+ ? "Click 'Next Question' to continue..."
1139
+ : "Click 'Start Quiz' to begin..."
1140
+ : spaceType === "group"
1141
+ ? "Type a message or drag files here... (mention @Clare to get AI assistance)"
1142
+ : learningMode === "general"
1143
+ ? "Ask me anything! Please provide context about your question..."
1144
+ : "Ask Clare anything about the course or drag files here..."
1145
  }
1146
+ disabled={!isLoggedIn || (chatMode === "quiz" && !quizState.waitingForAnswer)}
1147
+ className={`min-h-[80px] pl-4 pr-20 resize-none bg-background border-2 ${
1148
+ isDragging ? "border-primary border-dashed" : "border-border"
1149
+ }`}
1150
  />
1151
 
1152
  <div className="absolute bottom-2 right-2 flex gap-1">
1153
+ <Button type="submit" size="icon" disabled={!input.trim() || !isLoggedIn} className="h-8 w-8 rounded-full">
 
 
 
 
 
1154
  <Send className="h-4 w-4" />
1155
  </Button>
1156
  </div>
 
1178
  <AlertDialogDescription>
1179
  Would you like to save the current chat before starting a new conversation?
1180
  </AlertDialogDescription>
1181
+
1182
  <Button
1183
  variant="ghost"
1184
  size="icon"
 
1189
  <span className="sr-only">Close</span>
1190
  </Button>
1191
  </AlertDialogHeader>
1192
+
1193
  <AlertDialogFooter className="flex-col sm:flex-row gap-2 sm:justify-end">
1194
  <Button variant="outline" onClick={() => onConfirmClear(false)} className="sm:flex-1 sm:max-w-[200px]">
1195
  Start New (Don't Save)
 
1212
  <Tabs
1213
  value={downloadTab}
1214
  onValueChange={(value) => {
1215
+ const v = value as "chat" | "summary";
1216
+ setDownloadTab(v);
1217
+ setDownloadPreview(v === "chat" ? buildPreviewContent() : buildSummaryContent());
1218
+ if (v === "summary") setDownloadOptions({ chat: false, summary: true });
1219
  else setDownloadOptions({ chat: true, summary: false });
1220
  }}
1221
  className="w-full"
 
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>
 
1252
  checked={downloadOptions.chat}
1253
  onCheckedChange={(checked) => setDownloadOptions({ ...downloadOptions, chat: checked === true })}
1254
  />
1255
+ <label htmlFor="download-chat" className="text-sm font-medium cursor-pointer">
1256
+ Download chat
1257
+ </label>
1258
  </div>
1259
  <div className="flex items-center space-x-2">
1260
  <Checkbox
 
1262
  checked={downloadOptions.summary}
1263
  onCheckedChange={(checked) => setDownloadOptions({ ...downloadOptions, summary: checked === true })}
1264
  />
1265
+ <label htmlFor="download-summary" className="text-sm font-medium cursor-pointer">
1266
+ Download summary
1267
+ </label>
1268
  </div>
1269
  </div>
1270
 
1271
  <DialogFooter>
1272
+ <Button variant="outline" onClick={() => setShowDownloadDialog(false)}>
1273
+ Cancel
1274
+ </Button>
1275
  <Button onClick={handleDownloadFile}>Download</Button>
1276
  </DialogFooter>
1277
  </DialogContent>
 
1289
  <Label>Copy Link</Label>
1290
  <div className="flex gap-2 items-center">
1291
  <Input value={shareLink} readOnly className="flex-1" />
1292
+ <Button variant="secondary" onClick={handleCopyShareLink}>
1293
+ Copy
1294
+ </Button>
1295
  </div>
1296
  <p className="text-xs text-muted-foreground">Temporary link valid for this session.</p>
1297
  </div>
1298
+
1299
  <div className="space-y-2">
1300
  <Label>Send to Workspace</Label>
1301
  <Select value={targetWorkspaceId} onValueChange={setTargetWorkspaceId}>
 
1303
  <SelectValue placeholder="Choose a workspace" />
1304
  </SelectTrigger>
1305
  <SelectContent>
1306
+ {workspaces.map((w) => (
1307
+ <SelectItem key={w.id} value={w.id}>
1308
+ {w.name}
1309
+ </SelectItem>
1310
  ))}
1311
  </SelectContent>
1312
  </Select>
1313
+ <p className="text-xs text-muted-foreground">
1314
+ Sends this conversation to the selected workspace&apos;s Saved Files.
1315
+ </p>
1316
+ <Button onClick={handleShareSendToWorkspace} className="w-full">
1317
+ Send
1318
+ </Button>
1319
  </div>
1320
  </div>
1321
  </DialogContent>
 
1327
  <AlertDialogHeader>
1328
  <AlertDialogTitle>Delete File</AlertDialogTitle>
1329
  <AlertDialogDescription>
1330
+ Are you sure you want to delete &quot;
1331
+ {fileToDelete !== null ? uploadedFiles[fileToDelete]?.file.name : ""}
1332
+ &quot;? This action cannot be undone.
1333
  </AlertDialogDescription>
1334
  </AlertDialogHeader>
1335
  <AlertDialogFooter>
 
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>
1373
  </Dialog>
1374
 
 
1380
  <DialogTitle>Select File Types</DialogTitle>
1381
  <DialogDescription>Please select the type for each file you are uploading.</DialogDescription>
1382
  </DialogHeader>
1383
+
1384
  <div className="space-y-3 max-h-64 overflow-y-auto">
1385
  {pendingFiles.map((pendingFile, index) => {
1386
  const Icon = getFileIcon(pendingFile.file.name);
 
1393
  <p className="text-xs text-muted-foreground">{formatFileSize(pendingFile.file.size)}</p>
1394
  </div>
1395
  </div>
1396
+
1397
  <div className="space-y-1">
1398
  <label className="text-xs text-muted-foreground">File Type</label>
1399
  <Select
 
1415
  );
1416
  })}
1417
  </div>
1418
+
1419
  <DialogFooter>
1420
+ <Button variant="outline" onClick={handleCancelUpload}>
1421
+ Cancel
1422
+ </Button>
1423
  <Button onClick={handleConfirmUpload}>Upload</Button>
1424
  </DialogFooter>
1425
  </DialogContent>