SarahXia0405 commited on
Commit
2c01580
·
verified ·
1 Parent(s): 3f142c3

Update web/src/components/Message.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/Message.tsx +234 -62
web/src/components/Message.tsx CHANGED
@@ -4,9 +4,21 @@ import { Button } from "./ui/button";
4
  import ReactMarkdown from "react-markdown";
5
  import remarkGfm from "remark-gfm";
6
 
7
- import { Copy, ThumbsUp, ThumbsDown, ChevronDown, ChevronUp, Check, X } from "lucide-react";
 
 
 
 
 
 
 
 
8
  import { Badge } from "./ui/badge";
9
- import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./ui/collapsible";
 
 
 
 
10
  import { Textarea } from "./ui/textarea";
11
  import type { Message as MessageType } from "../App";
12
  import { toast } from "sonner";
@@ -15,13 +27,14 @@ import clareAvatar from "../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png"
15
  interface MessageProps {
16
  key?: React.Key;
17
  message: MessageType;
18
- showSenderInfo?: boolean;
19
- isFirstGreeting?: boolean;
20
- showNextButton?: boolean;
21
- onNextQuestion?: () => void;
22
- chatMode?: "ask" | "review" | "quiz";
23
  }
24
 
 
25
  const FEEDBACK_TAGS = {
26
  "not-helpful": [
27
  "Code was incorrect",
@@ -30,15 +43,50 @@ const FEEDBACK_TAGS = {
30
  "Don't like the style",
31
  "Not factually correct",
32
  ],
33
- helpful: ["Accurate and helpful", "Clear explanation", "Good examples", "Solved my problem", "Well structured"],
 
 
 
 
 
 
34
  };
35
 
36
- function unescapeMarkdown(s: string) {
37
- // 处理最常见的“被转义导致 Markdown 不生效”的情况:\*\*bold\*\*
38
- return (s || "")
39
- .replace(/\\\*/g, "*")
40
- .replace(/\\_/g, "_")
41
- .replace(/\\`/g, "`");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  }
43
 
44
  export function Message({
@@ -49,17 +97,22 @@ export function Message({
49
  onNextQuestion,
50
  chatMode = "ask",
51
  }: MessageProps) {
52
- const [feedback, setFeedback] = useState<"helpful" | "not-helpful" | null>(null);
 
 
53
  const [copied, setCopied] = useState(false);
54
  const [referencesOpen, setReferencesOpen] = useState(false);
55
  const [showFeedbackArea, setShowFeedbackArea] = useState(false);
56
- const [feedbackType, setFeedbackType] = useState<"helpful" | "not-helpful" | null>(null);
 
 
57
  const [feedbackText, setFeedbackText] = useState("");
58
  const [selectedTags, setSelectedTags] = useState<string[]>([]);
59
  const [nextButtonClicked, setNextButtonClicked] = useState(false);
60
 
61
  const isUser = message.role === "user";
62
- const isWelcomeMessage = isFirstGreeting || message.id === "review-1" || message.id === "quiz-1";
 
63
  const shouldShowActions = isUser ? true : !isWelcomeMessage;
64
 
65
  const handleCopy = async () => {
@@ -91,7 +144,9 @@ export function Message({
91
  };
92
 
93
  const handleTagToggle = (tag: string) => {
94
- setSelectedTags((prev) => (prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]));
 
 
95
  };
96
 
97
  const handleFeedbackSubmit = () => {
@@ -101,21 +156,38 @@ export function Message({
101
  text: feedbackText,
102
  messageId: message.id || message.content.substring(0, 50),
103
  };
 
104
  console.log("Feedback submitted:", feedbackData);
105
  toast.success("感谢您的反馈!");
106
  handleFeedbackClose();
107
  };
108
 
 
 
 
 
109
  const markdownClass = useMemo(() => {
110
- return (
111
- "text-base leading-relaxed max-w-none " +
112
- "whitespace-pre-wrap break-words"
113
- );
 
 
 
 
 
 
 
 
114
  }, []);
115
 
116
  const renderBubbleContent = () => {
117
  if (isUser) {
118
- return <p className="whitespace-pre-wrap text-base break-words">{message.content}</p>;
 
 
 
 
119
  }
120
 
121
  return (
@@ -123,65 +195,120 @@ export function Message({
123
  remarkPlugins={[remarkGfm]}
124
  className={markdownClass}
125
  components={{
 
126
  p: ({ children, ...props }) => (
127
- <p className="my-2 whitespace-pre-wrap break-words" {...props}>
128
  {children}
129
  </p>
130
  ),
 
 
131
  ul: ({ children, ...props }) => (
132
- <ul className="my-2 list-disc pl-5" {...props}>
133
  {children}
134
  </ul>
135
  ),
 
 
136
  ol: ({ children, ...props }) => (
137
- <ol className="my-2 list-decimal pl-5" {...props}>
 
 
 
138
  {children}
139
  </ol>
140
  ),
141
- li: ({ children, ...props }) => (
142
- <li className="my-1" {...props}>
143
- {children}
144
- </li>
145
- ),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  strong: ({ children, ...props }) => (
147
  <strong className="font-semibold" {...props}>
148
  {children}
149
  </strong>
150
  ),
 
151
  em: ({ children, ...props }) => (
152
  <em className="italic" {...props}>
153
  {children}
154
  </em>
155
  ),
 
156
  code: ({ children, ...props }) => (
157
- <code className="px-1 py-0.5 rounded bg-black/5 dark:bg-white/10" {...props}>
 
 
 
158
  {children}
159
  </code>
160
  ),
 
161
  pre: ({ children, ...props }) => (
162
- <pre className="my-2 p-3 rounded-lg overflow-auto bg-black/5 dark:bg-white/10" {...props}>
 
 
 
163
  {children}
164
  </pre>
165
  ),
 
166
  a: ({ children, ...props }) => (
167
- <a className="underline underline-offset-2" target="_blank" rel="noreferrer" {...props}>
 
 
 
 
 
168
  {children}
169
  </a>
170
  ),
171
  }}
172
  >
173
- {unescapeMarkdown(message.content)}
174
  </ReactMarkdown>
175
  );
176
  };
177
 
178
  return (
179
- <div className={`flex gap-2 ${isUser && !showSenderInfo ? "justify-end" : "justify-start"} px-4`}>
 
 
 
 
180
  {/* Avatar */}
181
  {showSenderInfo && message.sender ? (
182
  <div className="w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 overflow-hidden bg-white">
183
  {message.sender.isAI ? (
184
- <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
 
 
 
 
185
  ) : (
186
  <img
187
  src={
@@ -202,40 +329,64 @@ export function Message({
202
  ) : null}
203
 
204
  <div
205
- className={`group flex flex-col gap-2 ${isUser && !showSenderInfo ? "items-end" : "items-start"}`}
 
 
206
  style={{ maxWidth: "min(770px, calc(100% - 2rem))" }}
207
  >
 
208
  {showSenderInfo && message.sender && (
209
  <div className="flex items-center gap-2 px-1">
210
  <span className="text-xs">{message.sender.name}</span>
211
- {message.sender.isAI && <Badge variant="secondary" className="text-xs h-4 px-1">AI</Badge>}
 
 
 
 
212
  </div>
213
  )}
214
 
215
- <div className={`rounded-2xl px-4 py-3 ${isUser && !showSenderInfo ? "bg-primary text-primary-foreground" : "bg-muted"}`}>
 
 
 
 
 
 
216
  {renderBubbleContent()}
217
  </div>
218
 
219
- {!isUser && showNextButton && !nextButtonClicked && chatMode === "quiz" && onNextQuestion && (
220
- <div className="mt-2">
221
- <Button
222
- onClick={() => {
223
- setNextButtonClicked(true);
224
- onNextQuestion();
225
- }}
226
- className="bg-primary hover:bg-primary/90"
227
- >
228
- Next Question
229
- </Button>
230
- </div>
231
- )}
 
 
 
 
 
232
 
 
233
  {message.references && message.references.length > 0 && (
234
  <Collapsible open={referencesOpen} onOpenChange={setReferencesOpen}>
235
  <CollapsibleTrigger asChild>
236
  <Button variant="ghost" size="sm" className="gap-1 h-7 text-xs">
237
- {referencesOpen ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
238
- {message.references.length} {message.references.length === 1 ? "reference" : "references"}
 
 
 
 
 
239
  </Button>
240
  </CollapsibleTrigger>
241
  <CollapsibleContent className="space-y-1 mt-1">
@@ -248,13 +399,16 @@ export function Message({
248
  </Collapsible>
249
  )}
250
 
 
251
  {shouldShowActions && (
252
  <div className="flex items-center gap-1">
253
  <Button
254
  variant="ghost"
255
  size="icon"
256
  className={`h-7 w-7 rounded-md transition-all hover:bg-accent hover:scale-110 active:scale-95 ${
257
- copied ? "bg-green-100 text-green-600 dark:bg-green-900/20 dark:text-green-400" : ""
 
 
258
  }`}
259
  onClick={handleCopy}
260
  title="Copy"
@@ -268,7 +422,9 @@ export function Message({
268
  variant="ghost"
269
  size="icon"
270
  className={`h-7 w-7 rounded-md transition-all hover:bg-accent hover:scale-110 active:scale-95 ${
271
- feedback === "helpful" ? "bg-green-100 text-green-600 dark:bg-green-900/20 dark:text-green-400" : ""
 
 
272
  }`}
273
  onClick={() => handleFeedbackClick("helpful")}
274
  title="Helpful"
@@ -279,7 +435,9 @@ export function Message({
279
  variant="ghost"
280
  size="icon"
281
  className={`h-7 w-7 rounded-md transition-all hover:bg-accent hover:scale-110 active:scale-95 ${
282
- feedback === "not-helpful" ? "bg-red-100 text-red-600 dark:bg-red-900/20 dark:text-red-400" : ""
 
 
283
  }`}
284
  onClick={() => handleFeedbackClick("not-helpful")}
285
  title="Not helpful"
@@ -291,11 +449,19 @@ export function Message({
291
  </div>
292
  )}
293
 
 
294
  {!isUser && showFeedbackArea && feedbackType && (
295
  <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">
296
  <div className="flex items-start justify-between mb-4">
297
- <h4 className="text-sm font-medium text-gray-900 dark:text-gray-100">Tell us more:</h4>
298
- <Button variant="ghost" size="sm" className="h-6 w-6 p-0" onClick={handleFeedbackClose}>
 
 
 
 
 
 
 
299
  <X className="h-4 w-4" />
300
  </Button>
301
  </div>
@@ -325,7 +491,11 @@ export function Message({
325
  <Button variant="outline" size="sm" onClick={handleFeedbackClose}>
326
  Cancel
327
  </Button>
328
- <Button size="sm" onClick={handleFeedbackSubmit} disabled={selectedTags.length === 0 && !feedbackText.trim()}>
 
 
 
 
329
  Submit
330
  </Button>
331
  </div>
@@ -339,7 +509,9 @@ export function Message({
339
  <img
340
  src={
341
  message.sender.avatar ||
342
- `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(message.sender.email || message.sender.name)}`
 
 
343
  }
344
  alt={message.sender.name}
345
  className="w-full h-full object-cover"
 
4
  import ReactMarkdown from "react-markdown";
5
  import remarkGfm from "remark-gfm";
6
 
7
+ import {
8
+ Copy,
9
+ ThumbsUp,
10
+ ThumbsDown,
11
+ ChevronDown,
12
+ ChevronUp,
13
+ Check,
14
+ X,
15
+ } from "lucide-react";
16
  import { Badge } from "./ui/badge";
17
+ import {
18
+ Collapsible,
19
+ CollapsibleContent,
20
+ CollapsibleTrigger,
21
+ } from "./ui/collapsible";
22
  import { Textarea } from "./ui/textarea";
23
  import type { Message as MessageType } from "../App";
24
  import { toast } from "sonner";
 
27
  interface MessageProps {
28
  key?: React.Key;
29
  message: MessageType;
30
+ showSenderInfo?: boolean; // For group chat mode
31
+ isFirstGreeting?: boolean; // Indicates if this is the first greeting message
32
+ showNextButton?: boolean; // For quiz mode
33
+ onNextQuestion?: () => void; // For quiz mode
34
+ chatMode?: "ask" | "review" | "quiz"; // Current chat mode
35
  }
36
 
37
+ // 反馈标签选项
38
  const FEEDBACK_TAGS = {
39
  "not-helpful": [
40
  "Code was incorrect",
 
43
  "Don't like the style",
44
  "Not factually correct",
45
  ],
46
+ helpful: [
47
+ "Accurate and helpful",
48
+ "Clear explanation",
49
+ "Good examples",
50
+ "Solved my problem",
51
+ "Well structured",
52
+ ],
53
  };
54
 
55
+ // ---- Markdown list normalization ----
56
+ // Fixes patterns like:
57
+ // 1.
58
+ //
59
+ // **Title**
60
+ // ... -> 1. **Title**
61
+ function normalizeMarkdownLists(input: string) {
62
+ if (!input) return input;
63
+
64
+ return (
65
+ input
66
+ // ordered list: "1.\n\n text" -> "1. text"
67
+ .replace(/(^|\n)(\s*)(\d+\.)\s*\n+\s+/g, "$1$2$3 ")
68
+ // unordered list: "-\n\n text" -> "- text"
69
+ .replace(/(^|\n)(\s*)([-*+])\s*\n+\s+/g, "$1$2$3 ")
70
+ );
71
+ }
72
+
73
+ function firstTextish(children: React.ReactNode): string {
74
+ const arr = React.Children.toArray(children);
75
+ for (const ch of arr) {
76
+ if (typeof ch === "string") return ch;
77
+ if (React.isValidElement(ch)) {
78
+ const inner = (ch.props as any)?.children;
79
+ const t = firstTextish(inner);
80
+ if (t) return t;
81
+ }
82
+ }
83
+ return "";
84
+ }
85
+
86
+ function startsWithEmojiBullet(text: string) {
87
+ const t = (text || "").trim();
88
+ // 你图二里的风格:✅ / 🔧 等开头就不要再加圆点
89
+ return /^(✅|☑️|✔️|🟢|🔧|⭐️|✨|🔥|👉|➡️|•)/.test(t);
90
  }
91
 
92
  export function Message({
 
97
  onNextQuestion,
98
  chatMode = "ask",
99
  }: MessageProps) {
100
+ const [feedback, setFeedback] = useState<"helpful" | "not-helpful" | null>(
101
+ null
102
+ );
103
  const [copied, setCopied] = useState(false);
104
  const [referencesOpen, setReferencesOpen] = useState(false);
105
  const [showFeedbackArea, setShowFeedbackArea] = useState(false);
106
+ const [feedbackType, setFeedbackType] = useState<
107
+ "helpful" | "not-helpful" | null
108
+ >(null);
109
  const [feedbackText, setFeedbackText] = useState("");
110
  const [selectedTags, setSelectedTags] = useState<string[]>([]);
111
  const [nextButtonClicked, setNextButtonClicked] = useState(false);
112
 
113
  const isUser = message.role === "user";
114
+ const isWelcomeMessage =
115
+ isFirstGreeting || message.id === "review-1" || message.id === "quiz-1";
116
  const shouldShowActions = isUser ? true : !isWelcomeMessage;
117
 
118
  const handleCopy = async () => {
 
144
  };
145
 
146
  const handleTagToggle = (tag: string) => {
147
+ setSelectedTags((prev) =>
148
+ prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]
149
+ );
150
  };
151
 
152
  const handleFeedbackSubmit = () => {
 
156
  text: feedbackText,
157
  messageId: message.id || message.content.substring(0, 50),
158
  };
159
+
160
  console.log("Feedback submitted:", feedbackData);
161
  toast.success("感谢您的反馈!");
162
  handleFeedbackClose();
163
  };
164
 
165
+ const normalizedMarkdown = useMemo(() => {
166
+ return normalizeMarkdownLists(message.content || "");
167
+ }, [message.content]);
168
+
169
  const markdownClass = useMemo(() => {
170
+ return [
171
+ "text-base leading-relaxed break-words",
172
+ // 让段落/列表更紧凑,像产品文案
173
+ " [&_p]:my-2",
174
+ " [&_p:first-child]:mt-0",
175
+ " [&_p:last-child]:mb-0",
176
+ // code / pre
177
+ " [&_code]:px-1 [&_code]:py-0.5 [&_code]:rounded [&_code]:bg-black/5 dark:[&_code]:bg-white/10",
178
+ " [&_pre]:my-2 [&_pre]:p-3 [&_pre]:rounded-lg [&_pre]:overflow-auto [&_pre]:bg-black/5 dark:[&_pre]:bg-white/10",
179
+ // links
180
+ " [&_a]:underline [&_a]:underline-offset-2",
181
+ ].join("");
182
  }, []);
183
 
184
  const renderBubbleContent = () => {
185
  if (isUser) {
186
+ return (
187
+ <p className="whitespace-pre-wrap text-base break-words">
188
+ {message.content}
189
+ </p>
190
+ );
191
  }
192
 
193
  return (
 
195
  remarkPlugins={[remarkGfm]}
196
  className={markdownClass}
197
  components={{
198
+ // paragraphs: keep compact and preserve line breaks
199
  p: ({ children, ...props }) => (
200
+ <p className="whitespace-pre-wrap break-words" {...props}>
201
  {children}
202
  </p>
203
  ),
204
+
205
+ // unordered list: "product" bullet style (dot), but if emoji-leading, no dot
206
  ul: ({ children, ...props }) => (
207
+ <ul className="my-2 space-y-2 pl-0" {...props}>
208
  {children}
209
  </ul>
210
  ),
211
+
212
+ // ordered list: keep decimal marker but nicer spacing + prevent huge gaps
213
  ol: ({ children, ...props }) => (
214
+ <ol
215
+ className="my-2 space-y-4 pl-6 list-decimal marker:font-semibold marker:text-foreground/60"
216
+ {...props}
217
+ >
218
  {children}
219
  </ol>
220
  ),
221
+
222
+ li: ({ children, node, ...props }) => {
223
+ const parentTag = (node as any)?.parent?.tagName;
224
+
225
+ // For UL: custom bullet layout
226
+ if (parentTag === "ul") {
227
+ const first = firstTextish(children);
228
+ const emoji = startsWithEmojiBullet(first);
229
+
230
+ return (
231
+ <li className="list-none" {...props}>
232
+ <div className="flex gap-2">
233
+ {!emoji ? (
234
+ <span className="mt-[0.62rem] h-1.5 w-1.5 rounded-full bg-foreground/40 flex-shrink-0" />
235
+ ) : null}
236
+ <div className="min-w-0">{children}</div>
237
+ </div>
238
+ </li>
239
+ );
240
+ }
241
+
242
+ // For OL: keep marker, but tighten inner spacing
243
+ return (
244
+ <li className="pl-1 [&_p]:my-1" {...props}>
245
+ {children}
246
+ </li>
247
+ );
248
+ },
249
+
250
  strong: ({ children, ...props }) => (
251
  <strong className="font-semibold" {...props}>
252
  {children}
253
  </strong>
254
  ),
255
+
256
  em: ({ children, ...props }) => (
257
  <em className="italic" {...props}>
258
  {children}
259
  </em>
260
  ),
261
+
262
  code: ({ children, ...props }) => (
263
+ <code
264
+ className="px-1 py-0.5 rounded bg-black/5 dark:bg-white/10"
265
+ {...props}
266
+ >
267
  {children}
268
  </code>
269
  ),
270
+
271
  pre: ({ children, ...props }) => (
272
+ <pre
273
+ className="my-2 p-3 rounded-lg overflow-auto bg-black/5 dark:bg-white/10"
274
+ {...props}
275
+ >
276
  {children}
277
  </pre>
278
  ),
279
+
280
  a: ({ children, ...props }) => (
281
+ <a
282
+ className="underline underline-offset-2"
283
+ target="_blank"
284
+ rel="noreferrer"
285
+ {...props}
286
+ >
287
  {children}
288
  </a>
289
  ),
290
  }}
291
  >
292
+ {normalizedMarkdown}
293
  </ReactMarkdown>
294
  );
295
  };
296
 
297
  return (
298
+ <div
299
+ className={`flex gap-2 ${
300
+ isUser && !showSenderInfo ? "justify-end" : "justify-start"
301
+ } px-4`}
302
+ >
303
  {/* Avatar */}
304
  {showSenderInfo && message.sender ? (
305
  <div className="w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 overflow-hidden bg-white">
306
  {message.sender.isAI ? (
307
+ <img
308
+ src={clareAvatar}
309
+ alt="Clare"
310
+ className="w-full h-full object-cover"
311
+ />
312
  ) : (
313
  <img
314
  src={
 
329
  ) : null}
330
 
331
  <div
332
+ className={`group flex flex-col gap-2 ${
333
+ isUser && !showSenderInfo ? "items-end" : "items-start"
334
+ }`}
335
  style={{ maxWidth: "min(770px, calc(100% - 2rem))" }}
336
  >
337
+ {/* Sender name in group chat */}
338
  {showSenderInfo && message.sender && (
339
  <div className="flex items-center gap-2 px-1">
340
  <span className="text-xs">{message.sender.name}</span>
341
+ {message.sender.isAI && (
342
+ <Badge variant="secondary" className="text-xs h-4 px-1">
343
+ AI
344
+ </Badge>
345
+ )}
346
  </div>
347
  )}
348
 
349
+ {/* Bubble */}
350
+ <div
351
+ className={`
352
+ rounded-2xl px-4 py-3
353
+ ${isUser && !showSenderInfo ? "bg-primary text-primary-foreground" : "bg-muted"}
354
+ `}
355
+ >
356
  {renderBubbleContent()}
357
  </div>
358
 
359
+ {/* Next Question Button for Quiz Mode */}
360
+ {!isUser &&
361
+ showNextButton &&
362
+ !nextButtonClicked &&
363
+ chatMode === "quiz" &&
364
+ onNextQuestion && (
365
+ <div className="mt-2">
366
+ <Button
367
+ onClick={() => {
368
+ setNextButtonClicked(true);
369
+ onNextQuestion();
370
+ }}
371
+ className="bg-primary hover:bg-primary/90"
372
+ >
373
+ Next Question
374
+ </Button>
375
+ </div>
376
+ )}
377
 
378
+ {/* References */}
379
  {message.references && message.references.length > 0 && (
380
  <Collapsible open={referencesOpen} onOpenChange={setReferencesOpen}>
381
  <CollapsibleTrigger asChild>
382
  <Button variant="ghost" size="sm" className="gap-1 h-7 text-xs">
383
+ {referencesOpen ? (
384
+ <ChevronUp className="h-3 w-3" />
385
+ ) : (
386
+ <ChevronDown className="h-3 w-3" />
387
+ )}
388
+ {message.references.length}{" "}
389
+ {message.references.length === 1 ? "reference" : "references"}
390
  </Button>
391
  </CollapsibleTrigger>
392
  <CollapsibleContent className="space-y-1 mt-1">
 
399
  </Collapsible>
400
  )}
401
 
402
+ {/* Message Actions */}
403
  {shouldShowActions && (
404
  <div className="flex items-center gap-1">
405
  <Button
406
  variant="ghost"
407
  size="icon"
408
  className={`h-7 w-7 rounded-md transition-all hover:bg-accent hover:scale-110 active:scale-95 ${
409
+ copied
410
+ ? "bg-green-100 text-green-600 dark:bg-green-900/20 dark:text-green-400"
411
+ : ""
412
  }`}
413
  onClick={handleCopy}
414
  title="Copy"
 
422
  variant="ghost"
423
  size="icon"
424
  className={`h-7 w-7 rounded-md transition-all hover:bg-accent hover:scale-110 active:scale-95 ${
425
+ feedback === "helpful"
426
+ ? "bg-green-100 text-green-600 dark:bg-green-900/20 dark:text-green-400"
427
+ : ""
428
  }`}
429
  onClick={() => handleFeedbackClick("helpful")}
430
  title="Helpful"
 
435
  variant="ghost"
436
  size="icon"
437
  className={`h-7 w-7 rounded-md transition-all hover:bg-accent hover:scale-110 active:scale-95 ${
438
+ feedback === "not-helpful"
439
+ ? "bg-red-100 text-red-600 dark:bg-red-900/20 dark:text-red-400"
440
+ : ""
441
  }`}
442
  onClick={() => handleFeedbackClick("not-helpful")}
443
  title="Not helpful"
 
449
  </div>
450
  )}
451
 
452
+ {/* Feedback Area */}
453
  {!isUser && showFeedbackArea && feedbackType && (
454
  <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">
455
  <div className="flex items-start justify-between mb-4">
456
+ <h4 className="text-sm font-medium text-gray-900 dark:text-gray-100">
457
+ Tell us more:
458
+ </h4>
459
+ <Button
460
+ variant="ghost"
461
+ size="sm"
462
+ className="h-6 w-6 p-0"
463
+ onClick={handleFeedbackClose}
464
+ >
465
  <X className="h-4 w-4" />
466
  </Button>
467
  </div>
 
491
  <Button variant="outline" size="sm" onClick={handleFeedbackClose}>
492
  Cancel
493
  </Button>
494
+ <Button
495
+ size="sm"
496
+ onClick={handleFeedbackSubmit}
497
+ disabled={selectedTags.length === 0 && !feedbackText.trim()}
498
+ >
499
  Submit
500
  </Button>
501
  </div>
 
509
  <img
510
  src={
511
  message.sender.avatar ||
512
+ `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(
513
+ message.sender.email || message.sender.name
514
+ )}`
515
  }
516
  alt={message.sender.name}
517
  className="w-full h-full object-cover"