SarahXia0405 commited on
Commit
b7d9f19
·
verified ·
1 Parent(s): f9b75bc

Update web/src/components/Message.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/Message.tsx +81 -158
web/src/components/Message.tsx CHANGED
@@ -7,7 +7,8 @@ import {
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,50 +17,22 @@ import type { Message as MessageType } from '../App';
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,28 +41,14 @@ const FEEDBACK_TAGS: Record<FeedbackRating, string[]> = {
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,21 +59,20 @@ export function Message({
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,76 +80,46 @@ export function Message({
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,9 +135,7 @@ export function Message({
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,31 +143,13 @@ export function Message({
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,7 +157,11 @@ export function Message({
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,7 +177,12 @@ export function Message({
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,22 +201,17 @@ export function Message({
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,17 +220,24 @@ export function Message({
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,7 +252,7 @@ export function Message({
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,17 +260,21 @@ export function Message({
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,4 +288,4 @@ export function Message({
365
  )}
366
  </div>
367
  );
368
- }
 
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
  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
  ],
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
  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
  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
  <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
  <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
  <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
 
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
  <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
  )}
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
  ))}
253
  </div>
254
 
255
+ {/* 文本输入框 */}
256
  <Textarea
257
  className="min-h-[60px] mb-4 bg-white dark:bg-gray-900"
258
  value={feedbackText}
 
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
  )}
289
  </div>
290
  );
291
+ }