SarahXia0405 commited on
Commit
a801d04
·
verified ·
1 Parent(s): c440346

Update web/src/components/Message.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/Message.tsx +158 -81
web/src/components/Message.tsx CHANGED
@@ -7,8 +7,7 @@ import {
7
  ChevronDown,
8
  ChevronUp,
9
  Check,
10
- Bot,
11
- X
12
  } from 'lucide-react';
13
  import { Badge } from './ui/badge';
14
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible';
@@ -17,22 +16,50 @@ import type { Message as MessageType } from '../App';
17
  import { toast } from 'sonner';
18
  import clareAvatar from '../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png';
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  interface MessageProps {
21
- key?: React.Key;
22
  message: MessageType;
23
  showSenderInfo?: boolean; // For group chat mode
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  }
25
 
26
  // 反馈标签选项
27
- const FEEDBACK_TAGS = {
28
- 'not-helpful': [
29
  'Code was incorrect',
30
  "Shouldn't have used Memory",
31
  "Don't like the personality",
32
  "Don't like the style",
33
  'Not factually correct',
34
  ],
35
- 'helpful': [
36
  'Accurate and helpful',
37
  'Clear explanation',
38
  'Good examples',
@@ -41,14 +68,28 @@ const FEEDBACK_TAGS = {
41
  ],
42
  };
43
 
44
- export function Message({ message, showSenderInfo = false }: MessageProps) {
45
- const [feedback, setFeedback] = useState<'helpful' | 'not-helpful' | null>(null);
 
 
 
 
 
 
 
 
 
 
46
  const [copied, setCopied] = useState(false);
47
  const [referencesOpen, setReferencesOpen] = useState(false);
48
  const [showFeedbackArea, setShowFeedbackArea] = useState(false);
49
- const [feedbackType, setFeedbackType] = useState<'helpful' | 'not-helpful' | null>(null);
 
 
 
50
  const [feedbackText, setFeedbackText] = useState('');
51
  const [selectedTags, setSelectedTags] = useState<string[]>([]);
 
52
 
53
  const isUser = message.role === 'user';
54
 
@@ -59,20 +100,21 @@ export function Message({ message, showSenderInfo = false }: MessageProps) {
59
  setTimeout(() => setCopied(false), 2000);
60
  };
61
 
62
- const handleFeedbackClick = (type: 'helpful' | 'not-helpful') => {
63
  if (feedback === type) {
64
- // 如果点击的是已选中的反馈,则关闭反馈区域
65
  setFeedback(null);
66
  setShowFeedbackArea(false);
67
  setFeedbackType(null);
68
  setFeedbackText('');
69
  setSelectedTags([]);
70
- } else {
71
- // 打开反馈区域
72
- setFeedback(type);
73
- setFeedbackType(type);
74
- setShowFeedbackArea(true);
75
  }
 
 
 
 
 
76
  };
77
 
78
  const handleFeedbackClose = () => {
@@ -80,46 +122,76 @@ export function Message({ message, showSenderInfo = false }: MessageProps) {
80
  setFeedbackType(null);
81
  setFeedbackText('');
82
  setSelectedTags([]);
83
- // 注意:不重置 feedback,保持点赞/点踩的状态
84
  };
85
 
86
  const handleTagToggle = (tag: string) => {
87
- setSelectedTags(prev =>
88
- prev.includes(tag)
89
- ? prev.filter(t => t !== tag)
90
- : [...prev, tag]
91
  );
92
  };
93
 
94
- const handleFeedbackSubmit = () => {
95
- // 这里可以发送反馈到后端
96
- const feedbackData = {
97
- type: feedbackType,
98
- tags: selectedTags,
99
- text: feedbackText,
100
- messageId: message.id || message.content.substring(0, 50),
101
- };
102
-
103
- console.log('Feedback submitted:', feedbackData);
104
- toast.success('感谢您的反馈!');
105
- handleFeedbackClose();
106
- // 保持反馈状态(点赞/点踩)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  };
108
 
109
  return (
110
  <div className={`flex gap-3 ${isUser && !showSenderInfo ? 'justify-end' : 'justify-start'}`}>
111
  {/* Avatar */}
112
  {showSenderInfo && message.sender ? (
113
- <div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
114
- message.sender.isAI
115
- ? 'overflow-hidden bg-white'
116
- : 'bg-muted'
117
- }`}>
118
  {message.sender.isAI ? (
119
  <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
120
  ) : (
121
  <span className="text-sm">
122
- {message.sender.name.split(' ').map(n => n[0]).join('').toUpperCase()}
 
 
 
 
123
  </span>
124
  )}
125
  </div>
@@ -135,7 +207,9 @@ export function Message({ message, showSenderInfo = false }: MessageProps) {
135
  <div className="flex items-center gap-2 px-1">
136
  <span className="text-xs">{message.sender.name}</span>
137
  {message.sender.isAI && (
138
- <Badge variant="secondary" className="text-xs h-4 px-1">AI</Badge>
 
 
139
  )}
140
  </div>
141
  )}
@@ -143,13 +217,31 @@ export function Message({ message, showSenderInfo = false }: MessageProps) {
143
  <div
144
  className={`
145
  rounded-2xl px-4 py-3
146
- ${isUser && !showSenderInfo
147
- ? 'bg-primary text-primary-foreground'
148
- : 'bg-muted'
149
- }
150
  `}
151
  >
152
- <p className="whitespace-pre-wrap">{message.content}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  </div>
154
 
155
  {/* References */}
@@ -157,11 +249,7 @@ export function Message({ message, showSenderInfo = false }: MessageProps) {
157
  <Collapsible open={referencesOpen} onOpenChange={setReferencesOpen}>
158
  <CollapsibleTrigger asChild>
159
  <Button variant="ghost" size="sm" className="gap-1 h-7 text-xs">
160
- {referencesOpen ? (
161
- <ChevronUp className="h-3 w-3" />
162
- ) : (
163
- <ChevronDown className="h-3 w-3" />
164
- )}
165
  {message.references.length} {message.references.length === 1 ? 'reference' : 'references'}
166
  </Button>
167
  </CollapsibleTrigger>
@@ -177,12 +265,7 @@ export function Message({ message, showSenderInfo = false }: MessageProps) {
177
 
178
  {/* Message Actions */}
179
  <div className="flex items-center gap-1">
180
- <Button
181
- variant="ghost"
182
- size="sm"
183
- className="h-7 gap-1"
184
- onClick={handleCopy}
185
- >
186
  {copied ? (
187
  <>
188
  <Check className="h-3 w-3" />
@@ -201,17 +284,22 @@ export function Message({ message, showSenderInfo = false }: MessageProps) {
201
  <Button
202
  variant="ghost"
203
  size="sm"
204
- className={`h-7 gap-1 ${feedback === 'helpful' ? 'bg-green-100 text-green-600 dark:bg-green-900/20' : ''}`}
 
 
205
  onClick={() => handleFeedbackClick('helpful')}
206
  >
207
  <ThumbsUp className="h-3 w-3" />
208
  <span className="text-xs">Helpful</span>
209
  </Button>
 
210
  <Button
211
  variant="ghost"
212
  size="sm"
213
- className={`h-7 gap-1 ${feedback === 'not-helpful' ? 'bg-red-100 text-red-600 dark:bg-red-900/20' : ''}`}
214
- onClick={() => handleFeedbackClick('not-helpful')}
 
 
215
  >
216
  <ThumbsDown className="h-3 w-3" />
217
  <span className="text-xs">Not helpful</span>
@@ -220,24 +308,17 @@ export function Message({ message, showSenderInfo = false }: MessageProps) {
220
  )}
221
  </div>
222
 
223
- {/* Feedback Area - 展开在消息下方 */}
224
  {!isUser && showFeedbackArea && feedbackType && (
225
  <div className="w-full mt-2 bg-gray-50 dark:bg-gray-800/50 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
226
  <div className="flex items-start justify-between mb-4">
227
- <h4 className="text-sm font-medium text-gray-900 dark:text-gray-100">
228
- Tell us more:
229
- </h4>
230
- <Button
231
- variant="ghost"
232
- size="sm"
233
- className="h-6 w-6 p-0"
234
- onClick={handleFeedbackClose}
235
- >
236
  <X className="h-4 w-4" />
237
  </Button>
238
  </div>
239
 
240
- {/* 标签按钮 */}
241
  <div className="flex flex-wrap gap-2 mb-4">
242
  {FEEDBACK_TAGS[feedbackType].map((tag) => (
243
  <Button
@@ -252,7 +333,7 @@ export function Message({ message, showSenderInfo = false }: MessageProps) {
252
  ))}
253
  </div>
254
 
255
- {/* 文本输入框 */}
256
  <Textarea
257
  className="min-h-[60px] mb-4 bg-white dark:bg-gray-900"
258
  value={feedbackText}
@@ -260,21 +341,17 @@ export function Message({ message, showSenderInfo = false }: MessageProps) {
260
  placeholder="Additional feedback (optional)..."
261
  />
262
 
263
- {/* 提交按钮 */}
264
  <div className="flex justify-end gap-2">
265
- <Button
266
- variant="outline"
267
- size="sm"
268
- onClick={handleFeedbackClose}
269
- >
270
  Cancel
271
  </Button>
272
  <Button
273
  size="sm"
274
  onClick={handleFeedbackSubmit}
275
- disabled={selectedTags.length === 0 && !feedbackText.trim()}
276
  >
277
- Submit
278
  </Button>
279
  </div>
280
  </div>
@@ -288,4 +365,4 @@ export function Message({ message, showSenderInfo = false }: MessageProps) {
288
  )}
289
  </div>
290
  );
291
- }
 
7
  ChevronDown,
8
  ChevronUp,
9
  Check,
10
+ X,
 
11
  } from 'lucide-react';
12
  import { Badge } from './ui/badge';
13
  import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible';
 
16
  import { toast } from 'sonner';
17
  import clareAvatar from '../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png';
18
 
19
+ // ✅ Markdown rendering (NEW)
20
+ import ReactMarkdown from 'react-markdown';
21
+ import remarkGfm from 'remark-gfm';
22
+
23
+ // ✅ NEW: call backend feedback API
24
+ import {
25
+ apiFeedback,
26
+ type User as ApiUser,
27
+ type LearningMode as ApiLearningMode,
28
+ type FileType as ApiFileType,
29
+ type FeedbackRating,
30
+ } from '../lib/api';
31
+
32
  interface MessageProps {
 
33
  message: MessageType;
34
  showSenderInfo?: boolean; // For group chat mode
35
+
36
+ // ✅ NEW (recommended) — for logging feedback correctly
37
+ user?: ApiUser | null;
38
+
39
+ // context (optional but useful for metadata)
40
+ learningMode?: ApiLearningMode;
41
+ docType?: ApiFileType | string;
42
+
43
+ // optional: supply refs (if your message.references is not the same as backend refs)
44
+ refs?: string[];
45
+
46
+ /**
47
+ * Optional: provide the user question that led to this assistant message.
48
+ * Best practice: parent passes a function that finds previous user message.
49
+ */
50
+ getContextUserText?: () => string;
51
  }
52
 
53
  // 反馈标签选项
54
+ const FEEDBACK_TAGS: Record<FeedbackRating, string[]> = {
55
+ not_helpful: [
56
  'Code was incorrect',
57
  "Shouldn't have used Memory",
58
  "Don't like the personality",
59
  "Don't like the style",
60
  'Not factually correct',
61
  ],
62
+ helpful: [
63
  'Accurate and helpful',
64
  'Clear explanation',
65
  'Good examples',
 
68
  ],
69
  };
70
 
71
+ export function Message({
72
+ message,
73
+ showSenderInfo = false,
74
+
75
+ // NEW
76
+ user = null,
77
+ learningMode,
78
+ docType,
79
+ refs,
80
+ getContextUserText,
81
+ }: MessageProps) {
82
+ const [feedback, setFeedback] = useState<FeedbackRating | null>(null);
83
  const [copied, setCopied] = useState(false);
84
  const [referencesOpen, setReferencesOpen] = useState(false);
85
  const [showFeedbackArea, setShowFeedbackArea] = useState(false);
86
+
87
+ // ✅ unify to backend enum
88
+ const [feedbackType, setFeedbackType] = useState<FeedbackRating | null>(null);
89
+
90
  const [feedbackText, setFeedbackText] = useState('');
91
  const [selectedTags, setSelectedTags] = useState<string[]>([]);
92
+ const [submitting, setSubmitting] = useState(false);
93
 
94
  const isUser = message.role === 'user';
95
 
 
100
  setTimeout(() => setCopied(false), 2000);
101
  };
102
 
103
+ const handleFeedbackClick = (type: FeedbackRating) => {
104
  if (feedback === type) {
105
+ // clicked same state -> collapse + reset detail
106
  setFeedback(null);
107
  setShowFeedbackArea(false);
108
  setFeedbackType(null);
109
  setFeedbackText('');
110
  setSelectedTags([]);
111
+ return;
 
 
 
 
112
  }
113
+
114
+ // open feedback area
115
+ setFeedback(type);
116
+ setFeedbackType(type);
117
+ setShowFeedbackArea(true);
118
  };
119
 
120
  const handleFeedbackClose = () => {
 
122
  setFeedbackType(null);
123
  setFeedbackText('');
124
  setSelectedTags([]);
125
+ // do NOT reset feedback (keeps thumbs state)
126
  };
127
 
128
  const handleTagToggle = (tag: string) => {
129
+ setSelectedTags((prev) =>
130
+ prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag],
 
 
131
  );
132
  };
133
 
134
+ const handleFeedbackSubmit = async () => {
135
+ if (!feedbackType) return;
136
+
137
+ // If not logged in / no user, we cannot attribute feedback to a student_id
138
+ if (!user?.email) {
139
+ toast.error('Please login to submit feedback.');
140
+ return;
141
+ }
142
+
143
+ const assistantText = (message.content || '').trim();
144
+ const userText = (getContextUserText ? getContextUserText() : '').trim();
145
+
146
+ // refs: prefer explicit refs prop; fallback to message.references
147
+ const refsToSend =
148
+ refs && refs.length > 0 ? refs : (message.references ?? []);
149
+
150
+ setSubmitting(true);
151
+ try {
152
+ await apiFeedback({
153
+ user,
154
+ rating: feedbackType,
155
+ assistantMessageId: message.id, // recommended
156
+ assistantText,
157
+ userText,
158
+ tags: selectedTags,
159
+ comment: feedbackText,
160
+ refs: refsToSend,
161
+ learningMode,
162
+ docType,
163
+ timestampMs: Date.now(),
164
+ });
165
+
166
+ toast.success('Thanks — feedback recorded.');
167
+ handleFeedbackClose();
168
+ // keep thumbs state
169
+ } catch (e: any) {
170
+ console.error('[feedback] submit failed:', e);
171
+ toast.error(e?.message ? `Feedback failed: ${e.message}` : 'Feedback failed.');
172
+ } finally {
173
+ setSubmitting(false);
174
+ }
175
  };
176
 
177
  return (
178
  <div className={`flex gap-3 ${isUser && !showSenderInfo ? 'justify-end' : 'justify-start'}`}>
179
  {/* Avatar */}
180
  {showSenderInfo && message.sender ? (
181
+ <div
182
+ className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
183
+ message.sender.isAI ? 'overflow-hidden bg-white' : 'bg-muted'
184
+ }`}
185
+ >
186
  {message.sender.isAI ? (
187
  <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
188
  ) : (
189
  <span className="text-sm">
190
+ {message.sender.name
191
+ .split(' ')
192
+ .map((n) => n[0])
193
+ .join('')
194
+ .toUpperCase()}
195
  </span>
196
  )}
197
  </div>
 
207
  <div className="flex items-center gap-2 px-1">
208
  <span className="text-xs">{message.sender.name}</span>
209
  {message.sender.isAI && (
210
+ <Badge variant="secondary" className="text-xs h-4 px-1">
211
+ AI
212
+ </Badge>
213
  )}
214
  </div>
215
  )}
 
217
  <div
218
  className={`
219
  rounded-2xl px-4 py-3
220
+ ${isUser && !showSenderInfo ? 'bg-primary text-primary-foreground' : 'bg-muted'}
 
 
 
221
  `}
222
  >
223
+ {/* ✅ KEY FIX: assistant uses Markdown renderer */}
224
+ {isUser ? (
225
+ <p className="whitespace-pre-wrap">{message.content}</p>
226
+ ) : (
227
+ <div className="prose prose-sm max-w-none dark:prose-invert">
228
+ <ReactMarkdown
229
+ remarkPlugins={[remarkGfm]}
230
+ components={{
231
+ // keep line breaks looking natural in chat bubbles
232
+ p: ({ children }) => <p className="whitespace-pre-wrap">{children}</p>,
233
+ // code blocks + inline code styling
234
+ code: ({ className, children }) => (
235
+ <code className={className ? className : 'px-1 py-0.5 rounded bg-black/5 dark:bg-white/10'}>
236
+ {children}
237
+ </code>
238
+ ),
239
+ }}
240
+ >
241
+ {message.content}
242
+ </ReactMarkdown>
243
+ </div>
244
+ )}
245
  </div>
246
 
247
  {/* References */}
 
249
  <Collapsible open={referencesOpen} onOpenChange={setReferencesOpen}>
250
  <CollapsibleTrigger asChild>
251
  <Button variant="ghost" size="sm" className="gap-1 h-7 text-xs">
252
+ {referencesOpen ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
 
 
 
 
253
  {message.references.length} {message.references.length === 1 ? 'reference' : 'references'}
254
  </Button>
255
  </CollapsibleTrigger>
 
265
 
266
  {/* Message Actions */}
267
  <div className="flex items-center gap-1">
268
+ <Button variant="ghost" size="sm" className="h-7 gap-1" onClick={handleCopy}>
 
 
 
 
 
269
  {copied ? (
270
  <>
271
  <Check className="h-3 w-3" />
 
284
  <Button
285
  variant="ghost"
286
  size="sm"
287
+ className={`h-7 gap-1 ${
288
+ feedback === 'helpful' ? 'bg-green-100 text-green-600 dark:bg-green-900/20' : ''
289
+ }`}
290
  onClick={() => handleFeedbackClick('helpful')}
291
  >
292
  <ThumbsUp className="h-3 w-3" />
293
  <span className="text-xs">Helpful</span>
294
  </Button>
295
+
296
  <Button
297
  variant="ghost"
298
  size="sm"
299
+ className={`h-7 gap-1 ${
300
+ feedback === 'not_helpful' ? 'bg-red-100 text-red-600 dark:bg-red-900/20' : ''
301
+ }`}
302
+ onClick={() => handleFeedbackClick('not_helpful')}
303
  >
304
  <ThumbsDown className="h-3 w-3" />
305
  <span className="text-xs">Not helpful</span>
 
308
  )}
309
  </div>
310
 
311
+ {/* Feedback Area */}
312
  {!isUser && showFeedbackArea && feedbackType && (
313
  <div className="w-full mt-2 bg-gray-50 dark:bg-gray-800/50 rounded-lg p-4 border border-gray-200 dark:border-gray-700">
314
  <div className="flex items-start justify-between mb-4">
315
+ <h4 className="text-sm font-medium text-gray-900 dark:text-gray-100">Tell us more:</h4>
316
+ <Button variant="ghost" size="sm" className="h-6 w-6 p-0" onClick={handleFeedbackClose}>
 
 
 
 
 
 
 
317
  <X className="h-4 w-4" />
318
  </Button>
319
  </div>
320
 
321
+ {/* Tags */}
322
  <div className="flex flex-wrap gap-2 mb-4">
323
  {FEEDBACK_TAGS[feedbackType].map((tag) => (
324
  <Button
 
333
  ))}
334
  </div>
335
 
336
+ {/* Comment box */}
337
  <Textarea
338
  className="min-h-[60px] mb-4 bg-white dark:bg-gray-900"
339
  value={feedbackText}
 
341
  placeholder="Additional feedback (optional)..."
342
  />
343
 
344
+ {/* Submit */}
345
  <div className="flex justify-end gap-2">
346
+ <Button variant="outline" size="sm" onClick={handleFeedbackClose} disabled={submitting}>
 
 
 
 
347
  Cancel
348
  </Button>
349
  <Button
350
  size="sm"
351
  onClick={handleFeedbackSubmit}
352
+ disabled={submitting || (selectedTags.length === 0 && !feedbackText.trim())}
353
  >
354
+ {submitting ? 'Submitting…' : 'Submit'}
355
  </Button>
356
  </div>
357
  </div>
 
365
  )}
366
  </div>
367
  );
368
+ }