SarahXia0405 commited on
Commit
7c567ff
·
verified ·
1 Parent(s): c3c70f8

Update web/src/components/ChatArea.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/ChatArea.tsx +330 -486
web/src/components/ChatArea.tsx CHANGED
@@ -1,11 +1,26 @@
 
1
  import React, { useState, useRef, useEffect } from 'react';
2
  import { Button } from './ui/button';
3
  import { Textarea } from './ui/textarea';
4
  import { Input } from './ui/input';
5
  import { Label } from './ui/label';
6
- import { Send, ArrowDown, AlertCircle, Trash2, Share2, Upload, X, File, FileText, Presentation, Image as ImageIcon, Bookmark, Plus, Download, Copy } from 'lucide-react';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  import { Message } from './Message';
8
- import { Alert, AlertDescription } from './ui/alert';
9
  import { Badge } from './ui/badge';
10
  import { Tabs, TabsContent, TabsList, TabsTrigger } from './ui/tabs';
11
  import type { Message as MessageType, LearningMode, UploadedFile, FileType, SpaceType, ChatMode, SavedChat, Workspace } from '../App';
@@ -17,7 +32,7 @@ import {
17
  DropdownMenuItem,
18
  DropdownMenuTrigger,
19
  } from './ui/dropdown-menu';
20
- import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogOverlay } from './ui/dialog';
21
  import { Checkbox } from './ui/checkbox';
