SarahXia0405 commited on
Commit
45f1637
·
verified ·
1 Parent(s): 3648357

Update web/src/components/Message.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/Message.tsx +143 -92
web/src/components/Message.tsx CHANGED
@@ -3,6 +3,7 @@ import React, { useMemo, useState } from "react";
3
  import { Button } from "./ui/button";
4
  import ReactMarkdown from "react-markdown";
5
  import remarkGfm from "remark-gfm";
 
6
  import pdfIcon from "../assets/file-icons/pdf.png";
7
  import pptIcon from "../assets/file-icons/ppt.png";
8
  import otherIcon from "../assets/file-icons/other_format.png";
@@ -33,17 +34,19 @@ import { apiFeedback } from "../lib/api";
33
  interface MessageProps {
34
  key?: React.Key;
35
  message: MessageType;
36
- showSenderInfo?: boolean;
37
- isFirstGreeting?: boolean;
38
- showNextButton?: boolean;
39
- onNextQuestion?: () => void;
40
- chatMode?: "ask" | "review" | "quiz";
41
 
 
42
  currentUserId?: string;
43
  docType?: string;
44
  learningMode?: string;
45
  }
46
 
 
47
  const FEEDBACK_TAGS = {
48
  "not-helpful": [
49
  "Code was incorrect",
@@ -61,6 +64,7 @@ const FEEDBACK_TAGS = {
61
  ],
62
  };
63
 
 
64
  function normalizeMarkdownLists(input: string) {
65
  if (!input) return input;
66
 
@@ -77,18 +81,23 @@ export function Message({
77
  onNextQuestion,
78
  chatMode = "ask",
79
 
 
80
  currentUserId,
81
  docType = "Syllabus",
82
  learningMode = "general",
83
  }: MessageProps) {
84
- const [feedback, setFeedback] = useState<"helpful" | "not-helpful" | null>(null);
 
 
85
  const [copied, setCopied] = useState(false);
86
 
87
- // ✅ References UI state (always available for assistant)
88
  const [referencesOpen, setReferencesOpen] = useState(false);
89
 
90
  const [showFeedbackArea, setShowFeedbackArea] = useState(false);
91
- const [feedbackType, setFeedbackType] = useState<"helpful" | "not-helpful" | null>(null);
 
 
92
  const [feedbackText, setFeedbackText] = useState("");
93
  const [selectedTags, setSelectedTags] = useState<string[]>([]);
94
  const [nextButtonClicked, setNextButtonClicked] = useState(false);
@@ -98,6 +107,11 @@ export function Message({
98
  isFirstGreeting || message.id === "review-1" || message.id === "quiz-1";
99
  const shouldShowActions = isUser ? true : !isWelcomeMessage;
100
 
 
 
 
 
 
101
  const handleCopy = async () => {
102
  await navigator.clipboard.writeText(message.content);
103
  setCopied(true);
@@ -132,6 +146,7 @@ export function Message({
132
  );
133
  };
134
 
 
135
  const handleFeedbackSubmit = async () => {
136
  if (!currentUserId || !currentUserId.trim()) {
137
  toast.error("Missing user_id; cannot submit feedback.");
@@ -142,6 +157,7 @@ export function Message({
142
  return;
143
  }
144
 
 
145
  const rating = feedbackType === "helpful" ? "helpful" : "not_helpful";
146
 
147
  try {
@@ -150,7 +166,7 @@ export function Message({
150
  rating,
151
  assistant_message_id: message.id,
152
  assistant_text: message.content,
153
- user_text: "",
154
  comment: feedbackText || "",
155
  tags: selectedTags,
156
  refs: message.references ?? [],
@@ -186,7 +202,11 @@ export function Message({
186
 
187
  const renderBubbleContent = () => {
188
  if (isUser) {
189
- return <p className="whitespace-pre-wrap text-base break-words">{message.content}</p>;
 
 
 
 
190
  }
191
 
192
  return (
@@ -194,8 +214,12 @@ export function Message({
194
  remarkPlugins={[remarkGfm]}
195
  className={markdownClass}
196
  components={{
197
- p: ({ children }) => <p className="my-2 whitespace-pre-wrap break-words">{children}</p>,
198
- ul: ({ children }) => <ul className="my-3 pl-6 space-y-2">{children}</ul>,
 
 
 
 
199
  ol: ({ children }) => <ol className="my-3 space-y-4">{children}</ol>,
200
  li: ({ children, node }) => {
201
  const parent = (node as any)?.parent?.tagName;
@@ -213,7 +237,9 @@ export function Message({
213
  }
214
  return <li>{children}</li>;
215
  },
216
- strong: ({ children }) => <strong className="font-semibold">{children}</strong>,
 
 
217
  em: ({ children }) => <em className="italic">{children}</em>,
218
  }}
219
  >
@@ -222,17 +248,12 @@ export function Message({
222
  );
223
  };
224
 
225
- const refsArr = (message.references || []).filter(Boolean);
226
- const refsCount = refsArr.length;
227
-
228
  const attachments = (message as any).attachments as
229
  | Array<{ name: string; kind: string; size: number; fileType?: string }>
230
  | undefined;
231
 
232
  const hasAttachments = !!(attachments && attachments.length);
233
 
234
- const showReferenceWidget = !isUser; // ✅ assistant always shows the ref widget
235
-
236
  return (
237
  <div
238
  className={`flex gap-2 ${
@@ -243,7 +264,11 @@ export function Message({
243
  {showSenderInfo && message.sender ? (
244
  <div className="w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 overflow-hidden bg-white">
245
  {message.sender.isAI ? (
246
- <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
 
 
 
 
247
  ) : (
248
  <img
249
  src={
@@ -259,7 +284,11 @@ export function Message({
259
  </div>
260
  ) : !isUser ? (
261
  <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
262
- <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
 
 
 
 
263
  </div>
264
  ) : null}
265
 
@@ -284,17 +313,20 @@ export function Message({
284
  {/* Bubble */}
285
  <div
286
  className={`
287
- relative
288
  rounded-2xl px-4 py-3
289
- ${!isUser ? "pb-10" : ""}
290
  ${isUser && !showSenderInfo ? "bg-primary text-primary-foreground" : "bg-muted"}
291
  `}
292
  >
293
- {/* Attachments */}
294
  {hasAttachments && (
295
  <div className="mb-3 flex flex-wrap gap-2">
296
  {attachments!.map((a, idx) => {
297
- const icon = a.kind === "pdf" ? pdfIcon : a.kind === "ppt" ? pptIcon : otherIcon;
 
 
 
 
 
298
 
299
  const label =
300
  a.kind === "pdf"
@@ -328,8 +360,12 @@ export function Message({
328
  draggable={false}
329
  />
330
  <div className="min-w-0 leading-tight">
331
- <div className="text-sm font-medium truncate max-w-[320px]">{a.name}</div>
332
- <div className="text-[11px] text-muted-foreground mt-0.5">{label}</div>
 
 
 
 
333
  </div>
334
  </div>
335
  );
@@ -338,74 +374,76 @@ export function Message({
338
  )}
339
 
340
  {renderBubbleContent()}
 
341
 
342
- {/* ✅ Bottom-left reference widget (always for assistant) */}
343
- {showReferenceWidget && (
344
- <div className="absolute left-3 bottom-2">
345
- <Collapsible open={referencesOpen} onOpenChange={setReferencesOpen}>
346
- <CollapsibleTrigger asChild>
347
- <button
348
- type="button"
349
- className="
350
- inline-flex items-center gap-1
351
- rounded-md border border-border
352
- bg-background/80 dark:bg-background/30
353
- px-2 py-1
354
- text-xs text-foreground
355
- shadow-sm
356
- hover:bg-background
357
- transition
358
- "
359
- title="References"
360
- >
361
- {referencesOpen ? (
362
- <ChevronUp className="h-3 w-3" />
363
- ) : (
364
- <ChevronDown className="h-3 w-3" />
365
- )}
366
- <span className="font-medium">References</span>
367
- <span className="opacity-70">({refsCount})</span>
368
- </button>
369
- </CollapsibleTrigger>
370
-
371
- <CollapsibleContent className="mt-2">
372
- {refsCount > 0 ? (
373
- <div className="space-y-1">
374
- {refsArr.map((ref, index) => (
375
- <div
376
- key={index}
377
- className="rounded-md border border-border bg-background/60 dark:bg-background/20 px-2 py-1 text-xs"
378
- >
379
- {ref}
380
- </div>
381
- ))}
382
- </div>
383
  ) : (
384
- <div className="rounded-md border border-border bg-background/60 dark:bg-background/20 px-2 py-1 text-xs opacity-80">
385
- No references returned.
386
- </div>
387
  )}
388
- </CollapsibleContent>
389
- </Collapsible>
390
- </div>
391
- )}
392
- </div>
393
-
394
- {/* Next Question Button for Quiz Mode */}
395
- {!isUser && showNextButton && !nextButtonClicked && chatMode === "quiz" && onNextQuestion && (
396
- <div className="mt-2">
397
- <Button
398
- onClick={() => {
399
- setNextButtonClicked(true);
400
- onNextQuestion();
401
- }}
402
- className="bg-primary hover:bg-primary/90"
403
- >
404
- Next Question
405
- </Button>
 
 
 
 
406
  </div>
407
  )}
408
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
  {/* Message Actions */}
410
  {shouldShowActions && (
411
  <div className="flex items-center gap-1">
@@ -413,12 +451,18 @@ export function Message({
413
  variant="ghost"
414
  size="icon"
415
  className={`h-7 w-7 rounded-md transition-all hover:bg-accent hover:scale-110 active:scale-95 ${
416
- copied ? "bg-green-100 text-green-600 dark:bg-green-900/20 dark:text-green-400" : ""
 
 
417
  }`}
418
  onClick={handleCopy}
419
  title="Copy"
420
  >
421
- {copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
 
 
 
 
422
  </Button>
423
 
424
  {!isUser && (
@@ -458,8 +502,15 @@ export function Message({
458
  {!isUser && showFeedbackArea && feedbackType && (
459
  <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">
460
  <div className="flex items-start justify-between mb-4">
461
- <h4 className="text-sm font-medium text-gray-900 dark:text-gray-100">Tell us more:</h4>
462
- <Button variant="ghost" size="sm" className="h-6 w-6 p-0" onClick={handleFeedbackClose}>
 
 
 
 
 
 
 
463
  <X className="h-4 w-4" />
464
  </Button>
465
  </div>
 
3
  import { Button } from "./ui/button";
4
  import ReactMarkdown from "react-markdown";
5
  import remarkGfm from "remark-gfm";
6
+
7
  import pdfIcon from "../assets/file-icons/pdf.png";
8
  import pptIcon from "../assets/file-icons/ppt.png";
9
  import otherIcon from "../assets/file-icons/other_format.png";
 
34
  interface MessageProps {
35
  key?: React.Key;
36
  message: MessageType;
37
+ showSenderInfo?: boolean; // For group chat mode
38
+ isFirstGreeting?: boolean; // Indicates if this is the first greeting message
39
+ showNextButton?: boolean; // For quiz mode
40
+ onNextQuestion?: () => void; // For quiz mode
41
+ chatMode?: "ask" | "review" | "quiz"; // Current chat mode
42
 
43
+ // ✅ NEW: for feedback submission
44
  currentUserId?: string;
45
  docType?: string;
46
  learningMode?: string;
47
  }
48
 
49
+ // 反馈标签选项
50
  const FEEDBACK_TAGS = {
51
  "not-helpful": [
52
  "Code was incorrect",
 
64
  ],
65
  };
66
 
67
+ // ---- Markdown list normalization ----
68
  function normalizeMarkdownLists(input: string) {
69
  if (!input) return input;
70
 
 
81
  onNextQuestion,
82
  chatMode = "ask",
83
 
84
+ // ✅ NEW
85
  currentUserId,
86
  docType = "Syllabus",
87
  learningMode = "general",
88
  }: MessageProps) {
89
+ const [feedback, setFeedback] = useState<"helpful" | "not-helpful" | null>(
90
+ null
91
+ );
92
  const [copied, setCopied] = useState(false);
93
 
94
+ // ✅ References UI state
95
  const [referencesOpen, setReferencesOpen] = useState(false);
96
 
97
  const [showFeedbackArea, setShowFeedbackArea] = useState(false);
98
+ const [feedbackType, setFeedbackType] = useState<
99
+ "helpful" | "not-helpful" | null
100
+ >(null);
101
  const [feedbackText, setFeedbackText] = useState("");
102
  const [selectedTags, setSelectedTags] = useState<string[]>([]);
103
  const [nextButtonClicked, setNextButtonClicked] = useState(false);
 
107
  isFirstGreeting || message.id === "review-1" || message.id === "quiz-1";
108
  const shouldShowActions = isUser ? true : !isWelcomeMessage;
109
 
110
+ // ✅ show reference pill for assistant messages (even if 0)
111
+ const showReferencesPill = !isUser; // assistant only
112
+ const refs = Array.isArray(message.references) ? message.references : [];
113
+ const hasRefs = refs.length > 0;
114
+
115
  const handleCopy = async () => {
116
  await navigator.clipboard.writeText(message.content);
117
  setCopied(true);
 
146
  );
147
  };
148
 
149
+ // ✅ REAL submit to backend -> LangSmith dataset
150
  const handleFeedbackSubmit = async () => {
151
  if (!currentUserId || !currentUserId.trim()) {
152
  toast.error("Missing user_id; cannot submit feedback.");
 
157
  return;
158
  }
159
 
160
+ // UI uses "not-helpful" (dash), backend expects "not_helpful" (underscore)
161
  const rating = feedbackType === "helpful" ? "helpful" : "not_helpful";
162
 
163
  try {
 
166
  rating,
167
  assistant_message_id: message.id,
168
  assistant_text: message.content,
169
+ user_text: "", // optional
170
  comment: feedbackText || "",
171
  tags: selectedTags,
172
  refs: message.references ?? [],
 
202
 
203
  const renderBubbleContent = () => {
204
  if (isUser) {
205
+ return (
206
+ <p className="whitespace-pre-wrap text-base break-words">
207
+ {message.content}
208
+ </p>
209
+ );
210
  }
211
 
212
  return (
 
214
  remarkPlugins={[remarkGfm]}
215
  className={markdownClass}
216
  components={{
217
+ p: ({ children }) => (
218
+ <p className="my-2 whitespace-pre-wrap break-words">{children}</p>
219
+ ),
220
+ ul: ({ children }) => (
221
+ <ul className="my-3 pl-6 space-y-2">{children}</ul>
222
+ ),
223
  ol: ({ children }) => <ol className="my-3 space-y-4">{children}</ol>,
224
  li: ({ children, node }) => {
225
  const parent = (node as any)?.parent?.tagName;
 
237
  }
238
  return <li>{children}</li>;
239
  },
240
+ strong: ({ children }) => (
241
+ <strong className="font-semibold">{children}</strong>
242
+ ),
243
  em: ({ children }) => <em className="italic">{children}</em>,
244
  }}
245
  >
 
248
  );
249
  };
250
 
 
 
 
251
  const attachments = (message as any).attachments as
252
  | Array<{ name: string; kind: string; size: number; fileType?: string }>
253
  | undefined;
254
 
255
  const hasAttachments = !!(attachments && attachments.length);
256
 
 
 
257
  return (
258
  <div
259
  className={`flex gap-2 ${
 
264
  {showSenderInfo && message.sender ? (
265
  <div className="w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 overflow-hidden bg-white">
266
  {message.sender.isAI ? (
267
+ <img
268
+ src={clareAvatar}
269
+ alt="Clare"
270
+ className="w-full h-full object-cover"
271
+ />
272
  ) : (
273
  <img
274
  src={
 
284
  </div>
285
  ) : !isUser ? (
286
  <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
287
+ <img
288
+ src={clareAvatar}
289
+ alt="Clare"
290
+ className="w-full h-full object-cover"
291
+ />
292
  </div>
293
  ) : null}
294
 
 
313
  {/* Bubble */}
314
  <div
315
  className={`
 
316
  rounded-2xl px-4 py-3
 
317
  ${isUser && !showSenderInfo ? "bg-primary text-primary-foreground" : "bg-muted"}
318
  `}
319
  >
320
+ {/* Attachments shown “with” the message (neutral card style) */}
321
  {hasAttachments && (
322
  <div className="mb-3 flex flex-wrap gap-2">
323
  {attachments!.map((a, idx) => {
324
+ const icon =
325
+ a.kind === "pdf"
326
+ ? pdfIcon
327
+ : a.kind === "ppt"
328
+ ? pptIcon
329
+ : otherIcon;
330
 
331
  const label =
332
  a.kind === "pdf"
 
360
  draggable={false}
361
  />
362
  <div className="min-w-0 leading-tight">
363
+ <div className="text-sm font-medium truncate max-w-[320px]">
364
+ {a.name}
365
+ </div>
366
+ <div className="text-[11px] text-muted-foreground mt-0.5">
367
+ {label}
368
+ </div>
369
  </div>
370
  </div>
371
  );
 
374
  )}
375
 
376
  {renderBubbleContent()}
377
+ </div>
378
 
379
+ {/* ✅ References block now sits BETWEEN bubble and actions (NOT inside bubble) */}
380
+ {showReferencesPill && (
381
+ <div className={isUser ? "hidden" : ""}>
382
+ <Collapsible open={referencesOpen} onOpenChange={setReferencesOpen}>
383
+ <CollapsibleTrigger asChild>
384
+ <button
385
+ type="button"
386
+ className="
387
+ inline-flex items-center gap-1
388
+ rounded-md border border-border
389
+ bg-background/80 dark:bg-background/30
390
+ px-2 py-1
391
+ text-xs text-foreground
392
+ shadow-sm
393
+ hover:bg-background
394
+ transition
395
+ "
396
+ title="References"
397
+ >
398
+ {referencesOpen ? (
399
+ <ChevronUp className="h-3 w-3" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  ) : (
401
+ <ChevronDown className="h-3 w-3" />
 
 
402
  )}
403
+ <span className="font-medium">References</span>
404
+ <span className="opacity-70">({refs.length})</span>
405
+ </button>
406
+ </CollapsibleTrigger>
407
+
408
+ <CollapsibleContent className="mt-2 space-y-1">
409
+ {hasRefs ? (
410
+ refs.map((ref, index) => (
411
+ <div
412
+ key={index}
413
+ className="rounded-md border border-border bg-background/60 dark:bg-background/20 px-2 py-1 text-xs"
414
+ >
415
+ {ref}
416
+ </div>
417
+ ))
418
+ ) : (
419
+ <div className="rounded-md border border-border bg-background/60 dark:bg-background/20 px-2 py-1 text-xs opacity-80">
420
+ No references returned.
421
+ </div>
422
+ )}
423
+ </CollapsibleContent>
424
+ </Collapsible>
425
  </div>
426
  )}
427
 
428
+ {/* Next Question Button for Quiz Mode */}
429
+ {!isUser &&
430
+ showNextButton &&
431
+ !nextButtonClicked &&
432
+ chatMode === "quiz" &&
433
+ onNextQuestion && (
434
+ <div className="mt-2">
435
+ <Button
436
+ onClick={() => {
437
+ setNextButtonClicked(true);
438
+ onNextQuestion();
439
+ }}
440
+ className="bg-primary hover:bg-primary/90"
441
+ >
442
+ Next Question
443
+ </Button>
444
+ </div>
445
+ )}
446
+
447
  {/* Message Actions */}
448
  {shouldShowActions && (
449
  <div className="flex items-center gap-1">
 
451
  variant="ghost"
452
  size="icon"
453
  className={`h-7 w-7 rounded-md transition-all hover:bg-accent hover:scale-110 active:scale-95 ${
454
+ copied
455
+ ? "bg-green-100 text-green-600 dark:bg-green-900/20 dark:text-green-400"
456
+ : ""
457
  }`}
458
  onClick={handleCopy}
459
  title="Copy"
460
  >
461
+ {copied ? (
462
+ <Check className="h-4 w-4" />
463
+ ) : (
464
+ <Copy className="h-4 w-4" />
465
+ )}
466
  </Button>
467
 
468
  {!isUser && (
 
502
  {!isUser && showFeedbackArea && feedbackType && (
503
  <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">
504
  <div className="flex items-start justify-between mb-4">
505
+ <h4 className="text-sm font-medium text-gray-900 dark:text-gray-100">
506
+ Tell us more:
507
+ </h4>
508
+ <Button
509
+ variant="ghost"
510
+ size="sm"
511
+ className="h-6 w-6 p-0"
512
+ onClick={handleFeedbackClose}
513
+ >
514
  <X className="h-4 w-4" />
515
  </Button>
516
  </div>