SarahXia0405 commited on
Commit
4d3c21a
·
verified ·
1 Parent(s): 651fb6c

Update web/src/components/Message.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/Message.tsx +57 -97
web/src/components/Message.tsx CHANGED
@@ -24,7 +24,7 @@ import type { Message as MessageType } from "../App";
24
  import { toast } from "sonner";
25
  import clareAvatar from "../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png";
26
 
27
- // ✅ NEW: call backend feedback api
28
  import { apiFeedback } from "../lib/api";
29
 
30
  interface MessageProps {
@@ -36,10 +36,10 @@ interface MessageProps {
36
  onNextQuestion?: () => void; // For quiz mode
37
  chatMode?: "ask" | "review" | "quiz"; // Current chat mode
38
 
39
- // ✅ NEW: required for submitting feedback to backend
40
- currentUserId?: string; // corresponds to backend user_id
41
- learningMode?: string;
42
  docType?: string;
 
43
  }
44
 
45
  // 反馈标签选项
@@ -61,42 +61,16 @@ const FEEDBACK_TAGS = {
61
  };
62
 
63
  // ---- Markdown list normalization ----
64
- // Fixes patterns like:
65
- // 1.
66
- //
67
- // **Title**
68
- // ... -> 1. **Title**
69
  function normalizeMarkdownLists(input: string) {
70
  if (!input) return input;
71
 
72
  return (
73
  input
74
- // ordered list: "1.\n\n text" -> "1. text"
75
  .replace(/(^|\n)(\s*)(\d+\.)\s*\n+\s+/g, "$1$2$3 ")
76
- // unordered list: "-\n\n text" -> "- text"
77
  .replace(/(^|\n)(\s*)([-*+])\s*\n+\s+/g, "$1$2$3 ")
78
  );
79
  }
80
 
81
- function firstTextish(children: React.ReactNode): string {
82
- const arr = React.Children.toArray(children);
83
- for (const ch of arr) {
84
- if (typeof ch === "string") return ch;
85
- if (React.isValidElement(ch)) {
86
- const inner = (ch.props as any)?.children;
87
- const t = firstTextish(inner);
88
- if (t) return t;
89
- }
90
- }
91
- return "";
92
- }
93
-
94
- function startsWithEmojiBullet(text: string) {
95
- const t = (text || "").trim();
96
- // 你图二里的风格:✅ / 🔧 等开头就不要再加圆点
97
- return /^(✅|☑️|✔️|🟢|🔧|⭐️|✨|🔥|👉|➡️|•)/.test(t);
98
- }
99
-
100
  export function Message({
101
  message,
102
  showSenderInfo = false,
@@ -104,9 +78,11 @@ export function Message({
104
  showNextButton = false,
105
  onNextQuestion,
106
  chatMode = "ask",
 
 
107
  currentUserId,
108
- learningMode,
109
- docType,
110
  }: MessageProps) {
111
  const [feedback, setFeedback] = useState<"helpful" | "not-helpful" | null>(
112
  null
@@ -121,9 +97,6 @@ export function Message({
121
  const [selectedTags, setSelectedTags] = useState<string[]>([]);
122
  const [nextButtonClicked, setNextButtonClicked] = useState(false);
123
 
124
- // ✅ NEW: prevent double submit
125
- const [submittingFeedback, setSubmittingFeedback] = useState(false);
126
-
127
  const isUser = message.role === "user";
128
  const isWelcomeMessage =
129
  isFirstGreeting || message.id === "review-1" || message.id === "quiz-1";
@@ -163,51 +136,41 @@ export function Message({
163
  );
164
  };
165
 
166
- // ✅ UPDATED: actually send to backend
167
  const handleFeedbackSubmit = async () => {
168
- if (!feedbackType) return;
169
-
170
  if (!currentUserId || !currentUserId.trim()) {
171
  toast.error("Missing user_id; cannot submit feedback.");
172
  return;
173
  }
 
 
 
 
174
 
 
175
  const rating = feedbackType === "helpful" ? "helpful" : "not_helpful";
176
 
177
- // run_id is stored on assistant message (you need to attach it in ChatArea when receiving /api/chat)
178
- const run_id = (message as any)?.run_id ?? null;
179
-
180
- // refs: your message.references appears to be string[] currently
181
- const refs = (message.references as any) ?? [];
182
-
183
  try {
184
- setSubmittingFeedback(true);
185
-
186
  await apiFeedback({
187
  user_id: currentUserId,
188
  rating,
189
- run_id,
190
-
191
  assistant_message_id: message.id,
192
- assistant_text: message.content || "",
193
- user_text: "",
194
-
195
- comment: (feedbackText || "").trim(),
196
  tags: selectedTags,
197
- refs,
198
-
199
- learning_mode: learningMode,
200
  doc_type: docType,
201
  timestamp_ms: Date.now(),
202
  });
203
 
204
- toast.success("Feedback submitted. Thank you!");
205
  handleFeedbackClose();
206
  } catch (e: any) {
207
- toast.error(e?.message ?? "Feedback submit failed");
208
- // keep dialog open so user can retry
209
- } finally {
210
- setSubmittingFeedback(false);
211
  }
212
  };
213
 
@@ -218,14 +181,11 @@ export function Message({
218
  const markdownClass = useMemo(() => {
219
  return [
220
  "text-base leading-relaxed break-words",
221
- // 让段落/列表更紧凑,像产品文案
222
  " [&_p]:my-2",
223
  " [&_p:first-child]:mt-0",
224
  " [&_p:last-child]:mb-0",
225
- // code / pre
226
  " [&_code]:px-1 [&_code]:py-0.5 [&_code]:rounded [&_code]:bg-black/5 dark:[&_code]:bg-white/10",
227
  " [&_pre]:my-2 [&_pre]:p-3 [&_pre]:rounded-lg [&_pre]:overflow-auto [&_pre]:bg-black/5 dark:[&_pre]:bg-white/10",
228
- // links
229
  " [&_a]:underline [&_a]:underline-offset-2",
230
  ].join("");
231
  }, []);
@@ -247,39 +207,25 @@ export function Message({
247
  p: ({ children }) => (
248
  <p className="my-2 whitespace-pre-wrap break-words">{children}</p>
249
  ),
250
-
251
- /* ---------- Unordered list (· bullet) ---------- */
252
  ul: ({ children }) => <ul className="my-3 pl-6 space-y-2">{children}</ul>,
253
-
254
- /* ---------- Ordered list (1. 2. 3.) ---------- */
255
  ol: ({ children }) => <ol className="my-3 space-y-4">{children}</ol>,
256
-
257
  li: ({ children, node }) => {
258
  const parent = (node as any)?.parent?.tagName;
259
-
260
- // —— ordered list: number aligned left, text indented
261
  if (parent === "ol") {
262
  return (
263
  <li className="list-none">
264
  <div className="flex items-start">
265
- {/* number column */}
266
  <span className="w-6 text-right pr-2 flex-shrink-0 font-medium">
267
  {(node as any)?.index + 1}.
268
  </span>
269
-
270
- {/* content column */}
271
  <div className="min-w-0">{children}</div>
272
  </div>
273
  </li>
274
  );
275
  }
276
-
277
- /* fallback (unordered handled elsewhere) */
278
  return <li>{children}</li>;
279
  },
280
-
281
  strong: ({ children }) => <strong className="font-semibold">{children}</strong>,
282
-
283
  em: ({ children }) => <em className="italic">{children}</em>,
284
  }}
285
  >
@@ -347,19 +293,23 @@ export function Message({
347
  </div>
348
 
349
  {/* Next Question Button for Quiz Mode */}
350
- {!isUser && showNextButton && !nextButtonClicked && chatMode === "quiz" && onNextQuestion && (
351
- <div className="mt-2">
352
- <Button
353
- onClick={() => {
354
- setNextButtonClicked(true);
355
- onNextQuestion();
356
- }}
357
- className="bg-primary hover:bg-primary/90"
358
- >
359
- Next Question
360
- </Button>
361
- </div>
362
- )}
 
 
 
 
363
 
364
  {/* References */}
365
  {message.references && message.references.length > 0 && (
@@ -367,7 +317,8 @@ export function Message({
367
  <CollapsibleTrigger asChild>
368
  <Button variant="ghost" size="sm" className="gap-1 h-7 text-xs">
369
  {referencesOpen ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
370
- {message.references.length} {message.references.length === 1 ? "reference" : "references"}
 
371
  </Button>
372
  </CollapsibleTrigger>
373
  <CollapsibleContent className="space-y-1 mt-1">
@@ -387,7 +338,9 @@ export function Message({
387
  variant="ghost"
388
  size="icon"
389
  className={`h-7 w-7 rounded-md transition-all hover:bg-accent hover:scale-110 active:scale-95 ${
390
- copied ? "bg-green-100 text-green-600 dark:bg-green-900/20 dark:text-green-400" : ""
 
 
391
  }`}
392
  onClick={handleCopy}
393
  title="Copy"
@@ -432,8 +385,15 @@ export function Message({
432
  {!isUser && showFeedbackArea && feedbackType && (
433
  <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">
434
  <div className="flex items-start justify-between mb-4">
435
- <h4 className="text-sm font-medium text-gray-900 dark:text-gray-100">Tell us more:</h4>
436
- <Button variant="ghost" size="sm" className="h-6 w-6 p-0" onClick={handleFeedbackClose}>
 
 
 
 
 
 
 
437
  <X className="h-4 w-4" />
438
  </Button>
439
  </div>
@@ -460,15 +420,15 @@ export function Message({
460
  />
461
 
462
  <div className="flex justify-end gap-2">
463
- <Button variant="outline" size="sm" onClick={handleFeedbackClose} disabled={submittingFeedback}>
464
  Cancel
465
  </Button>
466
  <Button
467
  size="sm"
468
  onClick={handleFeedbackSubmit}
469
- disabled={submittingFeedback || (selectedTags.length === 0 && !feedbackText.trim())}
470
  >
471
- {submittingFeedback ? "Submitting..." : "Submit"}
472
  </Button>
473
  </div>
474
  </div>
 
24
  import { toast } from "sonner";
25
  import clareAvatar from "../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png";
26
 
27
+ // ✅ NEW: real API call
28
  import { apiFeedback } from "../lib/api";
29
 
30
  interface MessageProps {
 
36
  onNextQuestion?: () => void; // For quiz mode
37
  chatMode?: "ask" | "review" | "quiz"; // Current chat mode
38
 
39
+ // ✅ NEW: for feedback submission
40
+ currentUserId?: string;
 
41
  docType?: string;
42
+ learningMode?: string;
43
  }
44
 
45
  // 反馈标签选项
 
61
  };
62
 
63
  // ---- Markdown list normalization ----
 
 
 
 
 
64
  function normalizeMarkdownLists(input: string) {
65
  if (!input) return input;
66
 
67
  return (
68
  input
 
69
  .replace(/(^|\n)(\s*)(\d+\.)\s*\n+\s+/g, "$1$2$3 ")
 
70
  .replace(/(^|\n)(\s*)([-*+])\s*\n+\s+/g, "$1$2$3 ")
71
  );
72
  }
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  export function Message({
75
  message,
76
  showSenderInfo = false,
 
78
  showNextButton = false,
79
  onNextQuestion,
80
  chatMode = "ask",
81
+
82
+ // ✅ NEW
83
  currentUserId,
84
+ docType = "Syllabus",
85
+ learningMode = "general",
86
  }: MessageProps) {
87
  const [feedback, setFeedback] = useState<"helpful" | "not-helpful" | null>(
88
  null
 
97
  const [selectedTags, setSelectedTags] = useState<string[]>([]);
98
  const [nextButtonClicked, setNextButtonClicked] = useState(false);
99
 
 
 
 
100
  const isUser = message.role === "user";
101
  const isWelcomeMessage =
102
  isFirstGreeting || message.id === "review-1" || message.id === "quiz-1";
 
136
  );
137
  };
138
 
139
+ // ✅ REAL submit to backend -> LangSmith dataset
140
  const handleFeedbackSubmit = async () => {
 
 
141
  if (!currentUserId || !currentUserId.trim()) {
142
  toast.error("Missing user_id; cannot submit feedback.");
143
  return;
144
  }
145
+ if (!feedbackType) {
146
+ toast.error("Please select Helpful / Not helpful.");
147
+ return;
148
+ }
149
 
150
+ // UI uses "not-helpful" (dash), backend expects "not_helpful" (underscore)
151
  const rating = feedbackType === "helpful" ? "helpful" : "not_helpful";
152
 
 
 
 
 
 
 
153
  try {
 
 
154
  await apiFeedback({
155
  user_id: currentUserId,
156
  rating,
 
 
157
  assistant_message_id: message.id,
158
+ assistant_text: message.content,
159
+ user_text: "", // optional; you can wire last user msg later if you want
160
+ comment: feedbackText || "",
 
161
  tags: selectedTags,
162
+ refs: message.references ?? [],
163
+ learning_mode: chatMode === "ask" ? learningMode : chatMode,
 
164
  doc_type: docType,
165
  timestamp_ms: Date.now(),
166
  });
167
 
168
+ toast.success("Thanks feedback submitted.");
169
  handleFeedbackClose();
170
  } catch (e: any) {
171
+ // eslint-disable-next-line no-console
172
+ console.error("feedback submit failed:", e);
173
+ toast.error(e?.message || "Failed to submit feedback.");
 
174
  }
175
  };
176
 
 
181
  const markdownClass = useMemo(() => {
182
  return [
183
  "text-base leading-relaxed break-words",
 
184
  " [&_p]:my-2",
185
  " [&_p:first-child]:mt-0",
186
  " [&_p:last-child]:mb-0",
 
187
  " [&_code]:px-1 [&_code]:py-0.5 [&_code]:rounded [&_code]:bg-black/5 dark:[&_code]:bg-white/10",
188
  " [&_pre]:my-2 [&_pre]:p-3 [&_pre]:rounded-lg [&_pre]:overflow-auto [&_pre]:bg-black/5 dark:[&_pre]:bg-white/10",
 
189
  " [&_a]:underline [&_a]:underline-offset-2",
190
  ].join("");
191
  }, []);
 
207
  p: ({ children }) => (
208
  <p className="my-2 whitespace-pre-wrap break-words">{children}</p>
209
  ),
 
 
210
  ul: ({ children }) => <ul className="my-3 pl-6 space-y-2">{children}</ul>,
 
 
211
  ol: ({ children }) => <ol className="my-3 space-y-4">{children}</ol>,
 
212
  li: ({ children, node }) => {
213
  const parent = (node as any)?.parent?.tagName;
 
 
214
  if (parent === "ol") {
215
  return (
216
  <li className="list-none">
217
  <div className="flex items-start">
 
218
  <span className="w-6 text-right pr-2 flex-shrink-0 font-medium">
219
  {(node as any)?.index + 1}.
220
  </span>
 
 
221
  <div className="min-w-0">{children}</div>
222
  </div>
223
  </li>
224
  );
225
  }
 
 
226
  return <li>{children}</li>;
227
  },
 
228
  strong: ({ children }) => <strong className="font-semibold">{children}</strong>,
 
229
  em: ({ children }) => <em className="italic">{children}</em>,
230
  }}
231
  >
 
293
  </div>
294
 
295
  {/* Next Question Button for Quiz Mode */}
296
+ {!isUser &&
297
+ showNextButton &&
298
+ !nextButtonClicked &&
299
+ chatMode === "quiz" &&
300
+ onNextQuestion && (
301
+ <div className="mt-2">
302
+ <Button
303
+ onClick={() => {
304
+ setNextButtonClicked(true);
305
+ onNextQuestion();
306
+ }}
307
+ className="bg-primary hover:bg-primary/90"
308
+ >
309
+ Next Question
310
+ </Button>
311
+ </div>
312
+ )}
313
 
314
  {/* References */}
315
  {message.references && message.references.length > 0 && (
 
317
  <CollapsibleTrigger asChild>
318
  <Button variant="ghost" size="sm" className="gap-1 h-7 text-xs">
319
  {referencesOpen ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
320
+ {message.references.length}{" "}
321
+ {message.references.length === 1 ? "reference" : "references"}
322
  </Button>
323
  </CollapsibleTrigger>
324
  <CollapsibleContent className="space-y-1 mt-1">
 
338
  variant="ghost"
339
  size="icon"
340
  className={`h-7 w-7 rounded-md transition-all hover:bg-accent hover:scale-110 active:scale-95 ${
341
+ copied
342
+ ? "bg-green-100 text-green-600 dark:bg-green-900/20 dark:text-green-400"
343
+ : ""
344
  }`}
345
  onClick={handleCopy}
346
  title="Copy"
 
385
  {!isUser && showFeedbackArea && feedbackType && (
386
  <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">
387
  <div className="flex items-start justify-between mb-4">
388
+ <h4 className="text-sm font-medium text-gray-900 dark:text-gray-100">
389
+ Tell us more:
390
+ </h4>
391
+ <Button
392
+ variant="ghost"
393
+ size="sm"
394
+ className="h-6 w-6 p-0"
395
+ onClick={handleFeedbackClose}
396
+ >
397
  <X className="h-4 w-4" />
398
  </Button>
399
  </div>
 
420
  />
421
 
422
  <div className="flex justify-end gap-2">
423
+ <Button variant="outline" size="sm" onClick={handleFeedbackClose}>
424
  Cancel
425
  </Button>
426
  <Button
427
  size="sm"
428
  onClick={handleFeedbackSubmit}
429
+ disabled={selectedTags.length === 0 && !feedbackText.trim()}
430
  >
431
+ Submit
432
  </Button>
433
  </div>
434
  </div>