SarahXia0405 commited on
Commit
d76cdcf
·
verified ·
1 Parent(s): 4b372df

Update web/src/components/Message.tsx

Browse files
Files changed (1) hide show
  1. web/src/components/Message.tsx +60 -75
web/src/components/Message.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react';
2
  import { Button } from './ui/button';
3
  import {
4
  Copy,
@@ -14,52 +14,56 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collap
14
  import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from './ui/dialog';
15
  import { Textarea } from './ui/textarea';
16
  import type { Message as MessageType, LearningMode } from '../App';
17
- import { toast } from 'sonner@2.0.3';
18
 
19
  interface MessageProps {
20
  message: MessageType;
21
- showSenderInfo?: boolean; // For group chat mode
22
 
23
- // ✅ 新增:写入 /api/feedback 需要的信息
24
  userId: string;
 
25
  learningMode: LearningMode;
26
- docType?: string;
27
- lastUserText?: string; // 可选:如果你想把本轮 user 的问题也一起上报
28
- }
29
 
30
- type UIFeedback = 'helpful' | 'not-helpful';
 
 
31
 
32
- async function postJson<T>(url: string, payload: any): Promise<T> {
33
- const res = await fetch(url, {
34
  method: 'POST',
35
  headers: { 'Content-Type': 'application/json' },
36
  body: JSON.stringify(payload),
37
  });
38
  if (!res.ok) {
39
- const text = await res.text().catch(() => '');
40
- throw new Error(`HTTP ${res.status}: ${text || res.statusText}`);
41
  }
42
- return (await res.json()) as T;
43
  }
44
 
45
  export function Message({
46
  message,
47
  showSenderInfo = false,
48
  userId,
 
49
  learningMode,
50
- docType = '',
51
  lastUserText = '',
52
  }: MessageProps) {
53
- const [feedback, setFeedback] = useState<UIFeedback | null>(null);
54
  const [copied, setCopied] = useState(false);
55
  const [referencesOpen, setReferencesOpen] = useState(false);
56
  const [showFeedbackDialog, setShowFeedbackDialog] = useState(false);
57
- const [feedbackType, setFeedbackType] = useState<UIFeedback | null>(null);
58
  const [feedbackText, setFeedbackText] = useState('');
59
  const [submitting, setSubmitting] = useState(false);
60
 
61
  const isUser = message.role === 'user';
62
 
 
 
63
  const handleCopy = async () => {
64
  await navigator.clipboard.writeText(message.content);
65
  setCopied(true);
@@ -67,10 +71,9 @@ export function Message({
67
  setTimeout(() => setCopied(false), 2000);
68
  };
69
 
70
- const handleFeedbackDialogOpen = (type: UIFeedback) => {
71
- // 已经提交过就不再允许重复写入
72
- if (feedback) {
73
- toast.message('Feedback already recorded for this message.');
74
  return;
75
  }
76
  setFeedbackType(type);
@@ -86,27 +89,26 @@ export function Message({
86
  const handleFeedbackDialogSubmit = async () => {
87
  if (!feedbackType) return;
88
 
89
- // UI: not-helpful -> API: not_helpful
90
- const apiRating = feedbackType === 'helpful' ? 'helpful' : 'not_helpful';
 
 
91
 
92
  try {
93
  setSubmitting(true);
94
 
95
- const payload = {
96
- user_id: (userId || '').trim(),
97
- rating: apiRating,
98
  assistant_message_id: message.id,
99
- assistant_text: message.content || '',
100
  user_text: lastUserText || '',
101
- comment: feedbackText.trim() || '',
102
- refs: message.references || [],
103
  learning_mode: learningMode,
104
  doc_type: docType,
105
  timestamp_ms: Date.now(),
106
- };
107
-
108
- const resp = await postJson<{ ok: boolean }>('/api/feedback', payload);
109
- if (!resp?.ok) throw new Error('ok=false');
110
 
111
  setFeedback(feedbackType);
112
  toast.success('Thanks for your feedback!');
@@ -123,16 +125,20 @@ export function Message({
123
  <div className={`flex gap-3 ${isUser && !showSenderInfo ? 'justify-end' : 'justify-start'}`}>
124
  {/* Avatar */}
125
  {showSenderInfo && message.sender ? (
126
- <div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
127
- message.sender.isAI
128
- ? 'bg-gradient-to-br from-purple-500 to-blue-500'
129
- : 'bg-muted'
130
- }`}>
131
  {message.sender.isAI ? (
132
  <Bot className="h-4 w-4 text-white" />
133
  ) : (
134
  <span className="text-sm">
135
- {message.sender.name.split(' ').map(n => n[0]).join('').toUpperCase()}
 
 
 
 
136
  </span>
137
  )}
138
  </div>
@@ -147,39 +153,30 @@ export function Message({
147
  {showSenderInfo && message.sender && (
148
  <div className="flex items-center gap-2 px-1">
149
  <span className="text-xs">{message.sender.name}</span>
150
- {message.sender.isAI && (
151
- <Badge variant="secondary" className="text-xs h-4 px-1">AI</Badge>
152
- )}
153
  </div>
154
  )}
155
 
156
  <div
157
  className={`
158
  rounded-2xl px-4 py-3
159
- ${isUser && !showSenderInfo
160
- ? 'bg-primary text-primary-foreground'
161
- : 'bg-muted'
162
- }
163
  `}
164
  >
165
  <p className="whitespace-pre-wrap">{message.content}</p>
166
  </div>
167
 
168
  {/* References */}
169
- {message.references && message.references.length > 0 && (
170
  <Collapsible open={referencesOpen} onOpenChange={setReferencesOpen}>
171
  <CollapsibleTrigger asChild>
172
  <Button variant="ghost" size="sm" className="gap-1 h-7 text-xs">
173
- {referencesOpen ? (
174
- <ChevronUp className="h-3 w-3" />
175
- ) : (
176
- <ChevronDown className="h-3 w-3" />
177
- )}
178
- {message.references.length} {message.references.length === 1 ? 'reference' : 'references'}
179
  </Button>
180
  </CollapsibleTrigger>
181
  <CollapsibleContent className="space-y-1 mt-1">
182
- {message.references.map((ref, index) => (
183
  <Badge key={index} variant="outline" className="text-xs">
184
  {ref}
185
  </Badge>
@@ -190,12 +187,7 @@ export function Message({
190
 
191
  {/* Message Actions */}
192
  <div className="flex items-center gap-1">
193
- <Button
194
- variant="ghost"
195
- size="sm"
196
- className="h-7 gap-1"
197
- onClick={handleCopy}
198
- >
199
  {copied ? (
200
  <>
201
  <Check className="h-3 w-3" />
@@ -216,20 +208,20 @@ export function Message({
216
  size="sm"
217
  className={`h-7 gap-1 ${feedback === 'helpful' ? 'text-green-600' : ''}`}
218
  onClick={() => handleFeedbackDialogOpen('helpful')}
219
- disabled={!!feedback}
220
  >
221
  <ThumbsUp className="h-3 w-3" />
222
- <span className="text-xs">{feedback === 'helpful' ? 'Helpful (sent)' : 'Helpful'}</span>
223
  </Button>
224
  <Button
225
  variant="ghost"
226
  size="sm"
227
  className={`h-7 gap-1 ${feedback === 'not-helpful' ? 'text-red-600' : ''}`}
228
  onClick={() => handleFeedbackDialogOpen('not-helpful')}
229
- disabled={!!feedback}
230
  >
231
  <ThumbsDown className="h-3 w-3" />
232
- <span className="text-xs">{feedback === 'not-helpful' ? 'Not helpful (sent)' : 'Not helpful'}</span>
233
  </Button>
234
  </>
235
  )}
@@ -253,27 +245,20 @@ export function Message({
253
  : 'Tell us why you found this message not helpful.'}
254
  </DialogDescription>
255
  </DialogHeader>
 
256
  <Textarea
257
  className="h-20"
258
  value={feedbackText}
259
  onChange={(e) => setFeedbackText(e.target.value)}
260
  placeholder="Type your feedback here..."
261
  />
 
262
  <DialogFooter>
263
- <Button
264
- type="button"
265
- variant="outline"
266
- onClick={handleFeedbackDialogClose}
267
- disabled={submitting}
268
- >
269
  Cancel
270
  </Button>
271
- <Button
272
- type="button"
273
- onClick={handleFeedbackDialogSubmit}
274
- disabled={submitting}
275
- >
276
- Submit
277
  </Button>
278
  </DialogFooter>
279
  </DialogContent>
 
1
+ import React, { useState, useMemo } from 'react';
2
  import { Button } from './ui/button';
3
  import {
4
  Copy,
 
14
  import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from './ui/dialog';
15
  import { Textarea } from './ui/textarea';
16
  import type { Message as MessageType, LearningMode } from '../App';
17
+ import { toast } from 'sonner';
18
 
19
  interface MessageProps {
20
  message: MessageType;
21
+ showSenderInfo?: boolean;
22
 
23
+ // ✅ needed for logging feedback
24
  userId: string;
25
+ isLoggedIn: boolean;
26
  learningMode: LearningMode;
27
+ docType: string;
 
 
28
 
29
+ // optional: pass the last user message so feedback can include question
30
+ lastUserText?: string;
31
+ }
32
 
33
+ async function postJson(path: string, payload: any) {
34
+ const res = await fetch(path, {
35
  method: 'POST',
36
  headers: { 'Content-Type': 'application/json' },
37
  body: JSON.stringify(payload),
38
  });
39
  if (!res.ok) {
40
+ const txt = await res.text().catch(() => '');
41
+ throw new Error(`HTTP ${res.status}: ${txt || res.statusText}`);
42
  }
43
+ return res.json();
44
  }
45
 
46
  export function Message({
47
  message,
48
  showSenderInfo = false,
49
  userId,
50
+ isLoggedIn,
51
  learningMode,
52
+ docType,
53
  lastUserText = '',
54
  }: MessageProps) {
55
+ const [feedback, setFeedback] = useState<'helpful' | 'not-helpful' | null>(null);
56
  const [copied, setCopied] = useState(false);
57
  const [referencesOpen, setReferencesOpen] = useState(false);
58
  const [showFeedbackDialog, setShowFeedbackDialog] = useState(false);
59
+ const [feedbackType, setFeedbackType] = useState<'helpful' | 'not-helpful' | null>(null);
60
  const [feedbackText, setFeedbackText] = useState('');
61
  const [submitting, setSubmitting] = useState(false);
62
 
63
  const isUser = message.role === 'user';
64
 
65
+ const refs = useMemo(() => (message.references || []).filter(Boolean), [message.references]);
66
+
67
  const handleCopy = async () => {
68
  await navigator.clipboard.writeText(message.content);
69
  setCopied(true);
 
71
  setTimeout(() => setCopied(false), 2000);
72
  };
73
 
74
+ const handleFeedbackDialogOpen = (type: 'helpful' | 'not-helpful') => {
75
+ if (!isLoggedIn || !userId) {
76
+ toast.error('Please log in first');
 
77
  return;
78
  }
79
  setFeedbackType(type);
 
89
  const handleFeedbackDialogSubmit = async () => {
90
  if (!feedbackType) return;
91
 
92
+ if (!isLoggedIn || !userId) {
93
+ toast.error('Please log in first');
94
+ return;
95
+ }
96
 
97
  try {
98
  setSubmitting(true);
99
 
100
+ await postJson('/api/feedback', {
101
+ user_id: userId,
102
+ rating: feedbackType === 'helpful' ? 'helpful' : 'not_helpful',
103
  assistant_message_id: message.id,
104
+ assistant_text: message.content,
105
  user_text: lastUserText || '',
106
+ comment: feedbackText.trim(),
107
+ refs,
108
  learning_mode: learningMode,
109
  doc_type: docType,
110
  timestamp_ms: Date.now(),
111
+ });
 
 
 
112
 
113
  setFeedback(feedbackType);
114
  toast.success('Thanks for your feedback!');
 
125
  <div className={`flex gap-3 ${isUser && !showSenderInfo ? 'justify-end' : 'justify-start'}`}>
126
  {/* Avatar */}
127
  {showSenderInfo && message.sender ? (
128
+ <div
129
+ className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
130
+ message.sender.isAI ? 'bg-gradient-to-br from-purple-500 to-blue-500' : 'bg-muted'
131
+ }`}
132
+ >
133
  {message.sender.isAI ? (
134
  <Bot className="h-4 w-4 text-white" />
135
  ) : (
136
  <span className="text-sm">
137
+ {message.sender.name
138
+ .split(' ')
139
+ .map((n) => n[0])
140
+ .join('')
141
+ .toUpperCase()}
142
  </span>
143
  )}
144
  </div>
 
153
  {showSenderInfo && message.sender && (
154
  <div className="flex items-center gap-2 px-1">
155
  <span className="text-xs">{message.sender.name}</span>
156
+ {message.sender.isAI && <Badge variant="secondary" className="text-xs h-4 px-1">AI</Badge>}
 
 
157
  </div>
158
  )}
159
 
160
  <div
161
  className={`
162
  rounded-2xl px-4 py-3
163
+ ${isUser && !showSenderInfo ? 'bg-primary text-primary-foreground' : 'bg-muted'}
 
 
 
164
  `}
165
  >
166
  <p className="whitespace-pre-wrap">{message.content}</p>
167
  </div>
168
 
169
  {/* References */}
170
+ {refs.length > 0 && (
171
  <Collapsible open={referencesOpen} onOpenChange={setReferencesOpen}>
172
  <CollapsibleTrigger asChild>
173
  <Button variant="ghost" size="sm" className="gap-1 h-7 text-xs">
174
+ {referencesOpen ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
175
+ {refs.length} {refs.length === 1 ? 'reference' : 'references'}
 
 
 
 
176
  </Button>
177
  </CollapsibleTrigger>
178
  <CollapsibleContent className="space-y-1 mt-1">
179
+ {refs.map((ref, index) => (
180
  <Badge key={index} variant="outline" className="text-xs">
181
  {ref}
182
  </Badge>
 
187
 
188
  {/* Message Actions */}
189
  <div className="flex items-center gap-1">
190
+ <Button variant="ghost" size="sm" className="h-7 gap-1" onClick={handleCopy}>
 
 
 
 
 
191
  {copied ? (
192
  <>
193
  <Check className="h-3 w-3" />
 
208
  size="sm"
209
  className={`h-7 gap-1 ${feedback === 'helpful' ? 'text-green-600' : ''}`}
210
  onClick={() => handleFeedbackDialogOpen('helpful')}
211
+ disabled={!isLoggedIn}
212
  >
213
  <ThumbsUp className="h-3 w-3" />
214
+ <span className="text-xs">Helpful</span>
215
  </Button>
216
  <Button
217
  variant="ghost"
218
  size="sm"
219
  className={`h-7 gap-1 ${feedback === 'not-helpful' ? 'text-red-600' : ''}`}
220
  onClick={() => handleFeedbackDialogOpen('not-helpful')}
221
+ disabled={!isLoggedIn}
222
  >
223
  <ThumbsDown className="h-3 w-3" />
224
+ <span className="text-xs">Not helpful</span>
225
  </Button>
226
  </>
227
  )}
 
245
  : 'Tell us why you found this message not helpful.'}
246
  </DialogDescription>
247
  </DialogHeader>
248
+
249
  <Textarea
250
  className="h-20"
251
  value={feedbackText}
252
  onChange={(e) => setFeedbackText(e.target.value)}
253
  placeholder="Type your feedback here..."
254
  />
255
+
256
  <DialogFooter>
257
+ <Button type="button" variant="outline" onClick={handleFeedbackDialogClose} disabled={submitting}>
 
 
 
 
 
258
  Cancel
259
  </Button>
260
+ <Button type="button" onClick={handleFeedbackDialogSubmit} disabled={submitting}>
261
+ {submitting ? 'Submitting...' : 'Submit'}
 
 
 
 
262
  </Button>
263
  </DialogFooter>
264
  </DialogContent>