Spaces:
Sleeping
Sleeping
Update web/src/components/Message.tsx
Browse files- web/src/components/Message.tsx +20 -45
web/src/components/Message.tsx
CHANGED
|
@@ -4,15 +4,7 @@ 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";
|
|
@@ -23,14 +15,13 @@ import clareAvatar from "../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png"
|
|
| 23 |
interface MessageProps {
|
| 24 |
key?: React.Key;
|
| 25 |
message: MessageType;
|
| 26 |
-
showSenderInfo?: boolean;
|
| 27 |
-
isFirstGreeting?: boolean;
|
| 28 |
-
showNextButton?: boolean;
|
| 29 |
-
onNextQuestion?: () => void;
|
| 30 |
-
chatMode?: "ask" | "review" | "quiz";
|
| 31 |
}
|
| 32 |
|
| 33 |
-
// 反馈标签选项
|
| 34 |
const FEEDBACK_TAGS = {
|
| 35 |
"not-helpful": [
|
| 36 |
"Code was incorrect",
|
|
@@ -42,6 +33,14 @@ const FEEDBACK_TAGS = {
|
|
| 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,
|
|
@@ -89,7 +88,6 @@ export function Message({
|
|
| 89 |
setFeedbackType(null);
|
| 90 |
setFeedbackText("");
|
| 91 |
setSelectedTags([]);
|
| 92 |
-
// 不重置 feedback,保持点赞/点踩的状态
|
| 93 |
};
|
| 94 |
|
| 95 |
const handleTagToggle = (tag: string) => {
|
|
@@ -103,25 +101,16 @@ export function Message({
|
|
| 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 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 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 = () => {
|
|
@@ -134,7 +123,6 @@ export function Message({
|
|
| 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}
|
|
@@ -165,7 +153,6 @@ export function Message({
|
|
| 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}
|
|
@@ -176,7 +163,6 @@ export function Message({
|
|
| 176 |
{children}
|
| 177 |
</pre>
|
| 178 |
),
|
| 179 |
-
// 你如果不想自动生成链接样式,可以在这里控制
|
| 180 |
a: ({ children, ...props }) => (
|
| 181 |
<a className="underline underline-offset-2" target="_blank" rel="noreferrer" {...props}>
|
| 182 |
{children}
|
|
@@ -184,7 +170,7 @@ export function Message({
|
|
| 184 |
),
|
| 185 |
}}
|
| 186 |
>
|
| 187 |
-
{message.content}
|
| 188 |
</ReactMarkdown>
|
| 189 |
);
|
| 190 |
};
|
|
@@ -219,7 +205,6 @@ export function Message({
|
|
| 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>
|
|
@@ -227,17 +212,10 @@ export function Message({
|
|
| 227 |
</div>
|
| 228 |
)}
|
| 229 |
|
| 230 |
-
{
|
| 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
|
|
@@ -252,7 +230,6 @@ export function Message({
|
|
| 252 |
</div>
|
| 253 |
)}
|
| 254 |
|
| 255 |
-
{/* References */}
|
| 256 |
{message.references && message.references.length > 0 && (
|
| 257 |
<Collapsible open={referencesOpen} onOpenChange={setReferencesOpen}>
|
| 258 |
<CollapsibleTrigger asChild>
|
|
@@ -271,7 +248,6 @@ export function Message({
|
|
| 271 |
</Collapsible>
|
| 272 |
)}
|
| 273 |
|
| 274 |
-
{/* Message Actions */}
|
| 275 |
{shouldShowActions && (
|
| 276 |
<div className="flex items-center gap-1">
|
| 277 |
<Button
|
|
@@ -315,7 +291,6 @@ export function Message({
|
|
| 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">
|
|
|
|
| 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";
|
|
|
|
| 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",
|
|
|
|
| 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({
|
| 45 |
message,
|
| 46 |
showSenderInfo = false,
|
|
|
|
| 88 |
setFeedbackType(null);
|
| 89 |
setFeedbackText("");
|
| 90 |
setSelectedTags([]);
|
|
|
|
| 91 |
};
|
| 92 |
|
| 93 |
const handleTagToggle = (tag: string) => {
|
|
|
|
| 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 = () => {
|
|
|
|
| 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}
|
|
|
|
| 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}
|
|
|
|
| 163 |
{children}
|
| 164 |
</pre>
|
| 165 |
),
|
|
|
|
| 166 |
a: ({ children, ...props }) => (
|
| 167 |
<a className="underline underline-offset-2" target="_blank" rel="noreferrer" {...props}>
|
| 168 |
{children}
|
|
|
|
| 170 |
),
|
| 171 |
}}
|
| 172 |
>
|
| 173 |
+
{unescapeMarkdown(message.content)}
|
| 174 |
</ReactMarkdown>
|
| 175 |
);
|
| 176 |
};
|
|
|
|
| 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>
|
|
|
|
| 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
|
|
|
|
| 230 |
</div>
|
| 231 |
)}
|
| 232 |
|
|
|
|
| 233 |
{message.references && message.references.length > 0 && (
|
| 234 |
<Collapsible open={referencesOpen} onOpenChange={setReferencesOpen}>
|
| 235 |
<CollapsibleTrigger asChild>
|
|
|
|
| 248 |
</Collapsible>
|
| 249 |
)}
|
| 250 |
|
|
|
|
| 251 |
{shouldShowActions && (
|
| 252 |
<div className="flex items-center gap-1">
|
| 253 |
<Button
|
|
|
|
| 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">
|