SarahXia0405 commited on
Commit
6fd4b72
·
verified ·
1 Parent(s): 54a2b2a

Update web/src/components/ChatArea.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/ChatArea.tsx +40 -72
web/src/components/ChatArea.tsx CHANGED
@@ -1,12 +1,10 @@
1
  import React, { useState, useRef, useEffect } from 'react';
2
  import { Button } from './ui/button';
3
  import { Textarea } from './ui/textarea';
4
- import { Send, ArrowDown, AlertCircle, Trash2, Share2 } from 'lucide-react';
5
  import { Message } from './Message';
6
  import { FileUploadArea } from './FileUploadArea';
7
  import { MemoryLine } from './MemoryLine';
8
- import { Alert, AlertDescription } from './ui/alert';
9
- import { Badge } from './ui/badge';
10
  import type { Message as MessageType, LearningMode, UploadedFile, FileType, SpaceType } from '../App';
11
  import { toast } from 'sonner';
12
  import {
@@ -18,7 +16,7 @@ import {
18
 
19
  interface ChatAreaProps {
20
  messages: MessageType[];
21
- onSendMessage: (content: string) => void;
22
  uploadedFiles: UploadedFile[];
23
  onFileUpload: (files: File[]) => void;
24
  onRemoveFile: (index: number) => void;
@@ -29,6 +27,7 @@ interface ChatAreaProps {
29
  onClearConversation: () => void;
30
  onLearningModeChange: (mode: LearningMode) => void;
31
  spaceType: SpaceType;
 
32
  }
33
 
34
  export function ChatArea({
@@ -44,9 +43,9 @@ export function ChatArea({
44
  onClearConversation,
45
  onLearningModeChange,
46
  spaceType,
 
47
  }: ChatAreaProps) {
48
  const [input, setInput] = useState('');
49
- const [isTyping, setIsTyping] = useState(false);
50
  const [showScrollButton, setShowScrollButton] = useState(false);
51
  const messagesEndRef = useRef<HTMLDivElement>(null);
52
  const scrollContainerRef = useRef<HTMLDivElement>(null);
@@ -57,7 +56,7 @@ export function ChatArea({
57
 
58
  useEffect(() => {
59
  scrollToBottom();
60
- }, [messages]);
61
 
62
  useEffect(() => {
63
  const handleScroll = () => {
@@ -72,20 +71,19 @@ export function ChatArea({
72
  return () => container?.removeEventListener('scroll', handleScroll);
73
  }, []);
74
 
75
- const handleSubmit = (e: React.FormEvent) => {
76
  e.preventDefault();
77
  if (!input.trim() || !isLoggedIn) return;
78
 
79
- onSendMessage(input);
80
  setInput('');
81
- setIsTyping(true);
82
- setTimeout(() => setIsTyping(false), 1500);
83
  };
84
 
85
  const handleKeyDown = (e: React.KeyboardEvent) => {
86
  if (e.key === 'Enter' && !e.shiftKey) {
87
  e.preventDefault();
88
- handleSubmit(e);
89
  }
90
  };
91
 
@@ -102,10 +100,9 @@ export function ChatArea({
102
  toast.info('No conversation to clear');
103
  return;
104
  }
105
-
106
  if (window.confirm('Are you sure you want to clear the conversation? This cannot be undone.')) {
107
  onClearConversation();
108
- toast.success('Conversation cleared');
109
  }
110
  };
111
 
@@ -114,18 +111,15 @@ export function ChatArea({
114
  toast.info('No conversation to share');
115
  return;
116
  }
117
-
118
- // Create a shareable text version of the conversation
119
  const conversationText = messages
120
- .map(msg => `${msg.sender === 'user' ? 'You' : 'Clare'}: ${msg.content}`)
121
  .join('\n\n');
122
-
123
- // Copy to clipboard
124
- navigator.clipboard.writeText(conversationText).then(() => {
125
- toast.success('Conversation copied to clipboard!');
126
- }).catch(() => {
127
- toast.error('Failed to copy conversation');
128
- });
129
  };
130
 
131
  return (
@@ -159,19 +153,12 @@ export function ChatArea({
159
  )}
160
 
161
  {/* Messages Area */}
162
- <div
163
- ref={scrollContainerRef}
164
- className="h-full max-h-[600px] overflow-y-auto px-4 py-6 pb-36"
165
- >
166
  <div className="max-w-4xl mx-auto space-y-6">
167
  {messages.map((message) => (
168
- <Message
169
- key={message.id}
170
- message={message}
171
- showSenderInfo={spaceType === 'group'}
172
- />
173
  ))}
174
-
175
  {isTyping && (
176
  <div className="flex gap-3">
177
  <div className="w-8 h-8 rounded-full bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center flex-shrink-0">
@@ -186,7 +173,7 @@ export function ChatArea({
186
  </div>
187
  </div>
188
  )}
189
-
190
  <div ref={messagesEndRef} />
191
  </div>
192
  </div>
@@ -210,7 +197,7 @@ export function ChatArea({
210
  <div className="max-w-4xl mx-auto px-4 py-4">
211
  <form onSubmit={handleSubmit}>
212
  <div className="relative">
213
- {/* Mode Selector - ChatGPT style at bottom left */}
214
  <DropdownMenu>
215
  <DropdownMenuTrigger asChild>
216
  <Button
@@ -221,21 +208,12 @@ export function ChatArea({
221
  type="button"
222
  >
223
  <span>{modeLabels[learningMode]}</span>
224
- <svg
225
- className="h-3 w-3 opacity-50"
226
- fill="none"
227
- stroke="currentColor"
228
- viewBox="0 0 24 24"
229
- >
230
- <path
231
- strokeLinecap="round"
232
- strokeLinejoin="round"
233
- strokeWidth={2}
234
- d="M19 9l-7 7-7-7"
235
- />
236
  </svg>
237
  </Button>
238
  </DropdownMenuTrigger>
 
239
  <DropdownMenuContent align="start" className="w-56">
240
  <DropdownMenuItem
241
  onClick={() => onLearningModeChange('concept')}
@@ -243,53 +221,47 @@ export function ChatArea({
243
  >
244
  <div className="flex flex-col">
245
  <span className="font-medium">Concept Explainer</span>
246
- <span className="text-xs text-muted-foreground">
247
- Get detailed explanations of concepts
248
- </span>
249
  </div>
250
  </DropdownMenuItem>
 
251
  <DropdownMenuItem
252
  onClick={() => onLearningModeChange('socratic')}
253
  className={learningMode === 'socratic' ? 'bg-accent' : ''}
254
  >
255
  <div className="flex flex-col">
256
  <span className="font-medium">Socratic Tutor</span>
257
- <span className="text-xs text-muted-foreground">
258
- Learn through guided questions
259
- </span>
260
  </div>
261
  </DropdownMenuItem>
 
262
  <DropdownMenuItem
263
  onClick={() => onLearningModeChange('exam')}
264
  className={learningMode === 'exam' ? 'bg-accent' : ''}
265
  >
266
  <div className="flex flex-col">
267
  <span className="font-medium">Exam Prep</span>
268
- <span className="text-xs text-muted-foreground">
269
- Practice with quiz questions
270
- </span>
271
  </div>
272
  </DropdownMenuItem>
 
273
  <DropdownMenuItem
274
  onClick={() => onLearningModeChange('assignment')}
275
  className={learningMode === 'assignment' ? 'bg-accent' : ''}
276
  >
277
  <div className="flex flex-col">
278
  <span className="font-medium">Assignment Helper</span>
279
- <span className="text-xs text-muted-foreground">
280
- Get help with assignments
281
- </span>
282
  </div>
283
  </DropdownMenuItem>
 
284
  <DropdownMenuItem
285
  onClick={() => onLearningModeChange('summary')}
286
  className={learningMode === 'summary' ? 'bg-accent' : ''}
287
  >
288
  <div className="flex flex-col">
289
  <span className="font-medium">Quick Summary</span>
290
- <span className="text-xs text-muted-foreground">
291
- Get concise summaries
292
- </span>
293
  </div>
294
  </DropdownMenuItem>
295
  </DropdownMenuContent>
@@ -302,19 +274,15 @@ export function ChatArea({
302
  placeholder={
303
  isLoggedIn
304
  ? spaceType === 'group'
305
- ? "Type a message... (mention @Clare to get AI assistance)"
306
- : "Ask Clare anything about the course..."
307
- : "Please log in on the right to start chatting..."
308
  }
309
  disabled={!isLoggedIn}
310
  className="min-h-[80px] pl-4 pr-12 resize-none bg-background border-2 border-border"
311
  />
312
- <Button
313
- type="submit"
314
- size="icon"
315
- disabled={!input.trim() || !isLoggedIn}
316
- className="absolute bottom-2 right-2 rounded-full"
317
- >
318
  <Send className="h-4 w-4" />
319
  </Button>
320
  </div>
@@ -340,4 +308,4 @@ export function ChatArea({
340
  </div>
341
  </div>
342
  );
343
- }
 
1
  import React, { useState, useRef, useEffect } from 'react';
2
  import { Button } from './ui/button';
3
  import { Textarea } from './ui/textarea';
4
+ import { Send, ArrowDown, Trash2, Share2 } from 'lucide-react';
5
  import { Message } from './Message';
6
  import { FileUploadArea } from './FileUploadArea';
7
  import { MemoryLine } from './MemoryLine';
 
 
8
  import type { Message as MessageType, LearningMode, UploadedFile, FileType, SpaceType } from '../App';
9
  import { toast } from 'sonner';
10
  import {
 
16
 
17
  interface ChatAreaProps {
18
  messages: MessageType[];
19
+ onSendMessage: (content: string) => Promise<void> | void;
20
  uploadedFiles: UploadedFile[];
21
  onFileUpload: (files: File[]) => void;
22
  onRemoveFile: (index: number) => void;
 
27
  onClearConversation: () => void;
28
  onLearningModeChange: (mode: LearningMode) => void;
29
  spaceType: SpaceType;
30
+ isTyping: boolean;
31
  }
32
 
33
  export function ChatArea({
 
43
  onClearConversation,
44
  onLearningModeChange,
45
  spaceType,
46
+ isTyping,
47
  }: ChatAreaProps) {
48
  const [input, setInput] = useState('');
 
49
  const [showScrollButton, setShowScrollButton] = useState(false);
50
  const messagesEndRef = useRef<HTMLDivElement>(null);
51
  const scrollContainerRef = useRef<HTMLDivElement>(null);
 
56
 
57
  useEffect(() => {
58
  scrollToBottom();
59
+ }, [messages, isTyping]);
60
 
61
  useEffect(() => {
62
  const handleScroll = () => {
 
71
  return () => container?.removeEventListener('scroll', handleScroll);
72
  }, []);
73
 
74
+ const handleSubmit = async (e: React.FormEvent) => {
75
  e.preventDefault();
76
  if (!input.trim() || !isLoggedIn) return;
77
 
78
+ const text = input;
79
  setInput('');
80
+ await onSendMessage(text);
 
81
  };
82
 
83
  const handleKeyDown = (e: React.KeyboardEvent) => {
84
  if (e.key === 'Enter' && !e.shiftKey) {
85
  e.preventDefault();
86
+ void handleSubmit(e as any);
87
  }
88
  };
89
 
 
100
  toast.info('No conversation to clear');
101
  return;
102
  }
103
+
104
  if (window.confirm('Are you sure you want to clear the conversation? This cannot be undone.')) {
105
  onClearConversation();
 
106
  }
107
  };
108
 
 
111
  toast.info('No conversation to share');
112
  return;
113
  }
114
+
 
115
  const conversationText = messages
116
+ .map((msg) => `${msg.role === 'user' ? 'You' : 'Clare'}: ${msg.content}`)
117
  .join('\n\n');
118
+
119
+ navigator.clipboard
120
+ .writeText(conversationText)
121
+ .then(() => toast.success('Conversation copied to clipboard!'))
122
+ .catch(() => toast.error('Failed to copy conversation'));
 
 
123
  };
124
 
125
  return (
 
153
  )}
154
 
155
  {/* Messages Area */}
156
+ <div ref={scrollContainerRef} className="h-full max-h-[600px] overflow-y-auto px-4 py-6 pb-36">
 
 
 
157
  <div className="max-w-4xl mx-auto space-y-6">
158
  {messages.map((message) => (
159
+ <Message key={message.id} message={message} showSenderInfo={spaceType === 'group'} />
 
 
 
 
160
  ))}
161
+
162
  {isTyping && (
163
  <div className="flex gap-3">
164
  <div className="w-8 h-8 rounded-full bg-gradient-to-br from-purple-500 to-blue-500 flex items-center justify-center flex-shrink-0">
 
173
  </div>
174
  </div>
175
  )}
176
+
177
  <div ref={messagesEndRef} />
178
  </div>
179
  </div>
 
197
  <div className="max-w-4xl mx-auto px-4 py-4">
198
  <form onSubmit={handleSubmit}>
199
  <div className="relative">
200
+ {/* Mode Selector */}
201
  <DropdownMenu>
202
  <DropdownMenuTrigger asChild>
203
  <Button
 
208
  type="button"
209
  >
210
  <span>{modeLabels[learningMode]}</span>
211
+ <svg className="h-3 w-3 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
212
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
 
 
 
 
 
 
 
 
 
 
213
  </svg>
214
  </Button>
215
  </DropdownMenuTrigger>
216
+
217
  <DropdownMenuContent align="start" className="w-56">
218
  <DropdownMenuItem
219
  onClick={() => onLearningModeChange('concept')}
 
221
  >
222
  <div className="flex flex-col">
223
  <span className="font-medium">Concept Explainer</span>
224
+ <span className="text-xs text-muted-foreground">Get detailed explanations of concepts</span>
 
 
225
  </div>
226
  </DropdownMenuItem>
227
+
228
  <DropdownMenuItem
229
  onClick={() => onLearningModeChange('socratic')}
230
  className={learningMode === 'socratic' ? 'bg-accent' : ''}
231
  >
232
  <div className="flex flex-col">
233
  <span className="font-medium">Socratic Tutor</span>
234
+ <span className="text-xs text-muted-foreground">Learn through guided questions</span>
 
 
235
  </div>
236
  </DropdownMenuItem>
237
+
238
  <DropdownMenuItem
239
  onClick={() => onLearningModeChange('exam')}
240
  className={learningMode === 'exam' ? 'bg-accent' : ''}
241
  >
242
  <div className="flex flex-col">
243
  <span className="font-medium">Exam Prep</span>
244
+ <span className="text-xs text-muted-foreground">Practice with quiz questions</span>
 
 
245
  </div>
246
  </DropdownMenuItem>
247
+
248
  <DropdownMenuItem
249
  onClick={() => onLearningModeChange('assignment')}
250
  className={learningMode === 'assignment' ? 'bg-accent' : ''}
251
  >
252
  <div className="flex flex-col">
253
  <span className="font-medium">Assignment Helper</span>
254
+ <span className="text-xs text-muted-foreground">Get help with assignments</span>
 
 
255
  </div>
256
  </DropdownMenuItem>
257
+
258
  <DropdownMenuItem
259
  onClick={() => onLearningModeChange('summary')}
260
  className={learningMode === 'summary' ? 'bg-accent' : ''}
261
  >
262
  <div className="flex flex-col">
263
  <span className="font-medium">Quick Summary</span>
264
+ <span className="text-xs text-muted-foreground">Get concise summaries</span>
 
 
265
  </div>
266
  </DropdownMenuItem>
267
  </DropdownMenuContent>
 
274
  placeholder={
275
  isLoggedIn
276
  ? spaceType === 'group'
277
+ ? 'Type a message... (mention @Clare to get AI assistance)'
278
+ : 'Ask Clare anything about the course...'
279
+ : 'Please log in on the right to start chatting...'
280
  }
281
  disabled={!isLoggedIn}
282
  className="min-h-[80px] pl-4 pr-12 resize-none bg-background border-2 border-border"
283
  />
284
+
285
+ <Button type="submit" size="icon" disabled={!input.trim() || !isLoggedIn} className="absolute bottom-2 right-2 rounded-full">
 
 
 
 
286
  <Send className="h-4 w-4" />
287
  </Button>
288
  </div>
 
308
  </div>
309
  </div>
310
  );
311
+ }