22
  import {
23
  AlertDialog,
@@ -110,7 +125,6 @@ export function ChatArea({
110
  showReviewBanner = false,
111
  }: ChatAreaProps) {
112
  const [input, setInput] = useState('');
113
- const [isTyping, setIsTyping] = useState(false);
114
  const [showScrollButton, setShowScrollButton] = useState(false);
115
  const [showTopBorder, setShowTopBorder] = useState(false);
116
  const [isDragging, setIsDragging] = useState(false);
@@ -118,7 +132,7 @@ export function ChatArea({
118
  const [showTypeDialog, setShowTypeDialog] = useState(false);
119
  const [showDeleteDialog, setShowDeleteDialog] = useState(false);
120
  const [fileToDelete, setFileToDelete] = useState<number | null>(null);
121
- const [selectedFile, setSelectedFile] = useState<{file: File; index: number} | null>(null);
122
  const [showFileViewer, setShowFileViewer] = useState(false);
123
  const [showDownloadDialog, setShowDownloadDialog] = useState(false);
124
  const [downloadPreview, setDownloadPreview] = useState('');
@@ -135,6 +149,7 @@ export function ChatArea({
135
  { id: 'course3', name: 'Data Structures' },
136
  { id: 'course4', name: 'Web Development' },
137
  ];
 
138
  const messagesEndRef = useRef<HTMLDivElement>(null);
139
  const scrollContainerRef = useRef<HTMLDivElement>(null);
140
  const fileInputRef = useRef<HTMLInputElement>(null);
@@ -185,22 +200,26 @@ export function ChatArea({
185
  }
186
  }, [messages]);
187
 
188
- // Use native event listeners to prevent scroll propagation to left panel
189
  useEffect(() => {
190
- const container = scrollContainerRef.current;
191
- if (!container) return;
192
 
193
- const handleWheel = (e: WheelEvent) => {
194
- // Always stop propagation to prevent scrolling left panel
195
  e.stopPropagation();
196
- e.stopImmediatePropagation();
197
- };
198
 
199
- container.addEventListener('wheel', handleWheel, { passive: false, capture: true });
200
-
201
- return () => {
202
- container.removeEventListener('wheel', handleWheel, { capture: true });
 
 
 
203
  };
 
 
 
204
  }, []);
205
 
206
  const handleSubmit = (e: React.FormEvent) => {
@@ -209,7 +228,6 @@ export function ChatArea({
209
 
210
  onSendMessage(input);
211
  setInput('');
212
- // All modes now use isAppTyping from App.tsx, so we don't set local isTyping
213
  };
214
 
215
  const handleKeyDown = (e: React.KeyboardEvent) => {
@@ -230,41 +248,32 @@ export function ChatArea({
230
 
231
  // Handle review topic button click
232
  const handleReviewTopic = (item: { title: string; previousQuestion: string; memoryRetention: number; schedule: string; status: string; weight: number; lastReviewed: string }) => {
233
- // Send a user message with review request and data
234
- // Format: User-friendly message + hidden data for parsing
235
  const userMessage = `Please help me review: ${item.title}`;
236
  const reviewData = `REVIEW_TOPIC:${item.title}|${item.previousQuestion}|${item.memoryRetention}|${item.schedule}|${item.status}|${item.weight}|${item.lastReviewed}`;
237
- // Store review data temporarily to pass to App.tsx
238
  (window as any).__lastReviewData = reviewData;
239
  onSendMessage(userMessage);
240
  };
241
 
242
  // Handle review all button click
243
  const handleReviewAll = () => {
244
- // Send a user message requesting review all
245
  (window as any).__lastReviewData = 'REVIEW_ALL';
246
  onSendMessage('Please help me review all topics that need attention.');
247
  };
248
 
249
  const handleClearClick = () => {
250
- // Check if current chat is a saved/previewed chat
251
  const isSavedChat = isCurrentChatSaved();
252
-
253
- // If viewing a saved chat, clear directly without dialog
254
  if (isSavedChat) {
255
- // Directly clear without showing dialog
256
  onConfirmClear(false);
257
  return;
258
  }
259
-
260
- // Check if there are user messages (not just welcome message)
261
  const hasUserMessages = messages.some(msg => msg.role === 'user');
262
  if (!hasUserMessages) {
263
- // No user messages, just clear without showing dialog
264
  onClearConversation();
265
  return;
266
  }
267
- // User messages exist, show dialog to save or discard
268
  onClearConversation();
269
  };
270
 
@@ -277,23 +286,21 @@ export function ChatArea({
277
 
278
  const buildSummaryContent = () => {
279
  if (!messages.length) return 'No messages to summarize.';
280
-
281
- // Simple summary: count messages and list main topics
282
  const userMessages = messages.filter(msg => msg.role === 'user');
283
  const assistantMessages = messages.filter(msg => msg.role === 'assistant');
284
-
285
  let summary = `Chat Summary\n================\n\n`;
286
  summary += `Total Messages: ${messages.length}\n`;
287
  summary += `- User Messages: ${userMessages.length}\n`;
288
  summary += `- Assistant Responses: ${assistantMessages.length}\n\n`;
289
-
290
  summary += `Key Points:\n`;
291
- // Extract first 3 user messages as key points
292
  userMessages.slice(0, 3).forEach((msg, idx) => {
293
  const preview = msg.content.substring(0, 80);
294
  summary += `${idx + 1}. ${preview}${msg.content.length > 80 ? '...' : ''}\n`;
295
  });
296
-
297
  return summary;
298
  };
299
 
@@ -316,49 +323,42 @@ export function ChatArea({
316
  const handleDownloadFile = async () => {
317
  try {
318
  let contentToPdf = '';
319
-
320
- // Build content based on selected options
321
  if (downloadOptions.chat) {
322
  contentToPdf += buildPreviewContent();
323
  }
324
-
325
  if (downloadOptions.summary) {
326
  if (downloadOptions.chat) {
327
  contentToPdf += '\n\n================\n\n';
328
  }
329
  contentToPdf += buildSummaryContent();
330
  }
331
-
332
  if (!contentToPdf.trim()) {
333
  toast.error('Please select at least one option');
334
  return;
335
  }
336
-
337
- // Create PDF
338
  const pdf = new jsPDF({
339
  orientation: 'portrait',
340
  unit: 'mm',
341
  format: 'a4'
342
  });
343
-
344
- // Set font
345
  pdf.setFontSize(11);
346
-
347
- // Add title
348
  pdf.setFontSize(14);
349
  pdf.text('Chat Export', 10, 10);
350
  pdf.setFontSize(11);
351
-
352
- // Split content into lines and add to PDF
353
  const pageHeight = pdf.internal.pageSize.getHeight();
354
  const margin = 10;
355
- const maxWidth = 190; // A4 width minus margins
356
  const lineHeight = 5;
357
  let yPosition = 20;
358
-
359
- // Split text into lines that fit in the page
360
  const lines = pdf.splitTextToSize(contentToPdf, maxWidth);
361
-
362
  lines.forEach((line: string) => {
363
  if (yPosition > pageHeight - margin) {
364
  pdf.addPage();
@@ -367,8 +367,7 @@ export function ChatArea({
367
  pdf.text(line, margin, yPosition);
368
  yPosition += lineHeight;
369
  });
370
-
371
- // Save PDF
372
  pdf.save('chat-export.pdf');
373
  setShowDownloadDialog(false);
374
  toast.success('PDF downloaded successfully');
@@ -381,13 +380,11 @@ export function ChatArea({
381
  // Check if current chat is already saved
382
  const isCurrentChatSaved = (): boolean => {
383
  if (messages.length <= 1) return false;
384
-
385
- // Find a saved chat that matches the current messages and chatMode
386
  return savedChats.some(chat => {
387
  if (chat.chatMode !== chatMode) return false;
388
  if (chat.messages.length !== messages.length) return false;
389
-
390
- // Check if all messages match
391
  return chat.messages.every((savedMsg, index) => {
392
  const currentMsg = messages[index];
393
  return (
@@ -404,7 +401,7 @@ export function ChatArea({
404
  toast.info('No conversation to save');
405
  return;
406
  }
407
-
408
  onSaveChat();
409
  };
410
 
@@ -413,12 +410,10 @@ export function ChatArea({
413
  toast.info('No conversation to share');
414
  return;
415
  }
416
- // Create a temporary share link for this session
417
  const conversationText = buildPreviewContent();
418
  const blob = new Blob([conversationText], { type: 'text/plain' });
419
  const url = URL.createObjectURL(blob);
420
  setShareLink(url);
421
- // Default to current workspace
422
  setTargetWorkspaceId(currentWorkspaceId);
423
  setShowShareDialog(true);
424
  };
@@ -469,7 +464,7 @@ export function ChatArea({
469
 
470
  const validFiles = files.filter((file) => {
471
  const ext = file.name.toLowerCase();
472
- return ['.pdf', '.docx', '.pptx', '.jpg', '.jpeg', '.png', '.gif', '.webp', '.doc', '.ppt'].some((allowedExt) =>
473
  ext.endsWith(allowedExt)
474
  );
475
  });
@@ -487,7 +482,7 @@ export function ChatArea({
487
  if (files.length > 0) {
488
  const validFiles = files.filter((file) => {
489
  const ext = file.name.toLowerCase();
490
- return ['.pdf', '.docx', '.pptx', '.jpg', '.jpeg', '.png', '.gif', '.webp', '.doc', '.ppt'].some((allowedExt) =>
491
  ext.endsWith(allowedExt)
492
  );
493
  });
@@ -503,7 +498,6 @@ export function ChatArea({
503
 
504
  const handleConfirmUpload = () => {
505
  onFileUpload(pendingFiles.map(pf => pf.file));
506
- // Update the parent's file types
507
  const startIndex = uploadedFiles.length;
508
  pendingFiles.forEach((pf, idx) => {
509
  setTimeout(() => {
@@ -521,9 +515,7 @@ export function ChatArea({
521
  };
522
 
523
  const handlePendingFileTypeChange = (index: number, type: FileType) => {
524
- setPendingFiles(prev => prev.map((pf, i) =>
525
- i === index ? { ...pf, type } : pf
526
- ));
527
  };
528
 
529
  const getFileIcon = (filename: string) => {
@@ -537,18 +529,10 @@ export function ChatArea({
537
 
538
  const getFileTypeInfo = (filename: string) => {
539
  const ext = filename.toLowerCase();
540
- if (ext.endsWith('.pdf')) {
541
- return { bgColor: 'bg-red-500', type: 'PDF' };
542
- }
543
- if (ext.endsWith('.docx') || ext.endsWith('.doc')) {
544
- return { bgColor: 'bg-blue-500', type: 'Document' };
545
- }
546
- if (ext.endsWith('.pptx') || ext.endsWith('.ppt')) {
547
- return { bgColor: 'bg-orange-500', type: 'Presentation' };
548
- }
549
- if (['.jpg', '.jpeg', '.png', '.gif', '.webp'].some(e => ext.endsWith(e))) {
550
- return { bgColor: 'bg-green-500', type: 'Image' };
551
- }
552
  return { bgColor: 'bg-gray-500', type: 'File' };
553
  };
554
 
@@ -558,25 +542,24 @@ export function ChatArea({
558
  return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
559
  };
560
 
561
- // File Thumbnail Component - ChatGPT style
562
- const FileThumbnail = ({
563
- file,
564
- Icon,
565
- fileInfo,
566
- isImage,
567
- onPreview,
568
- onRemove
569
- }: {
570
- file: File;
571
- Icon: React.ComponentType<{ className?: string }>;
572
- fileInfo: { bgColor: string; type: string };
573
- isImage: boolean;
574
- onPreview: () => void;
575
  onRemove: (e: React.MouseEvent) => void;
576
  }) => {
577
  const [imagePreview, setImagePreview] = useState<string | null>(null);
578
  const [imageLoading, setImageLoading] = useState(true);
579
-
580
  useEffect(() => {
581
  if (isImage) {
582
  setImageLoading(true);
@@ -594,9 +577,7 @@ export function ChatArea({
594
  setImageLoading(false);
595
  }
596
  }, [file, isImage]);
597
-
598
- // All files have consistent height: h-16 (64px)
599
- // Image files: square card (w-16 h-16), no filename
600
  if (isImage) {
601
  return (
602
  <div
@@ -604,7 +585,6 @@ export function ChatArea({
604
  onClick={onPreview}
605
  style={{ width: '64px', height: '64px', flexShrink: 0 }}
606
  >
607
- {/* Square image thumbnail card - ChatGPT style, same height as other files */}
608
  <div className="w-full h-full relative bg-card border border-border rounded-lg hover:border-primary/50 transition-colors">
609
  <div className="w-full h-full overflow-hidden rounded-lg absolute inset-0">
610
  {imageLoading ? (
@@ -612,8 +592,8 @@ export function ChatArea({
612
  <Icon className="h-5 w-5 text-muted-foreground animate-pulse" />
613
  </div>
614
  ) : imagePreview ? (
615
- <img
616
- src={imagePreview}
617
  alt={file.name}
618
  className="w-full h-full object-cover"
619
  onError={(e) => {
@@ -627,7 +607,6 @@ export function ChatArea({
627
  </div>
628
  )}
629
  </div>
630
- {/* Remove button - top right corner inside card, always visible - ChatGPT style */}
631
  <button
632
  type="button"
633
  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"
@@ -637,9 +616,9 @@ export function ChatArea({
637
  }}
638
  style={{ zIndex: 100, position: 'absolute', top: '4px', right: '4px' }}
639
  >
640
- <X
641
- className="h-2.5 w-2.5"
642
- style={{
643
  color: 'rgb(0, 0, 0)',
644
  strokeWidth: 2
645
  }}
@@ -649,23 +628,18 @@ export function ChatArea({
649
  </div>
650
  );
651
  }
652
-
653
- // Other files: horizontal rectangle with icon, filename and type (same height as images, fixed width)
654
  return (
655
  <div
656
  className="relative cursor-pointer"
657
  onClick={onPreview}
658
  style={{ width: '240px', flexShrink: 0 }}
659
  >
660
- {/* Horizontal file card - ChatGPT style, same height as images, fixed width */}
661
  <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">
662
- {/* File icon with colored background */}
663
  <div className={`${fileInfo.bgColor} flex items-center justify-center w-10 h-10 rounded shrink-0`}>
664
  <Icon className="h-5 w-5 text-white" />
665
  </div>
666
- {/* Spacing between icon and filename - one character width */}
667
  <div className="w-2 shrink-0" />
668
- {/* File name and type */}
669
  <div className="flex-1 min-w-0 flex flex-col justify-center pr-7">
670
  <p className="text-xs text-foreground truncate" title={file.name}>
671
  {file.name}
@@ -674,7 +648,6 @@ export function ChatArea({
674
  {fileInfo.type}
675
  </p>
676
  </div>
677
- {/* Remove button - top right corner inside card, always visible - ChatGPT style */}
678
  <button
679
  type="button"
680
  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"
@@ -684,9 +657,9 @@ export function ChatArea({
684
  }}
685
  style={{ position: 'absolute', top: '4px', right: '4px', zIndex: 10 }}
686
  >
687
- <X
688
- className="h-2.5 w-2.5"
689
- style={{
690
  color: 'rgb(0, 0, 0)',
691
  strokeWidth: 2
692
  }}
@@ -697,7 +670,6 @@ export function ChatArea({
697
  );
698
  };
699
 
700
- // File Viewer Content Component
701
  const FileViewerContent = ({ file }: { file: File }) => {
702
  const [content, setContent] = useState<string>('');
703
  const [loading, setLoading] = useState(true);
@@ -708,11 +680,10 @@ export function ChatArea({
708
  try {
709
  setLoading(true);
710
  setError(null);
711
-
712
  const ext = file.name.toLowerCase();
713
-
714
  if (['.jpg', '.jpeg', '.png', '.gif', '.webp'].some(e => ext.endsWith(e))) {
715
- // Image file
716
  const reader = new FileReader();
717
  reader.onload = (e) => {
718
  setContent(e.target?.result as string);
@@ -724,11 +695,9 @@ export function ChatArea({
724
  };
725
  reader.readAsDataURL(file);
726
  } else if (ext.endsWith('.pdf')) {
727
- // PDF file - show info
728
  setContent('PDF files cannot be previewed directly. Please download the file to view it.');
729
  setLoading(false);
730
  } else {
731
- // Text-based files
732
  const reader = new FileReader();
733
  reader.onload = (e) => {
734
  setContent(e.target?.result as string);
@@ -749,13 +718,8 @@ export function ChatArea({
749
  loadFile();
750
  }, [file]);
751
 
752
- if (loading) {
753
- return <div className="text-center py-8">Loading...</div>;
754
- }
755
-
756
- if (error) {
757
- return <div className="text-center py-8 text-destructive">{error}</div>;
758
- }
759
 
760
  const ext = file.name.toLowerCase();
761
  const isImage = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].some(e => ext.endsWith(e));
@@ -775,237 +739,205 @@ export function ChatArea({
775
  );
776
  };
777
 
778
- const getFileTypeLabel = (type: FileType) => {
779
- const labels: Record<FileType, string> = {
780
- 'syllabus': 'Syllabus',
781
- 'lecture-slides': 'Lecture Slides / PPT',
782
- 'literature-review': 'Literature Review / Paper',
783
- 'other': 'Other Course Document',
784
- };
785
- return labels[type];
786
- };
787
-
788
  return (
789
  <div className="flex flex-col h-full min-h-0 overflow-hidden" style={{ overscrollBehavior: 'none' }}>
790
- {/* Chat Area with Floating Input */}
791
  <div className="flex-1 relative min-h-0 flex flex-col overflow-hidden" style={{ overscrollBehavior: 'none' }}>
792
- {/* Messages Area */}
793
- <div
794
  ref={scrollContainerRef}
795
- className="flex-1 overflow-y-auto"
796
- style={{
797
- overscrollBehavior: 'none',
798
  }}
799
  >
800
- {/* Top Bar - Course Selector, Chat Mode Tabs, and Action Buttons - Sticky at top */}
801
- <div
802
- className={`sticky top-0 flex items-center justify-between px-4 z-20 bg-card ${
803
- showTopBorder ? 'border-b border-border' : ''
804
- }`}
805
- style={{
806
  height: '4.5rem',
807
  margin: 0,
808
  padding: '1rem 1rem',
809
  boxSizing: 'border-box',
810
  }}
811
  >
812
- {/* Course Selector - Left */}
813
- <div className="flex-shrink-0">
814
- {(() => {
815
- const current = workspaces.find(w => w.id === currentWorkspaceId);
816
- if (current?.type === 'group') {
817
- if (current.category === 'course' && current.courseName) {
818
- // Show fixed course label, not selectable
819
- return (
820
- <div className="h-9 px-3 inline-flex items-center rounded-md border font-semibold">
821
- {current.courseName}
822
- </div>
823
- );
 
824
  }
825
- // Personal interest: hide selector
826
- return null;
827
- }
828
- // Individual workspace: show selectable courses
829
- return (
830
- <Select value={currentCourseId || 'course1'} onValueChange={(val) => onCourseChange && onCourseChange(val)}>
831
- <SelectTrigger className="w-[200px] h-9 font-semibold">
832
- <SelectValue placeholder="Select course" />
833
- </SelectTrigger>
834
- <SelectContent>
835
- {courses.map((course) => (
836
- <SelectItem key={course.id} value={course.id}>
837
- {course.name}
838
- </SelectItem>
839
- ))}
840
- </SelectContent>
841
- </Select>
842
- );
843
- })()}
844
- </div>
845
 
846
- {/* Chat Mode Tabs - Center (aligned with panel toggle button) */}
847
- <div className="absolute left-1/2 -translate-x-1/2 flex-shrink-0">
848
- <Tabs
849
- value={chatMode}
850
- onValueChange={(value) => onChatModeChange(value as ChatMode)}
851
- className="w-auto"
852
- orientation="horizontal"
853
- >
854
- <TabsList className="inline-flex h-8 items-center justify-center rounded-xl bg-muted p-1 text-muted-foreground">
855
- <TabsTrigger value="ask" className="w-[140px] px-3 text-sm">Ask</TabsTrigger>
856
- <TabsTrigger value="review" className="w-[140px] px-3 text-sm relative">
857
- Review
858
- {/* Red dot badge in top-right corner */}
859
- <span
860
- className="absolute top-0 right-0 bg-red-500 rounded-full border-2"
861
- style={{
862
- width: '10px',
863
- height: '10px',
864
- transform: 'translate(25%, -25%)',
865
- zIndex: 10,
866
- borderColor: 'var(--muted)'
867
- }}
868
- />
869
- </TabsTrigger>
870
- <TabsTrigger value="quiz" className="w-[140px] px-3 text-sm">Quiz</TabsTrigger>
871
- </TabsList>
872
- </Tabs>
873
- </div>
874
 
875
- {/* Action Buttons - Right */}
876
- <div className="flex items-center gap-2 flex-shrink-0">
877
- <Button
878
- variant="ghost"
879
- size="icon"
880
- onClick={handleSaveClick}
881
- disabled={!isLoggedIn}
882
- className={`h-8 w-8 rounded-md hover:bg-muted/50 ${
883
- isCurrentChatSaved() ? 'text-primary' : ''
884
- }`}
885
- title={isCurrentChatSaved() ? 'Unsave' : 'Save'}
886
- >
887
- <Bookmark
888
- className={`h-4 w-4 ${isCurrentChatSaved() ? 'fill-primary text-primary' : ''}`}
889
- />
890
- </Button>
891
- <Button
892
- variant="ghost"
893
- size="icon"
894
- onClick={handleOpenDownloadDialog}
895
- disabled={!isLoggedIn}
896
- className="h-8 w-8 rounded-md hover:bg-muted/50"
897
- title="Download"
898
- >
899
- <Download className="h-4 w-4" />
900
- </Button>
901
- <Button
902
- variant="ghost"
903
- size="icon"
904
- onClick={handleShareClick}
905
- disabled={!isLoggedIn}
906
- className="h-8 w-8 rounded-md hover:bg-muted/50"
907
- title="Share"
908
- >
909
- <Share2 className="h-4 w-4" />
910
- </Button>
911
- <Button
912
- variant="outline"
913
- onClick={handleClearClick}
914
- disabled={!isLoggedIn}
915
- className="h-8 px-3 gap-2 rounded-md border border-border disabled:opacity-60 !bg-[var(--card)] !text-[var(--card-foreground)] hover:!opacity-90 [&_svg]:!text-[var(--card-foreground)] [&_span]:!text-[var(--card-foreground)]"
916
- title="New Chat"
917
- >
918
- <Plus className="h-4 w-4" />
919
- <span className="text-sm font-medium">New chat</span>
920
- </Button>
921
  </div>
922
- </div>
923
 
924
  {/* Messages Content */}
925
- <div
926
  className="py-6"
927
- style={{
928
- paddingBottom: '12rem' // Ensure enough space for input box and action buttons
929
  }}
930
  >
931
- <div className="w-full space-y-6 max-w-4xl mx-auto">
932
- {messages.map((message, index) => (
933
- <React.Fragment key={message.id}>
934
- <Message
935
- message={message}
936
- showSenderInfo={spaceType === 'group'}
937
- isFirstGreeting={
938
- (message.id === '1' || message.id === 'review-1' || message.id === 'quiz-1') &&
939
- message.role === 'assistant'
940
- }
941
- showNextButton={message.showNextButton && !isAppTyping}
942
- onNextQuestion={onNextQuestion}
943
- chatMode={chatMode}
944
- />
945
- {/* Smart Review - Show below welcome message in Review mode */}
946
- {chatMode === 'review' &&
947
- message.id === 'review-1' &&
948
- message.role === 'assistant' && (
949
- <div className="flex gap-2 justify-start px-4">
950
- {/* Avatar placeholder to align with message bubble */}
951
- <div className="w-10 h-10 flex-shrink-0"></div>
952
- <div
953
- className="w-full"
954
- style={{ maxWidth: 'min(770px, calc(100% - 2rem))' }}
955
- >
956
- <SmartReview
957
- onReviewTopic={handleReviewTopic}
958
- onReviewAll={handleReviewAll}
959
- />
960
- </div>
961
- </div>
962
- )}
963
- {/* Quiz Mode Start Button - Below welcome message */}
964
- {chatMode === 'quiz' &&
965
- message.id === 'quiz-1' &&
966
- message.role === 'assistant' &&
967
- quizState.currentQuestion === 0 &&
968
- !quizState.waitingForAnswer &&
969
- !isAppTyping && (
970
- <div className="flex justify-center py-4">
971
- <Button
972
- onClick={onStartQuiz}
973
- className="bg-red-500 hover:bg-red-600 text-white"
974
- >
975
- Start Quiz
976
- </Button>
977
  </div>
978
- )}
979
- </React.Fragment>
980
- ))}
981
-
982
- {/* Show typing indicator - use isAppTyping for all modes to ensure consistent display */}
983
- {isAppTyping && (
984
- <div className="flex gap-2 justify-start px-4">
985
- <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
986
- <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
987
- </div>
988
- <div className="bg-muted rounded-2xl px-4 py-3">
989
- <div className="flex gap-1">
990
- <div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: '0ms' }} />
991
- <div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: '150ms' }} />
992
- <div className="w-2 h-2 rounded-full bg-muted-foreground/50 animate-bounce" style={{ animationDelay: '300ms' }} />
993
  </div>
994
  </div>
995
- </div>
996
- )}
997
-
998
- <div ref={messagesEndRef} />
999
  </div>
1000
  </div>
1001
- </div>
1002
 
1003
- {/* Scroll to Bottom Button - Above input box */}
1004
  {showScrollButton && (
1005
- <div
1006
  className="fixed z-30 flex justify-center pointer-events-none"
1007
- style={{
1008
- bottom: '120px', // Position 20px above input box (input box height ~100px)
1009
  left: leftPanelVisible ? '320px' : '0px',
1010
  right: '0px'
1011
  }}
@@ -1022,25 +954,24 @@ export function ChatArea({
1022
  </div>
1023
  )}
1024
 
1025
- {/* Floating Input Area - Fixed at viewport bottom, horizontally centered in chat area */}
1026
- <div
1027
  className="fixed bottom-0 bg-background/95 backdrop-blur-sm z-10"
1028
- style={{
1029
  left: leftPanelVisible ? '320px' : '0px',
1030
  right: '0px'
1031
  }}
1032
  >
1033
  <div className="max-w-4xl mx-auto px-4 py-4">
1034
- {/* Uploaded Files Display */}
1035
  {uploadedFiles.length > 0 && (
1036
  <div className="mb-2 flex flex-wrap gap-2 max-h-32 overflow-y-auto">
1037
  {uploadedFiles.map((uploadedFile, index) => {
1038
  const Icon = getFileIcon(uploadedFile.file.name);
1039
  const fileInfo = getFileTypeInfo(uploadedFile.file.name);
1040
- const isImage = ['jpg', 'jpeg', 'png', 'gif', 'webp'].some(ext =>
1041
  uploadedFile.file.name.toLowerCase().endsWith(`.${ext}`)
1042
  );
1043
-
1044
  return (
1045
  <div key={index}>
1046
  <FileThumbnail
@@ -1064,7 +995,7 @@ export function ChatArea({
1064
  </div>
1065
  )}
1066
 
1067
- <form
1068
  onSubmit={handleSubmit}
1069
  onDragOver={handleDragOver}
1070
  onDragLeave={handleDragLeave}
@@ -1072,9 +1003,8 @@ export function ChatArea({
1072
  className={isDragging ? 'opacity-75' : ''}
1073
  >
1074
  <div className="relative">
1075
- {/* Mode Selector and Upload Button - ChatGPT style at bottom left */}
1076
  <div className="absolute bottom-3 left-2 flex items-center gap-1 z-10">
1077
- {/* Only show mode selector in ask mode */}
1078
  {chatMode === 'ask' && (
1079
  <DropdownMenu>
1080
  <DropdownMenuTrigger asChild>
@@ -1086,92 +1016,52 @@ export function ChatArea({
1086
  type="button"
1087
  >
1088
  <span>{modeLabels[learningMode]}</span>
1089
- <svg
1090
- className="h-3 w-3 opacity-50"
1091
- fill="none"
1092
- stroke="currentColor"
1093
- viewBox="0 0 24 24"
1094
- >
1095
- <path
1096
- strokeLinecap="round"
1097
- strokeLinejoin="round"
1098
- strokeWidth={2}
1099
- d="M19 9l-7 7-7-7"
1100
- />
1101
  </svg>
1102
  </Button>
1103
  </DropdownMenuTrigger>
1104
  <DropdownMenuContent align="start" className="w-56">
1105
- <DropdownMenuItem
1106
- onClick={() => onLearningModeChange('general')}
1107
- className={learningMode === 'general' ? 'bg-accent' : ''}
1108
- >
1109
  <div className="flex flex-col">
1110
  <span className="font-medium">General</span>
1111
- <span className="text-xs text-muted-foreground">
1112
- Answer various questions (context required)
1113
- </span>
1114
  </div>
1115
  </DropdownMenuItem>
1116
- <DropdownMenuItem
1117
- onClick={() => onLearningModeChange('concept')}
1118
- className={learningMode === 'concept' ? 'bg-accent' : ''}
1119
- >
1120
  <div className="flex flex-col">
1121
  <span className="font-medium">Concept Explainer</span>
1122
- <span className="text-xs text-muted-foreground">
1123
- Get detailed explanations of concepts
1124
- </span>
1125
  </div>
1126
  </DropdownMenuItem>
1127
- <DropdownMenuItem
1128
- onClick={() => onLearningModeChange('socratic')}
1129
- className={learningMode === 'socratic' ? 'bg-accent' : ''}
1130
- >
1131
  <div className="flex flex-col">
1132
  <span className="font-medium">Socratic Tutor</span>
1133
- <span className="text-xs text-muted-foreground">
1134
- Learn through guided questions
1135
- </span>
1136
  </div>
1137
  </DropdownMenuItem>
1138
- <DropdownMenuItem
1139
- onClick={() => onLearningModeChange('exam')}
1140
- className={learningMode === 'exam' ? 'bg-accent' : ''}
1141
- >
1142
  <div className="flex flex-col">
1143
  <span className="font-medium">Exam Prep</span>
1144
- <span className="text-xs text-muted-foreground">
1145
- Practice with quiz questions
1146
- </span>
1147
  </div>
1148
  </DropdownMenuItem>
1149
- <DropdownMenuItem
1150
- onClick={() => onLearningModeChange('assignment')}
1151
- className={learningMode === 'assignment' ? 'bg-accent' : ''}
1152
- >
1153
  <div className="flex flex-col">
1154
  <span className="font-medium">Assignment Helper</span>
1155
- <span className="text-xs text-muted-foreground">
1156
- Get help with assignments
1157
- </span>
1158
  </div>
1159
  </DropdownMenuItem>
1160
- <DropdownMenuItem
1161
- onClick={() => onLearningModeChange('summary')}
1162
- className={learningMode === 'summary' ? 'bg-accent' : ''}
1163
- >
1164
  <div className="flex flex-col">
1165
  <span className="font-medium">Quick Summary</span>
1166
- <span className="text-xs text-muted-foreground">
1167
- Get concise summaries
1168
- </span>
1169
  </div>
1170
  </DropdownMenuItem>
1171
  </DropdownMenuContent>
1172
  </DropdownMenu>
1173
  )}
1174
- {/* Upload Button - Right of mode selector */}
1175
  <Button
1176
  type="button"
1177
  size="icon"
@@ -1184,6 +1074,7 @@ export function ChatArea({
1184
  <Upload className="h-4 w-4" />
1185
  </Button>
1186
  </div>
 
1187
  <Textarea
1188
  value={input}
1189
  onChange={(e) => setInput(e.target.value)}
@@ -1192,22 +1083,21 @@ export function ChatArea({
1192
  !isLoggedIn
1193
  ? "Please log in on the right to start chatting..."
1194
  : chatMode === 'quiz'
1195
- ? quizState.waitingForAnswer
1196
- ? "Type your answer here..."
1197
- : quizState.currentQuestion > 0
1198
- ? "Click 'Next Question' to continue..."
1199
- : "Click 'Start Quiz' to begin..."
1200
- : spaceType === 'group'
1201
- ? "Type a message or drag files here... (mention @Clare to get AI assistance)"
1202
- : learningMode === 'general'
1203
- ? "Ask me anything! Please provide context about your question..."
1204
- : "Ask Clare anything about the course or drag files here..."
1205
  }
1206
  disabled={!isLoggedIn || (chatMode === 'quiz' && !quizState.waitingForAnswer)}
1207
- className={`min-h-[80px] pl-4 pr-20 resize-none bg-background border-2 ${
1208
- isDragging ? 'border-primary border-dashed' : 'border-border'
1209
- }`}
1210
  />
 
1211
  <div className="absolute bottom-2 right-2 flex gap-1">
1212
  <Button
1213
  type="submit"
@@ -1218,6 +1108,7 @@ export function ChatArea({
1218
  <Send className="h-4 w-4" />
1219
  </Button>
1220
  </div>
 
1221
  <input
1222
  ref={fileInputRef}
1223
  type="file"
@@ -1241,7 +1132,6 @@ export function ChatArea({
1241
  <AlertDialogDescription>
1242
  Would you like to save the current chat before starting a new conversation?
1243
  </AlertDialogDescription>
1244
- {/* Close button in top-right corner */}
1245
  <Button
1246
  variant="ghost"
1247
  size="icon"
@@ -1253,11 +1143,7 @@ export function ChatArea({
1253
  </Button>
1254
  </AlertDialogHeader>
1255
  <AlertDialogFooter className="flex-col sm:flex-row gap-2 sm:justify-end">
1256
- <Button
1257
- variant="outline"
1258
- onClick={() => onConfirmClear(false)}
1259
- className="sm:flex-1 sm:max-w-[200px]"
1260
- >
1261
  Start New (Don't Save)
1262
  </Button>
1263
  <AlertDialogAction onClick={() => onConfirmClear(true)} className="sm:flex-1 sm:max-w-[200px]">
@@ -1274,35 +1160,27 @@ export function ChatArea({
1274
  <DialogTitle>Download this chat</DialogTitle>
1275
  <DialogDescription>Preview and copy before downloading.</DialogDescription>
1276
  </DialogHeader>
1277
-
1278
- {/* Tab Selection */}
1279
- <Tabs value={downloadTab} onValueChange={(value) => {
1280
- setDownloadTab(value as 'chat' | 'summary');
1281
- setDownloadPreview(value === 'chat' ? buildPreviewContent() : buildSummaryContent());
1282
- // Auto-check options based on selected tab
1283
- if (value === 'summary') {
1284
- setDownloadOptions({ chat: false, summary: true });
1285
- } else {
1286
- setDownloadOptions({ chat: true, summary: false });
1287
- }
1288
- }} className="w-full">
1289
  <TabsList className="grid w-full grid-cols-2">
1290
  <TabsTrigger value="chat">Download chat</TabsTrigger>
1291
  <TabsTrigger value="summary">Summary of the chat</TabsTrigger>
1292
  </TabsList>
1293
  </Tabs>
1294
-
1295
- {/* Preview Section */}
1296
  <div className="border rounded-lg bg-muted/40 flex flex-col max-h-64">
1297
  <div className="flex items-center justify-between p-4 sticky top-0 bg-muted/40 border-b z-10">
1298
  <span className="text-sm font-medium">Preview</span>
1299
- <Button
1300
- variant="outline"
1301
- size="sm"
1302
- className="h-7 px-2 text-xs gap-1.5"
1303
- onClick={handleCopyPreview}
1304
- title="Copy preview"
1305
- >
1306
  <Copy className="h-3 w-3" />
1307
  Copy
1308
  </Button>
@@ -1311,35 +1189,26 @@ export function ChatArea({
1311
  <div className="whitespace-pre-wrap">{downloadPreview}</div>
1312
  </div>
1313
  </div>
1314
-
1315
- {/* Download Options Checkboxes */}
1316
  <div className="space-y-3">
1317
  <div className="flex items-center space-x-2">
1318
  <Checkbox
1319
  id="download-chat"
1320
  checked={downloadOptions.chat}
1321
- onCheckedChange={(checked) => {
1322
- setDownloadOptions({ ...downloadOptions, chat: checked === true });
1323
- }}
1324
  />
1325
- <label htmlFor="download-chat" className="text-sm font-medium cursor-pointer">
1326
- Download chat
1327
- </label>
1328
  </div>
1329
  <div className="flex items-center space-x-2">
1330
  <Checkbox
1331
  id="download-summary"
1332
  checked={downloadOptions.summary}
1333
- onCheckedChange={(checked) => {
1334
- setDownloadOptions({ ...downloadOptions, summary: checked === true });
1335
- }}
1336
  />
1337
- <label htmlFor="download-summary" className="text-sm font-medium cursor-pointer">
1338
- Download summary
1339
- </label>
1340
  </div>
1341
  </div>
1342
-
1343
  <DialogFooter>
1344
  <Button variant="outline" onClick={() => setShowDownloadDialog(false)}>Cancel</Button>
1345
  <Button onClick={handleDownloadFile}>Download</Button>
@@ -1412,9 +1281,9 @@ export function ChatArea({
1412
  <Dialog open={showFileViewer} onOpenChange={setShowFileViewer}>
1413
  <DialogContent className="max-w-4xl max-h-[85vh] flex flex-col overflow-hidden">
1414
  <DialogHeader className="min-w-0 flex-shrink-0">
1415
- <DialogTitle
1416
  className="pr-8 break-words break-all overflow-wrap-anywhere leading-relaxed"
1417
- style={{
1418
  wordBreak: 'break-all',
1419
  overflowWrap: 'anywhere',
1420
  maxWidth: '100%',
@@ -1428,57 +1297,41 @@ export function ChatArea({
1428
  </DialogDescription>
1429
  </DialogHeader>
1430
  <div className="flex-1 min-h-0 overflow-y-auto mt-4">
1431
- {selectedFile && (
1432
- <FileViewerContent file={selectedFile.file} />
1433
- )}
1434
  </div>
1435
  </DialogContent>
1436
  </Dialog>
1437
 
1438
- {/* File Type Selection Dialog - Highest z-index */}
1439
  {showTypeDialog && (
1440
  <Dialog open={showTypeDialog} onOpenChange={setShowTypeDialog}>
1441
- <DialogContent
1442
- className="sm:max-w-[425px]"
1443
- overlayClassName="!z-[99998]"
1444
- style={{ zIndex: 99999 }}
1445
- >
1446
  <DialogHeader>
1447
  <DialogTitle>Select File Types</DialogTitle>
1448
- <DialogDescription>
1449
- Please select the type for each file you are uploading.
1450
- </DialogDescription>
1451
  </DialogHeader>
1452
  <div className="space-y-3 max-h-64 overflow-y-auto">
1453
  {pendingFiles.map((pendingFile, index) => {
1454
  const Icon = getFileIcon(pendingFile.file.name);
1455
  return (
1456
- <div
1457
- key={index}
1458
- className="p-3 bg-muted rounded-md space-y-2"
1459
- >
1460
  <div className="flex items-center gap-2 group">
1461
  <Icon className="h-4 w-4 text-muted-foreground flex-shrink-0" />
1462
  <div className="flex-1 min-w-0">
1463
  <p className="text-sm truncate">{pendingFile.file.name}</p>
1464
- <p className="text-xs text-muted-foreground">
1465
- {formatFileSize(pendingFile.file.size)}
1466
- </p>
1467
  </div>
1468
  </div>
1469
  <div className="space-y-1">
1470
  <label className="text-xs text-muted-foreground">File Type</label>
1471
- <Select
1472
- value={pendingFile.type}
1473
  onValueChange={(value) => handlePendingFileTypeChange(index, value as FileType)}
1474
  >
1475
  <SelectTrigger className="h-8 text-xs">
1476
  <SelectValue />
1477
  </SelectTrigger>
1478
- <SelectContent
1479
- className="!z-[100000] !bg-background !text-foreground"
1480
- style={{ zIndex: 100000 }}
1481
- >
1482
  <SelectItem value="syllabus">Syllabus</SelectItem>
1483
  <SelectItem value="lecture-slides">Lecture Slides / PPT</SelectItem>
1484
  <SelectItem value="literature-review">Literature Review / Paper</SelectItem>
@@ -1491,21 +1344,12 @@ export function ChatArea({
1491
  })}
1492
  </div>
1493
  <DialogFooter>
1494
- <Button
1495
- variant="outline"
1496
- onClick={handleCancelUpload}
1497
- >
1498
- Cancel
1499
- </Button>
1500
- <Button
1501
- onClick={handleConfirmUpload}
1502
- >
1503
- Upload
1504
- </Button>
1505
  </DialogFooter>
1506
  </DialogContent>
1507
  </Dialog>
1508
  )}
1509
  </div>
1510
  );
1511
- }
 
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,
14
+ File,
15
+ FileText,
16
+ Presentation,
17
+ Image as ImageIcon,
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';
 
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,
 
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);
 
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('');
 
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);
 
200
  }
201
  }, [messages]);
202
 
203
+ // Prevent scroll chaining to left panel / outer containers
204
  useEffect(() => {
205
+ const el = scrollContainerRef.current;
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
+
216
+ if ((atTop && e.deltaY < 0) || (atBottom && e.deltaY > 0)) {
217
+ e.preventDefault();
218
+ }
219
  };
220
+
221
+ el.addEventListener("wheel", onWheel, { passive: false });
222
+ return () => el.removeEventListener("wheel", onWheel);
223
  }, []);
224
 
225
  const handleSubmit = (e: React.FormEvent) => {
 
228
 
229
  onSendMessage(input);
230
  setInput('');
 
231
  };
232
 
233
  const handleKeyDown = (e: React.KeyboardEvent) => {
 
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
 
 
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`;
295
  summary += `- User Messages: ${userMessages.length}\n`;
296
  summary += `- Assistant Responses: ${assistantMessages.length}\n\n`;
297
+
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
 
 
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();
 
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');
 
380
  // Check if current chat is already saved
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 (
 
401
  toast.info('No conversation to save');
402
  return;
403
  }
404
+
405
  onSaveChat();
406
  };
407
 
 
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);
418
  setShowShareDialog(true);
419
  };
 
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
  });
 
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
  });
 
498
 
499
  const handleConfirmUpload = () => {
500
  onFileUpload(pendingFiles.map(pf => pf.file));
 
501
  const startIndex = uploadedFiles.length;
502
  pendingFiles.forEach((pf, idx) => {
503
  setTimeout(() => {
 
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) => {
 
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
 
 
542
  return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
543
  };
544
 
545
+ const FileThumbnail = ({
546
+ file,
547
+ Icon,
548
+ fileInfo,
549
+ isImage,
550
+ onPreview,
551
+ onRemove
552
+ }: {
553
+ file: File;
554
+ Icon: React.ComponentType<{ className?: string }>;
555
+ fileInfo: { bgColor: string; type: string };
556
+ isImage: boolean;
557
+ onPreview: () => void;
 
558
  onRemove: (e: React.MouseEvent) => void;
559
  }) => {
560
  const [imagePreview, setImagePreview] = useState<string | null>(null);
561
  const [imageLoading, setImageLoading] = useState(true);
562
+
563
  useEffect(() => {
564
  if (isImage) {
565
  setImageLoading(true);
 
577
  setImageLoading(false);
578
  }
579
  }, [file, isImage]);
580
+
 
 
581
  if (isImage) {
582
  return (
583
  <div
 
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">
590
  {imageLoading ? (
 
592
  <Icon className="h-5 w-5 text-muted-foreground animate-pulse" />
593
  </div>
594
  ) : imagePreview ? (
595
+ <img
596
+ src={imagePreview}
597
  alt={file.name}
598
  className="w-full h-full object-cover"
599
  onError={(e) => {
 
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"
 
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
  }}
 
628
  </div>
629
  );
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" />
641
  </div>
 
642
  <div className="w-2 shrink-0" />
 
643
  <div className="flex-1 min-w-0 flex flex-col justify-center pr-7">
644
  <p className="text-xs text-foreground truncate" title={file.name}>
645
  {file.name}
 
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"
 
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
  }}
 
670
  );
671
  };
672
 
 
673
  const FileViewerContent = ({ file }: { file: File }) => {
674
  const [content, setContent] = useState<string>('');
675
  const [loading, setLoading] = useState(true);
 
680
  try {
681
  setLoading(true);
682
  setError(null);
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);
 
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);
 
718
  loadFile();
719
  }, [file]);
720
 
721
+ if (loading) return <div className="text-center py-8">Loading...</div>;
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));
 
739
  );
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}
772
+ </div>
773
+ );
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>
782
+ <SelectContent>
783
+ {courses.map((course) => (
784
+ <SelectItem key={course.id} value={course.id}>
785
+ {course.name}
786
+ </SelectItem>
787
+ ))}
788
+ </SelectContent>
789
+ </Select>
790
+ );
791
+ })()}
792
+ </div>
 
 
 
 
793
 
794
+ {/* Chat Mode Tabs - Center */}
795
+ <div className="absolute left-1/2 -translate-x-1/2 flex-shrink-0">
796
+ <Tabs
797
+ value={chatMode}
798
+ onValueChange={(value) => onChatModeChange(value as ChatMode)}
799
+ className="w-auto"
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>
 
821
 
822
+ {/* Action Buttons - Right */}
823
+ <div className="flex items-center gap-2 flex-shrink-0">
824
+ <Button
825
+ variant="ghost"
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"
837
+ onClick={handleOpenDownloadDialog}
838
+ disabled={!isLoggedIn}
839
+ className="h-8 w-8 rounded-md hover:bg-muted/50"
840
+ title="Download"
841
+ >
842
+ <Download className="h-4 w-4" />
843
+ </Button>
844
+ <Button
845
+ variant="ghost"
846
+ size="icon"
847
+ onClick={handleShareClick}
848
+ disabled={!isLoggedIn}
849
+ className="h-8 w-8 rounded-md hover:bg-muted/50"
850
+ title="Share"
851
+ >
852
+ <Share2 className="h-4 w-4" />
853
+ </Button>
854
+ <Button
855
+ variant="outline"
856
+ onClick={handleClearClick}
857
+ disabled={!isLoggedIn}
858
+ className="h-8 px-3 gap-2 rounded-md border border-border disabled:opacity-60 !bg-[var(--card)] !text-[var(--card-foreground)] hover:!opacity-90 [&_svg]:!text-[var(--card-foreground)] [&_span]:!text-[var(--card-foreground)]"
859
+ title="New Chat"
860
+ >
861
+ <Plus className="h-4 w-4" />
862
+ <span className="text-sm font-medium">New chat</span>
863
+ </Button>
864
+ </div>
 
 
 
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 && (
906
+ <div className="flex justify-center py-4">
907
+ <Button onClick={onStartQuiz} className="bg-red-500 hover:bg-red-600 text-white">
908
+ Start Quiz
909
+ </Button>
910
+ </div>
911
+ )}
912
+ </React.Fragment>
913
+ ))}
914
+
915
+ {isAppTyping && (
916
+ <div className="flex gap-2 justify-start px-4">
917
+ <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
918
+ <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
 
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>
928
+ )}
929
+
930
+ <div ref={messagesEndRef} />
931
+ </div>
932
  </div>
933
  </div>
 
934
 
935
+ {/* Scroll to Bottom Button */}
936
  {showScrollButton && (
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
  }}
 
954
  </div>
955
  )}
956
 
957
+ {/* Floating Input Area */}
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">
 
966
  {uploadedFiles.length > 0 && (
967
  <div className="mb-2 flex flex-wrap gap-2 max-h-32 overflow-y-auto">
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
+
975
  return (
976
  <div key={index}>
977
  <FileThumbnail
 
995
  </div>
996
  )}
997
 
998
+ <form
999
  onSubmit={handleSubmit}
1000
  onDragOver={handleDragOver}
1001
  onDragLeave={handleDragLeave}
 
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>
 
1016
  type="button"
1017
  >
1018
  <span>{modeLabels[learningMode]}</span>
1019
+ <svg className="h-3 w-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1020
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
 
 
 
 
 
 
 
 
 
 
1021
  </svg>
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>
 
 
1059
  </div>
1060
  </DropdownMenuItem>
1061
  </DropdownMenuContent>
1062
  </DropdownMenu>
1063
  )}
1064
+
1065
  <Button
1066
  type="button"
1067
  size="icon"
 
1074
  <Upload className="h-4 w-4" />
1075
  </Button>
1076
  </div>
1077
+
1078
  <Textarea
1079
  value={input}
1080
  onChange={(e) => setInput(e.target.value)}
 
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"
 
1108
  <Send className="h-4 w-4" />
1109
  </Button>
1110
  </div>
1111
+
1112
  <input
1113
  ref={fileInputRef}
1114
  type="file"
 
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"
 
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)
1148
  </Button>
1149
  <AlertDialogAction onClick={() => onConfirmClear(true)} className="sm:flex-1 sm:max-w-[200px]">
 
1160
  <DialogTitle>Download this chat</DialogTitle>
1161
  <DialogDescription>Preview and copy before downloading.</DialogDescription>
1162
  </DialogHeader>
1163
+
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"
1173
+ >
 
1174
  <TabsList className="grid w-full grid-cols-2">
1175
  <TabsTrigger value="chat">Download chat</TabsTrigger>
1176
  <TabsTrigger value="summary">Summary of the chat</TabsTrigger>
1177
  </TabsList>
1178
  </Tabs>
1179
+
 
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>
 
1189
  <div className="whitespace-pre-wrap">{downloadPreview}</div>
1190
  </div>
1191
  </div>
1192
+
 
1193
  <div className="space-y-3">
1194
  <div className="flex items-center space-x-2">
1195
  <Checkbox
1196
  id="download-chat"
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
1204
  id="download-summary"
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>
 
1281
  <Dialog open={showFileViewer} onOpenChange={setShowFileViewer}>
1282
  <DialogContent className="max-w-4xl max-h-[85vh] flex flex-col overflow-hidden">
1283
  <DialogHeader className="min-w-0 flex-shrink-0">
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%',
 
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
 
1305
+ {/* File Type Selection Dialog */}
1306
  {showTypeDialog && (
1307
  <Dialog open={showTypeDialog} onOpenChange={setShowTypeDialog}>
1308
+ <DialogContent className="sm:max-w-[425px]" style={{ zIndex: 99999 }}>
 
 
 
 
1309
  <DialogHeader>
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);
1316
  return (
1317
+ <div key={index} className="p-3 bg-muted rounded-md space-y-2">
 
 
 
1318
  <div className="flex items-center gap-2 group">
1319
  <Icon className="h-4 w-4 text-muted-foreground flex-shrink-0" />
1320
  <div className="flex-1 min-w-0">
1321
  <p className="text-sm truncate">{pendingFile.file.name}</p>
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
1328
+ value={pendingFile.type}
1329
  onValueChange={(value) => handlePendingFileTypeChange(index, value as FileType)}
1330
  >
1331
  <SelectTrigger className="h-8 text-xs">
1332
  <SelectValue />
1333
  </SelectTrigger>
1334
+ <SelectContent className="!z-[100000] !bg-background !text-foreground" style={{ zIndex: 100000 }}>
 
 
 
1335
  <SelectItem value="syllabus">Syllabus</SelectItem>
1336
  <SelectItem value="lecture-slides">Lecture Slides / PPT</SelectItem>
1337
  <SelectItem value="literature-review">Literature Review / Paper</SelectItem>
 
1344
  })}
1345
  </div>
1346
  <DialogFooter>
1347
+ <Button variant="outline" onClick={handleCancelUpload}>Cancel</Button>
1348
+ <Button onClick={handleConfirmUpload}>Upload</Button>
 
 
 
 
 
 
 
 
 
1349
  </DialogFooter>
1350
  </DialogContent>
1351
  </Dialog>
1352
  )}
1353
  </div>
1354
  );
1355
+ }