SarahXia0405 commited on
Commit
e842252
·
verified ·
1 Parent(s): e11fd85

Update web/src/components/Message.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/Message.tsx +107 -25
web/src/components/Message.tsx CHANGED
@@ -7,7 +7,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";
9
 
10
-
11
  import {
12
  Copy,
13
  ThumbsUp,
@@ -16,6 +15,7 @@ import {
16
  ChevronUp,
17
  Check,
18
  X,
 
19
  } from "lucide-react";
20
  import { Badge } from "./ui/badge";
21
  import {
@@ -73,6 +73,58 @@ function normalizeMarkdownLists(input: string) {
73
  .replace(/(^|\n)(\s*)([-*+])\s*\n+\s+/g, "$1$2$3 ");
74
  }
75
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  export function Message({
77
  message,
78
  showSenderInfo = false,
@@ -91,7 +143,7 @@ export function Message({
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);
@@ -141,6 +193,14 @@ export function Message({
141
  );
142
  };
143
 
 
 
 
 
 
 
 
 
144
  // ✅ REAL submit to backend -> LangSmith dataset
145
  const handleFeedbackSubmit = async () => {
146
  if (!currentUserId || !currentUserId.trim()) {
@@ -164,7 +224,7 @@ export function Message({
164
  user_text: "", // optional
165
  comment: feedbackText || "",
166
  tags: selectedTags,
167
- refs: message.references ?? [],
168
  learning_mode: chatMode === "ask" ? learningMode : chatMode,
169
  doc_type: docType,
170
  timestamp_ms: Date.now(),
@@ -243,14 +303,11 @@ export function Message({
243
  );
244
  };
245
 
246
- const hasRefs = !!(message.references && message.references.length > 0);
247
-
248
  const attachments = (message as any).attachments as
249
  | Array<{ name: string; kind: string; size: number; fileType?: string }>
250
  | undefined;
251
-
252
- const hasAttachments = !!(attachments && attachments.length);
253
 
 
254
 
255
  return (
256
  <div
@@ -282,7 +339,11 @@ export function Message({
282
  </div>
283
  ) : !isUser ? (
284
  <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
285
- <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
 
 
 
 
286
  </div>
287
  ) : null}
288
 
@@ -304,12 +365,13 @@ export function Message({
304
  </div>
305
  )}
306
 
307
- {/* Bubble (make it relative so we can anchor the ref box to bottom-left) */}
308
  <div
309
  className={`
310
  relative
311
  rounded-2xl px-4 py-3
312
  ${isUser && !showSenderInfo ? "bg-primary text-primary-foreground" : "bg-muted"}
 
313
  `}
314
  >
315
  {/* ✅ Attachments shown “with” the message (neutral card style) */}
@@ -318,7 +380,7 @@ export function Message({
318
  {attachments!.map((a, idx) => {
319
  const icon =
320
  a.kind === "pdf" ? pdfIcon : a.kind === "ppt" ? pptIcon : otherIcon;
321
-
322
  const label =
323
  a.kind === "pdf"
324
  ? "PDF"
@@ -329,7 +391,7 @@ export function Message({
329
  : a.kind === "image"
330
  ? "Image"
331
  : "File";
332
-
333
  return (
334
  <div
335
  key={`${a.name}-${idx}`}
@@ -363,12 +425,12 @@ export function Message({
363
  })}
364
  </div>
365
  )}
366
-
367
  {renderBubbleContent()}
368
 
369
- {/* ✅ Restore "left-bottom small reference box" */}
370
  {!isUser && hasRefs && (
371
- <div className="mt-3">
372
  <Collapsible open={referencesOpen} onOpenChange={setReferencesOpen}>
373
  <CollapsibleTrigger asChild>
374
  <button
@@ -376,7 +438,7 @@ export function Message({
376
  className="
377
  inline-flex items-center gap-1
378
  rounded-md border border-border
379
- bg-background/80 dark:bg-background/30
380
  px-2 py-1
381
  text-xs text-foreground
382
  shadow-sm
@@ -385,25 +447,45 @@ export function Message({
385
  "
386
  title="References"
387
  >
 
 
 
388
  {referencesOpen ? (
389
  <ChevronUp className="h-3 w-3" />
390
  ) : (
391
  <ChevronDown className="h-3 w-3" />
392
  )}
393
- <span className="font-medium">References</span>
394
- <span className="opacity-70">({message.references!.length})</span>
395
  </button>
396
  </CollapsibleTrigger>
397
 
398
- <CollapsibleContent className="mt-2 space-y-1">
399
- {message.references!.map((ref, index) => (
400
- <div
401
- key={index}
402
- className="rounded-md border border-border bg-background/60 dark:bg-background/20 px-2 py-1 text-xs"
403
- >
404
- {ref}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
405
  </div>
406
- ))}
407
  </CollapsibleContent>
408
  </Collapsible>
409
  </div>
 
7
  import pptIcon from "../assets/file-icons/ppt.png";
8
  import otherIcon from "../assets/file-icons/other_format.png";
9
 
 
10
  import {
11
  Copy,
12
  ThumbsUp,
 
15
  ChevronUp,
16
  Check,
17
  X,
18
+ FileText,
19
  } from "lucide-react";
20
  import { Badge } from "./ui/badge";
21
  import {
 
73
  .replace(/(^|\n)(\s*)([-*+])\s*\n+\s+/g, "$1$2$3 ");
74
  }
75
 
76
+ // ---- Refs normalization (supports old string[] + new {source_file, section}[]) ----
77
+ type RefObj = { source_file?: string; section?: string };
78
+
79
+ function normalizeRefs(raw: any): RefObj[] {
80
+ const arr: any[] = Array.isArray(raw) ? raw : [];
81
+ return arr
82
+ .map((x) => {
83
+ // object ref
84
+ if (x && typeof x === "object" && !Array.isArray(x)) {
85
+ const sf =
86
+ x.source_file != null
87
+ ? String(x.source_file).trim()
88
+ : x.file != null
89
+ ? String(x.file).trim()
90
+ : "";
91
+ const sec =
92
+ x.section != null
93
+ ? String(x.section).trim()
94
+ : x.loc != null
95
+ ? String(x.loc).trim()
96
+ : "";
97
+ if (!sf && !sec) return null;
98
+ return { source_file: sf || undefined, section: sec || undefined };
99
+ }
100
+
101
+ // string ref, try parse "file — section"
102
+ if (typeof x === "string") {
103
+ const s = x.trim();
104
+ if (!s) return null;
105
+ const parts = s.split("—").map((p) => p.trim()).filter(Boolean);
106
+ if (parts.length >= 2) {
107
+ return {
108
+ source_file: parts[0] || undefined,
109
+ section: parts.slice(1).join(" — ") || undefined,
110
+ };
111
+ }
112
+ return { source_file: s || undefined, section: undefined };
113
+ }
114
+
115
+ return null;
116
+ })
117
+ .filter(Boolean) as RefObj[];
118
+ }
119
+
120
+ function stringifyRefsForFeedback(refs: RefObj[]): string[] {
121
+ return (refs || []).map((r) => {
122
+ const a = (r.source_file || "").trim();
123
+ const b = (r.section || "").trim();
124
+ return b ? `${a} — ${b}` : a;
125
+ });
126
+ }
127
+
128
  export function Message({
129
  message,
130
  showSenderInfo = false,
 
143
  );
144
  const [copied, setCopied] = useState(false);
145
 
146
+ // ✅ References UI state (badge toggle)
147
  const [referencesOpen, setReferencesOpen] = useState(false);
148
 
149
  const [showFeedbackArea, setShowFeedbackArea] = useState(false);
 
193
  );
194
  };
195
 
196
+ // ✅ Normalize refs from either `message.refs` (new) or `message.references` (old)
197
+ const refs: RefObj[] = useMemo(() => {
198
+ const raw = (message as any).refs ?? (message as any).references ?? [];
199
+ return normalizeRefs(raw);
200
+ }, [message]);
201
+
202
+ const hasRefs = refs.length > 0;
203
+
204
  // ✅ REAL submit to backend -> LangSmith dataset
205
  const handleFeedbackSubmit = async () => {
206
  if (!currentUserId || !currentUserId.trim()) {
 
224
  user_text: "", // optional
225
  comment: feedbackText || "",
226
  tags: selectedTags,
227
+ refs: stringifyRefsForFeedback(refs),
228
  learning_mode: chatMode === "ask" ? learningMode : chatMode,
229
  doc_type: docType,
230
  timestamp_ms: Date.now(),
 
303
  );
304
  };
305
 
 
 
306
  const attachments = (message as any).attachments as
307
  | Array<{ name: string; kind: string; size: number; fileType?: string }>
308
  | undefined;
 
 
309
 
310
+ const hasAttachments = !!(attachments && attachments.length);
311
 
312
  return (
313
  <div
 
339
  </div>
340
  ) : !isUser ? (
341
  <div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
342
+ <img
343
+ src={clareAvatar}
344
+ alt="Clare"
345
+ className="w-full h-full object-cover"
346
+ />
347
  </div>
348
  ) : null}
349
 
 
365
  </div>
366
  )}
367
 
368
+ {/* Bubble (relative so we can anchor the ref badge to bottom-left) */}
369
  <div
370
  className={`
371
  relative
372
  rounded-2xl px-4 py-3
373
  ${isUser && !showSenderInfo ? "bg-primary text-primary-foreground" : "bg-muted"}
374
+ ${!isUser && hasRefs ? "pb-10" : ""}
375
  `}
376
  >
377
  {/* ✅ Attachments shown “with” the message (neutral card style) */}
 
380
  {attachments!.map((a, idx) => {
381
  const icon =
382
  a.kind === "pdf" ? pdfIcon : a.kind === "ppt" ? pptIcon : otherIcon;
383
+
384
  const label =
385
  a.kind === "pdf"
386
  ? "PDF"
 
391
  : a.kind === "image"
392
  ? "Image"
393
  : "File";
394
+
395
  return (
396
  <div
397
  key={`${a.name}-${idx}`}
 
425
  })}
426
  </div>
427
  )}
428
+
429
  {renderBubbleContent()}
430
 
431
+ {/* ✅ Left-bottom small reference badge (overlay) */}
432
  {!isUser && hasRefs && (
433
+ <div className="absolute left-3 bottom-2">
434
  <Collapsible open={referencesOpen} onOpenChange={setReferencesOpen}>
435
  <CollapsibleTrigger asChild>
436
  <button
 
438
  className="
439
  inline-flex items-center gap-1
440
  rounded-md border border-border
441
+ bg-background/85 dark:bg-background/30
442
  px-2 py-1
443
  text-xs text-foreground
444
  shadow-sm
 
447
  "
448
  title="References"
449
  >
450
+ <FileText className="h-3.5 w-3.5" />
451
+ <span className="font-medium">Refs</span>
452
+ <span className="opacity-70">({refs.length})</span>
453
  {referencesOpen ? (
454
  <ChevronUp className="h-3 w-3" />
455
  ) : (
456
  <ChevronDown className="h-3 w-3" />
457
  )}
 
 
458
  </button>
459
  </CollapsibleTrigger>
460
 
461
+ <CollapsibleContent className="mt-2">
462
+ <div
463
+ className="
464
+ w-[320px] max-w-[70vw]
465
+ rounded-md border border-border
466
+ bg-background/95 dark:bg-background/40
467
+ p-2
468
+ shadow-md
469
+ "
470
+ >
471
+ <div className="max-h-40 overflow-auto space-y-2 pr-1">
472
+ {refs.map((ref, index) => (
473
+ <div
474
+ key={index}
475
+ className="rounded-md border border-border bg-background/60 dark:bg-background/20 px-2 py-1 text-xs"
476
+ >
477
+ <div className="font-medium break-words">
478
+ {ref.source_file || "Unknown file"}
479
+ </div>
480
+ {ref.section ? (
481
+ <div className="text-muted-foreground break-words mt-0.5">
482
+ {ref.section}
483
+ </div>
484
+ ) : null}
485
+ </div>
486
+ ))}
487
  </div>
488
+ </div>
489
  </CollapsibleContent>
490
  </Collapsible>
491
  </div>