SarahXia0405 commited on
Commit
b976f1b
·
verified ·
1 Parent(s): 6277dae

Update web/src/components/Message.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/Message.tsx +150 -143
web/src/components/Message.tsx CHANGED
@@ -1,23 +1,24 @@
1
- import React, { useState } from 'react';
2
- import { Button } from './ui/button';
 
3
  import ReactMarkdown from "react-markdown";
4
  import remarkGfm from "remark-gfm";
5
 
6
- import {
7
- Copy,
8
- ThumbsUp,
9
- ThumbsDown,
10
- ChevronDown,
11
  ChevronUp,
12
  Check,
13
- X
14
- } from 'lucide-react';
15
- import { Badge } from './ui/badge';
16
- import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible';
17
- import { Textarea } from './ui/textarea';
18
- import type { Message as MessageType } from '../App';
19
- import { toast } from 'sonner';
20
- import clareAvatar from '../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png';
21
 
22
  interface MessageProps {
23
  key?: React.Key;
@@ -26,69 +27,58 @@ interface MessageProps {
26
  isFirstGreeting?: boolean; // Indicates if this is the first greeting message
27
  showNextButton?: boolean; // For quiz mode
28
  onNextQuestion?: () => void; // For quiz mode
29
- chatMode?: 'ask' | 'review' | 'quiz'; // Current chat mode
30
  }
31
 
32
  // 反馈标签选项
33
  const FEEDBACK_TAGS = {
34
- 'not-helpful': [
35
- 'Code was incorrect',
36
  "Shouldn't have used Memory",
37
  "Don't like the personality",
38
  "Don't like the style",
39
- 'Not factually correct',
40
- ],
41
- 'helpful': [
42
- 'Accurate and helpful',
43
- 'Clear explanation',
44
- 'Good examples',
45
- 'Solved my problem',
46
- 'Well structured',
47
  ],
 
48
  };
49
 
50
- export function Message({
51
- message,
52
- showSenderInfo = false,
53
  isFirstGreeting = false,
54
  showNextButton = false,
55
  onNextQuestion,
56
- chatMode = 'ask',
57
  }: MessageProps) {
58
- const [feedback, setFeedback] = useState<'helpful' | 'not-helpful' | null>(null);
59
  const [copied, setCopied] = useState(false);
60
  const [referencesOpen, setReferencesOpen] = useState(false);
61
  const [showFeedbackArea, setShowFeedbackArea] = useState(false);
62
- const [feedbackType, setFeedbackType] = useState<'helpful' | 'not-helpful' | null>(null);
63
- const [feedbackText, setFeedbackText] = useState('');
64
  const [selectedTags, setSelectedTags] = useState<string[]>([]);
65
- const [isHovered, setIsHovered] = useState(false);
66
  const [nextButtonClicked, setNextButtonClicked] = useState(false);
67
-
68
- const isUser = message.role === 'user';
69
- // For user messages: always show. For assistant messages: always show except first greeting (or review-1, quiz-1)
70
- const isWelcomeMessage = isFirstGreeting || message.id === 'review-1' || message.id === 'quiz-1';
71
  const shouldShowActions = isUser ? true : !isWelcomeMessage;
72
 
73
  const handleCopy = async () => {
74
  await navigator.clipboard.writeText(message.content);
75
  setCopied(true);
76
- toast.success('Message copied to clipboard');
77
  setTimeout(() => setCopied(false), 2000);
78
  };
79
 
80
- const handleFeedbackClick = (type: 'helpful' | 'not-helpful') => {
81
  if (feedback === type) {
82
- // 如果点击的是已选中的反馈,则关闭反馈区域
83
  setFeedback(null);
84
  setShowFeedbackArea(false);
85
  setFeedbackType(null);
86
- setFeedbackText('');
87
  setSelectedTags([]);
88
  } else {
89
- // 立即设置反馈状态(按钮会立即显示颜色)
90
  setFeedback(type);
91
- // 打开反馈区域
92
  setFeedbackType(type);
93
  setShowFeedbackArea(true);
94
  }
@@ -97,50 +87,122 @@ export function Message({
97
  const handleFeedbackClose = () => {
98
  setShowFeedbackArea(false);
99
  setFeedbackType(null);
100
- setFeedbackText('');
101
  setSelectedTags([]);
102
- // 注意:不重置 feedback,保持点赞/点踩的状态
103
  };
104
 
105
  const handleTagToggle = (tag: string) => {
106
- setSelectedTags(prev =>
107
- prev.includes(tag)
108
- ? prev.filter(t => t !== tag)
109
- : [...prev, tag]
110
- );
111
  };
112
 
113
  const handleFeedbackSubmit = () => {
114
- // 这里可以发送反馈到后端
115
  const feedbackData = {
116
  type: feedbackType,
117
  tags: selectedTags,
118
  text: feedbackText,
119
  messageId: message.id || message.content.substring(0, 50),
120
  };
121
-
122
- console.log('Feedback submitted:', feedbackData);
123
- toast.success('感谢您的反馈!');
124
  handleFeedbackClose();
125
- // 保持反馈状态(点赞/点踩)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  };
127
 
128
  return (
129
- <div className={`flex gap-2 ${isUser && !showSenderInfo ? 'justify-end' : 'justify-start'} px-4`}>
130
  {/* Avatar */}
131
  {showSenderInfo && message.sender ? (
132
- <div className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${
133
- message.sender.isAI
134
- ? 'overflow-hidden bg-white'
135
- : 'overflow-hidden bg-white'
136
- }`}>
137
  {message.sender.isAI ? (
138
  <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
139
  ) : (
140
  <img
141
  src={
142
- message.sender.avatar ||
143
- `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(message.sender.email || message.sender.name)}`
 
 
144
  }
145
  alt={message.sender.name}
146
  className="w-full h-full object-cover"
@@ -153,59 +215,30 @@ export function Message({
153
  </div>
154
  ) : null}
155
 
156
- <div
157
- className={`group flex flex-col gap-2 ${isUser && !showSenderInfo ? 'items-end' : 'items-start'}`}
158
- style={{ maxWidth: 'min(770px, calc(100% - 2rem))' }}
159
- onMouseEnter={() => setIsHovered(true)}
160
- onMouseLeave={() => setIsHovered(false)}
161
  >
162
  {/* Sender name in group chat */}
163
  {showSenderInfo && message.sender && (
164
  <div className="flex items-center gap-2 px-1">
165
  <span className="text-xs">{message.sender.name}</span>
166
- {message.sender.isAI && (
167
- <Badge variant="secondary" className="text-xs h-4 px-1">AI</Badge>
168
- )}
169
  </div>
170
  )}
171
 
 
172
  <div
173
  className={`
174
  rounded-2xl px-4 py-3
175
- ${isUser && !showSenderInfo
176
- ? 'bg-primary text-primary-foreground'
177
- : 'bg-muted'
178
- }
179
  `}
180
  >
181
- {/* ✅ Render markdown so **bold** becomes bold (no asterisks shown) */}
182
- <ReactMarkdown
183
- remarkPlugins={[remarkGfm]}
184
- className={[
185
- "text-base",
186
- "whitespace-pre-wrap",
187
- "prose prose-sm max-w-none",
188
- "prose-p:my-1",
189
- "prose-ul:my-1 prose-ol:my-1",
190
- "prose-li:my-0",
191
- "prose-strong:font-semibold",
192
- "dark:prose-invert",
193
- "prose-code:before:content-[''] prose-code:after:content-['']",
194
- "prose-pre:bg-background/60 prose-pre:border prose-pre:border-border prose-pre:rounded-lg",
195
- "prose-pre:p-3",
196
- ].join(" ")}
197
- components={{
198
- a: ({ node, ...props }) => (
199
- <a {...props} target="_blank" rel="noreferrer" />
200
- ),
201
- }}
202
- >
203
- {message.content}
204
- </ReactMarkdown>
205
  </div>
206
 
207
  {/* Next Question Button for Quiz Mode */}
208
- {!isUser && showNextButton && !nextButtonClicked && chatMode === 'quiz' && onNextQuestion && (
209
  <div className="mt-2">
210
  <Button
211
  onClick={() => {
@@ -224,12 +257,8 @@ export function Message({
224
  <Collapsible open={referencesOpen} onOpenChange={setReferencesOpen}>
225
  <CollapsibleTrigger asChild>
226
  <Button variant="ghost" size="sm" className="gap-1 h-7 text-xs">
227
- {referencesOpen ? (
228
- <ChevronUp className="h-3 w-3" />
229
- ) : (
230
- <ChevronDown className="h-3 w-3" />
231
- )}
232
- {message.references.length} {message.references.length === 1 ? 'reference' : 'references'}
233
  </Button>
234
  </CollapsibleTrigger>
235
  <CollapsibleContent className="space-y-1 mt-1">
@@ -249,16 +278,12 @@ export function Message({
249
  variant="ghost"
250
  size="icon"
251
  className={`h-7 w-7 rounded-md transition-all hover:bg-accent hover:scale-110 active:scale-95 ${
252
- copied ? 'bg-green-100 text-green-600 dark:bg-green-900/20 dark:text-green-400' : ''
253
  }`}
254
  onClick={handleCopy}
255
  title="Copy"
256
  >
257
- {copied ? (
258
- <Check className="h-4 w-4" />
259
- ) : (
260
- <Copy className="h-4 w-4" />
261
- )}
262
  </Button>
263
 
264
  {!isUser && (
@@ -267,9 +292,9 @@ export function Message({
267
  variant="ghost"
268
  size="icon"
269
  className={`h-7 w-7 rounded-md transition-all hover:bg-accent hover:scale-110 active:scale-95 ${
270
- feedback === 'helpful' ? 'bg-green-100 text-green-600 dark:bg-green-900/20 dark:text-green-400' : ''
271
  }`}
272
- onClick={() => handleFeedbackClick('helpful')}
273
  title="Helpful"
274
  >
275
  <ThumbsUp className="h-4 w-4" />
@@ -278,9 +303,9 @@ export function Message({
278
  variant="ghost"
279
  size="icon"
280
  className={`h-7 w-7 rounded-md transition-all hover:bg-accent hover:scale-110 active:scale-95 ${
281
- feedback === 'not-helpful' ? 'bg-red-100 text-red-600 dark:bg-red-900/20 dark:text-red-400' : ''
282
  }`}
283
- onClick={() => handleFeedbackClick('not-helpful')}
284
  title="Not helpful"
285
  >
286
  <ThumbsDown className="h-4 w-4" />
@@ -290,29 +315,21 @@ export function Message({
290
  </div>
291
  )}
292
 
293
- {/* Feedback Area - 展开在消息下方 */}
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">
298
- Tell us more:
299
- </h4>
300
- <Button
301
- variant="ghost"
302
- size="sm"
303
- className="h-6 w-6 p-0"
304
- onClick={handleFeedbackClose}
305
- >
306
  <X className="h-4 w-4" />
307
  </Button>
308
  </div>
309
-
310
- {/* 标签按钮 */}
311
  <div className="flex flex-wrap gap-2 mb-4">
312
  {FEEDBACK_TAGS[feedbackType].map((tag) => (
313
  <Button
314
  key={tag}
315
- variant={selectedTags.includes(tag) ? 'default' : 'outline'}
316
  size="sm"
317
  className="h-7 text-xs"
318
  onClick={() => handleTagToggle(tag)}
@@ -322,7 +339,6 @@ export function Message({
322
  ))}
323
  </div>
324
 
325
- {/* 文本输入框 */}
326
  <Textarea
327
  className="min-h-[60px] mb-4 bg-gray-100/50 dark:bg-gray-700/50 border-gray-200 dark:border-gray-600"
328
  value={feedbackText}
@@ -330,20 +346,11 @@ export function Message({
330
  placeholder="Additional feedback (optional)..."
331
  />
332
 
333
- {/* 提交按钮 */}
334
  <div className="flex justify-end gap-2">
335
- <Button
336
- variant="outline"
337
- size="sm"
338
- onClick={handleFeedbackClose}
339
- >
340
  Cancel
341
  </Button>
342
- <Button
343
- size="sm"
344
- onClick={handleFeedbackSubmit}
345
- disabled={selectedTags.length === 0 && !feedbackText.trim()}
346
- >
347
  Submit
348
  </Button>
349
  </div>
@@ -356,7 +363,7 @@ export function Message({
356
  {message.sender ? (
357
  <img
358
  src={
359
- message.sender.avatar ||
360
  `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(message.sender.email || message.sender.name)}`
361
  }
362
  alt={message.sender.name}
 
1
+ // web/src/components/Message.tsx
2
+ 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
 
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 { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./ui/collapsible";
18
+ import { Textarea } from "./ui/textarea";
19
+ import type { Message as MessageType } from "../App";
20
+ import { toast } from "sonner";
21
+ import clareAvatar from "../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png";
22
 
23
  interface MessageProps {
24
  key?: React.Key;
 
27
  isFirstGreeting?: boolean; // Indicates if this is the first greeting message
28
  showNextButton?: boolean; // For quiz mode
29
  onNextQuestion?: () => void; // For quiz mode
30
+ chatMode?: "ask" | "review" | "quiz"; // Current chat mode
31
  }
32
 
33
  // 反馈标签选项
34
  const FEEDBACK_TAGS = {
35
+ "not-helpful": [
36
+ "Code was incorrect",
37
  "Shouldn't have used Memory",
38
  "Don't like the personality",
39
  "Don't like the style",
40
+ "Not factually correct",
 
 
 
 
 
 
 
41
  ],
42
+ helpful: ["Accurate and helpful", "Clear explanation", "Good examples", "Solved my problem", "Well structured"],
43
  };
44
 
45
+ export function Message({
46
+ message,
47
+ showSenderInfo = false,
48
  isFirstGreeting = false,
49
  showNextButton = false,
50
  onNextQuestion,
51
+ chatMode = "ask",
52
  }: MessageProps) {
53
+ const [feedback, setFeedback] = useState<"helpful" | "not-helpful" | null>(null);
54
  const [copied, setCopied] = useState(false);
55
  const [referencesOpen, setReferencesOpen] = useState(false);
56
  const [showFeedbackArea, setShowFeedbackArea] = useState(false);
57
+ const [feedbackType, setFeedbackType] = useState<"helpful" | "not-helpful" | null>(null);
58
+ const [feedbackText, setFeedbackText] = useState("");
59
  const [selectedTags, setSelectedTags] = useState<string[]>([]);
 
60
  const [nextButtonClicked, setNextButtonClicked] = useState(false);
61
+
62
+ const isUser = message.role === "user";
63
+ const isWelcomeMessage = isFirstGreeting || message.id === "review-1" || message.id === "quiz-1";
 
64
  const shouldShowActions = isUser ? true : !isWelcomeMessage;
65
 
66
  const handleCopy = async () => {
67
  await navigator.clipboard.writeText(message.content);
68
  setCopied(true);
69
+ toast.success("Message copied to clipboard");
70
  setTimeout(() => setCopied(false), 2000);
71
  };
72
 
73
+ const handleFeedbackClick = (type: "helpful" | "not-helpful") => {
74
  if (feedback === type) {
 
75
  setFeedback(null);
76
  setShowFeedbackArea(false);
77
  setFeedbackType(null);
78
+ setFeedbackText("");
79
  setSelectedTags([]);
80
  } else {
 
81
  setFeedback(type);
 
82
  setFeedbackType(type);
83
  setShowFeedbackArea(true);
84
  }
 
87
  const handleFeedbackClose = () => {
88
  setShowFeedbackArea(false);
89
  setFeedbackType(null);
90
+ setFeedbackText("");
91
  setSelectedTags([]);
92
+ // 不重置 feedback,保持点赞/点踩的状态
93
  };
94
 
95
  const handleTagToggle = (tag: string) => {
96
+ setSelectedTags((prev) => (prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]));
 
 
 
 
97
  };
98
 
99
  const handleFeedbackSubmit = () => {
 
100
  const feedbackData = {
101
  type: feedbackType,
102
  tags: selectedTags,
103
  text: feedbackText,
104
  messageId: message.id || message.content.substring(0, 50),
105
  };
106
+
107
+ console.log("Feedback submitted:", feedbackData);
108
+ toast.success("感谢您的反馈!");
109
  handleFeedbackClose();
110
+ };
111
+
112
+ // ✅ Markdown renderer (only for assistant; user keeps plain text)
113
+ const markdownClass = useMemo(() => {
114
+ // 这里用 prose 让列表/加粗/斜体更自然;并控制尺寸不要太夸张
115
+ // 注意:Tailwind 的 prose 需要你项目里已启用 typography 插件;如果你没启用,也不会报错,只是 class 不生效
116
+ // 即便 prose 不生效,下面 components 也能保证基本样式
117
+ const base =
118
+ "text-base leading-relaxed " +
119
+ "prose prose-sm max-w-none " +
120
+ "prose-p:my-2 prose-ul:my-2 prose-ol:my-2 prose-li:my-1 " +
121
+ "prose-strong:font-semibold prose-code:px-1 prose-code:py-0.5 prose-code:rounded " +
122
+ "prose-pre:p-3 prose-pre:rounded-lg " +
123
+ "whitespace-pre-wrap break-words";
124
+ return base;
125
+ }, []);
126
+
127
+ const renderBubbleContent = () => {
128
+ if (isUser) {
129
+ return <p className="whitespace-pre-wrap text-base break-words">{message.content}</p>;
130
+ }
131
+
132
+ return (
133
+ <ReactMarkdown
134
+ remarkPlugins={[remarkGfm]}
135
+ className={markdownClass}
136
+ components={{
137
+ // 保证段落不要把气泡撑得很松
138
+ p: ({ children, ...props }) => (
139
+ <p className="my-2 whitespace-pre-wrap break-words" {...props}>
140
+ {children}
141
+ </p>
142
+ ),
143
+ ul: ({ children, ...props }) => (
144
+ <ul className="my-2 list-disc pl-5" {...props}>
145
+ {children}
146
+ </ul>
147
+ ),
148
+ ol: ({ children, ...props }) => (
149
+ <ol className="my-2 list-decimal pl-5" {...props}>
150
+ {children}
151
+ </ol>
152
+ ),
153
+ li: ({ children, ...props }) => (
154
+ <li className="my-1" {...props}>
155
+ {children}
156
+ </li>
157
+ ),
158
+ strong: ({ children, ...props }) => (
159
+ <strong className="font-semibold" {...props}>
160
+ {children}
161
+ </strong>
162
+ ),
163
+ em: ({ children, ...props }) => (
164
+ <em className="italic" {...props}>
165
+ {children}
166
+ </em>
167
+ ),
168
+ // 避免 code 把气泡撑开
169
+ code: ({ children, ...props }) => (
170
+ <code className="px-1 py-0.5 rounded bg-black/5 dark:bg-white/10" {...props}>
171
+ {children}
172
+ </code>
173
+ ),
174
+ pre: ({ children, ...props }) => (
175
+ <pre className="my-2 p-3 rounded-lg overflow-auto bg-black/5 dark:bg-white/10" {...props}>
176
+ {children}
177
+ </pre>
178
+ ),
179
+ // 你如果不想自动生成链接样式,可以在这里控制
180
+ a: ({ children, ...props }) => (
181
+ <a className="underline underline-offset-2" target="_blank" rel="noreferrer" {...props}>
182
+ {children}
183
+ </a>
184
+ ),
185
+ }}
186
+ >
187
+ {message.content}
188
+ </ReactMarkdown>
189
+ );
190
  };
191
 
192
  return (
193
+ <div className={`flex gap-2 ${isUser && !showSenderInfo ? "justify-end" : "justify-start"} px-4`}>
194
  {/* Avatar */}
195
  {showSenderInfo && message.sender ? (
196
+ <div className="w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 overflow-hidden bg-white">
 
 
 
 
197
  {message.sender.isAI ? (
198
  <img src={clareAvatar} alt="Clare" className="w-full h-full object-cover" />
199
  ) : (
200
  <img
201
  src={
202
+ message.sender.avatar ||
203
+ `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(
204
+ message.sender.email || message.sender.name
205
+ )}`
206
  }
207
  alt={message.sender.name}
208
  className="w-full h-full object-cover"
 
215
  </div>
216
  ) : null}
217
 
218
+ <div
219
+ className={`group flex flex-col gap-2 ${isUser && !showSenderInfo ? "items-end" : "items-start"}`}
220
+ style={{ maxWidth: "min(770px, calc(100% - 2rem))" }}
 
 
221
  >
222
  {/* Sender name in group chat */}
223
  {showSenderInfo && message.sender && (
224
  <div className="flex items-center gap-2 px-1">
225
  <span className="text-xs">{message.sender.name}</span>
226
+ {message.sender.isAI && <Badge variant="secondary" className="text-xs h-4 px-1">AI</Badge>}
 
 
227
  </div>
228
  )}
229
 
230
+ {/* Bubble */}
231
  <div
232
  className={`
233
  rounded-2xl px-4 py-3
234
+ ${isUser && !showSenderInfo ? "bg-primary text-primary-foreground" : "bg-muted"}
 
 
 
235
  `}
236
  >
237
+ {renderBubbleContent()}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
238
  </div>
239
 
240
  {/* Next Question Button for Quiz Mode */}
241
+ {!isUser && showNextButton && !nextButtonClicked && chatMode === "quiz" && onNextQuestion && (
242
  <div className="mt-2">
243
  <Button
244
  onClick={() => {
 
257
  <Collapsible open={referencesOpen} onOpenChange={setReferencesOpen}>
258
  <CollapsibleTrigger asChild>
259
  <Button variant="ghost" size="sm" className="gap-1 h-7 text-xs">
260
+ {referencesOpen ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
261
+ {message.references.length} {message.references.length === 1 ? "reference" : "references"}
 
 
 
 
262
  </Button>
263
  </CollapsibleTrigger>
264
  <CollapsibleContent className="space-y-1 mt-1">
 
278
  variant="ghost"
279
  size="icon"
280
  className={`h-7 w-7 rounded-md transition-all hover:bg-accent hover:scale-110 active:scale-95 ${
281
+ copied ? "bg-green-100 text-green-600 dark:bg-green-900/20 dark:text-green-400" : ""
282
  }`}
283
  onClick={handleCopy}
284
  title="Copy"
285
  >
286
+ {copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
 
 
 
 
287
  </Button>
288
 
289
  {!isUser && (
 
292
  variant="ghost"
293
  size="icon"
294
  className={`h-7 w-7 rounded-md transition-all hover:bg-accent hover:scale-110 active:scale-95 ${
295
+ feedback === "helpful" ? "bg-green-100 text-green-600 dark:bg-green-900/20 dark:text-green-400" : ""
296
  }`}
297
+ onClick={() => handleFeedbackClick("helpful")}
298
  title="Helpful"
299
  >
300
  <ThumbsUp className="h-4 w-4" />
 
303
  variant="ghost"
304
  size="icon"
305
  className={`h-7 w-7 rounded-md transition-all hover:bg-accent hover:scale-110 active:scale-95 ${
306
+ feedback === "not-helpful" ? "bg-red-100 text-red-600 dark:bg-red-900/20 dark:text-red-400" : ""
307
  }`}
308
+ onClick={() => handleFeedbackClick("not-helpful")}
309
  title="Not helpful"
310
  >
311
  <ThumbsDown className="h-4 w-4" />
 
315
  </div>
316
  )}
317
 
318
+ {/* Feedback Area */}
319
  {!isUser && showFeedbackArea && feedbackType && (
320
  <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">
321
  <div className="flex items-start justify-between mb-4">
322
+ <h4 className="text-sm font-medium text-gray-900 dark:text-gray-100">Tell us more:</h4>
323
+ <Button variant="ghost" size="sm" className="h-6 w-6 p-0" onClick={handleFeedbackClose}>
 
 
 
 
 
 
 
324
  <X className="h-4 w-4" />
325
  </Button>
326
  </div>
327
+
 
328
  <div className="flex flex-wrap gap-2 mb-4">
329
  {FEEDBACK_TAGS[feedbackType].map((tag) => (
330
  <Button
331
  key={tag}
332
+ variant={selectedTags.includes(tag) ? "default" : "outline"}
333
  size="sm"
334
  className="h-7 text-xs"
335
  onClick={() => handleTagToggle(tag)}
 
339
  ))}
340
  </div>
341
 
 
342
  <Textarea
343
  className="min-h-[60px] mb-4 bg-gray-100/50 dark:bg-gray-700/50 border-gray-200 dark:border-gray-600"
344
  value={feedbackText}
 
346
  placeholder="Additional feedback (optional)..."
347
  />
348
 
 
349
  <div className="flex justify-end gap-2">
350
+ <Button variant="outline" size="sm" onClick={handleFeedbackClose}>
 
 
 
 
351
  Cancel
352
  </Button>
353
+ <Button size="sm" onClick={handleFeedbackSubmit} disabled={selectedTags.length === 0 && !feedbackText.trim()}>
 
 
 
 
354
  Submit
355
  </Button>
356
  </div>
 
363
  {message.sender ? (
364
  <img
365
  src={
366
+ message.sender.avatar ||
367
  `https://api.dicebear.com/7.x/avataaars/svg?seed=${encodeURIComponent(message.sender.email || message.sender.name)}`
368
  }
369
  alt={message.sender.name}