Spaces:
Sleeping
Sleeping
Update web/src/components/Message.tsx
Browse files- web/src/components/Message.tsx +57 -97
web/src/components/Message.tsx
CHANGED
|
@@ -24,7 +24,7 @@ import type { Message as MessageType } from "../App";
|
|
| 24 |
import { toast } from "sonner";
|
| 25 |
import clareAvatar from "../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png";
|
| 26 |
|
| 27 |
-
// ✅ NEW:
|
| 28 |
import { apiFeedback } from "../lib/api";
|
| 29 |
|
| 30 |
interface MessageProps {
|
|
@@ -36,10 +36,10 @@ interface MessageProps {
|
|
| 36 |
onNextQuestion?: () => void; // For quiz mode
|
| 37 |
chatMode?: "ask" | "review" | "quiz"; // Current chat mode
|
| 38 |
|
| 39 |
-
// ✅ NEW:
|
| 40 |
-
currentUserId?: string;
|
| 41 |
-
learningMode?: string;
|
| 42 |
docType?: string;
|
|
|
|
| 43 |
}
|
| 44 |
|
| 45 |
// 反馈标签选项
|
|
@@ -61,42 +61,16 @@ const FEEDBACK_TAGS = {
|
|
| 61 |
};
|
| 62 |
|
| 63 |
// ---- Markdown list normalization ----
|
| 64 |
-
// Fixes patterns like:
|
| 65 |
-
// 1.
|
| 66 |
-
//
|
| 67 |
-
// **Title**
|
| 68 |
-
// ... -> 1. **Title**
|
| 69 |
function normalizeMarkdownLists(input: string) {
|
| 70 |
if (!input) return input;
|
| 71 |
|
| 72 |
return (
|
| 73 |
input
|
| 74 |
-
// ordered list: "1.\n\n text" -> "1. text"
|
| 75 |
.replace(/(^|\n)(\s*)(\d+\.)\s*\n+\s+/g, "$1$2$3 ")
|
| 76 |
-
// unordered list: "-\n\n text" -> "- text"
|
| 77 |
.replace(/(^|\n)(\s*)([-*+])\s*\n+\s+/g, "$1$2$3 ")
|
| 78 |
);
|
| 79 |
}
|
| 80 |
|
| 81 |
-
function firstTextish(children: React.ReactNode): string {
|
| 82 |
-
const arr = React.Children.toArray(children);
|
| 83 |
-
for (const ch of arr) {
|
| 84 |
-
if (typeof ch === "string") return ch;
|
| 85 |
-
if (React.isValidElement(ch)) {
|
| 86 |
-
const inner = (ch.props as any)?.children;
|
| 87 |
-
const t = firstTextish(inner);
|
| 88 |
-
if (t) return t;
|
| 89 |
-
}
|
| 90 |
-
}
|
| 91 |
-
return "";
|
| 92 |
-
}
|
| 93 |
-
|
| 94 |
-
function startsWithEmojiBullet(text: string) {
|
| 95 |
-
const t = (text || "").trim();
|
| 96 |
-
// 你图二里的风格:✅ / 🔧 等开头就不要再加圆点
|
| 97 |
-
return /^(✅|☑️|✔️|🟢|🔧|⭐️|✨|🔥|👉|➡️|•)/.test(t);
|
| 98 |
-
}
|
| 99 |
-
|
| 100 |
export function Message({
|
| 101 |
message,
|
| 102 |
showSenderInfo = false,
|
|
@@ -104,9 +78,11 @@ export function Message({
|
|
| 104 |
showNextButton = false,
|
| 105 |
onNextQuestion,
|
| 106 |
chatMode = "ask",
|
|
|
|
|
|
|
| 107 |
currentUserId,
|
| 108 |
-
|
| 109 |
-
|
| 110 |
}: MessageProps) {
|
| 111 |
const [feedback, setFeedback] = useState<"helpful" | "not-helpful" | null>(
|
| 112 |
null
|
|
@@ -121,9 +97,6 @@ export function Message({
|
|
| 121 |
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
| 122 |
const [nextButtonClicked, setNextButtonClicked] = useState(false);
|
| 123 |
|
| 124 |
-
// ✅ NEW: prevent double submit
|
| 125 |
-
const [submittingFeedback, setSubmittingFeedback] = useState(false);
|
| 126 |
-
|
| 127 |
const isUser = message.role === "user";
|
| 128 |
const isWelcomeMessage =
|
| 129 |
isFirstGreeting || message.id === "review-1" || message.id === "quiz-1";
|
|
@@ -163,51 +136,41 @@ export function Message({
|
|
| 163 |
);
|
| 164 |
};
|
| 165 |
|
| 166 |
-
// ✅
|
| 167 |
const handleFeedbackSubmit = async () => {
|
| 168 |
-
if (!feedbackType) return;
|
| 169 |
-
|
| 170 |
if (!currentUserId || !currentUserId.trim()) {
|
| 171 |
toast.error("Missing user_id; cannot submit feedback.");
|
| 172 |
return;
|
| 173 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
|
|
|
|
| 175 |
const rating = feedbackType === "helpful" ? "helpful" : "not_helpful";
|
| 176 |
|
| 177 |
-
// run_id is stored on assistant message (you need to attach it in ChatArea when receiving /api/chat)
|
| 178 |
-
const run_id = (message as any)?.run_id ?? null;
|
| 179 |
-
|
| 180 |
-
// refs: your message.references appears to be string[] currently
|
| 181 |
-
const refs = (message.references as any) ?? [];
|
| 182 |
-
|
| 183 |
try {
|
| 184 |
-
setSubmittingFeedback(true);
|
| 185 |
-
|
| 186 |
await apiFeedback({
|
| 187 |
user_id: currentUserId,
|
| 188 |
rating,
|
| 189 |
-
run_id,
|
| 190 |
-
|
| 191 |
assistant_message_id: message.id,
|
| 192 |
-
assistant_text: message.content
|
| 193 |
-
user_text: "",
|
| 194 |
-
|
| 195 |
-
comment: (feedbackText || "").trim(),
|
| 196 |
tags: selectedTags,
|
| 197 |
-
refs,
|
| 198 |
-
|
| 199 |
-
learning_mode: learningMode,
|
| 200 |
doc_type: docType,
|
| 201 |
timestamp_ms: Date.now(),
|
| 202 |
});
|
| 203 |
|
| 204 |
-
toast.success("
|
| 205 |
handleFeedbackClose();
|
| 206 |
} catch (e: any) {
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
setSubmittingFeedback(false);
|
| 211 |
}
|
| 212 |
};
|
| 213 |
|
|
@@ -218,14 +181,11 @@ export function Message({
|
|
| 218 |
const markdownClass = useMemo(() => {
|
| 219 |
return [
|
| 220 |
"text-base leading-relaxed break-words",
|
| 221 |
-
// 让段落/列表更紧凑,像产品文案
|
| 222 |
" [&_p]:my-2",
|
| 223 |
" [&_p:first-child]:mt-0",
|
| 224 |
" [&_p:last-child]:mb-0",
|
| 225 |
-
// code / pre
|
| 226 |
" [&_code]:px-1 [&_code]:py-0.5 [&_code]:rounded [&_code]:bg-black/5 dark:[&_code]:bg-white/10",
|
| 227 |
" [&_pre]:my-2 [&_pre]:p-3 [&_pre]:rounded-lg [&_pre]:overflow-auto [&_pre]:bg-black/5 dark:[&_pre]:bg-white/10",
|
| 228 |
-
// links
|
| 229 |
" [&_a]:underline [&_a]:underline-offset-2",
|
| 230 |
].join("");
|
| 231 |
}, []);
|
|
@@ -247,39 +207,25 @@ export function Message({
|
|
| 247 |
p: ({ children }) => (
|
| 248 |
<p className="my-2 whitespace-pre-wrap break-words">{children}</p>
|
| 249 |
),
|
| 250 |
-
|
| 251 |
-
/* ---------- Unordered list (· bullet) ---------- */
|
| 252 |
ul: ({ children }) => <ul className="my-3 pl-6 space-y-2">{children}</ul>,
|
| 253 |
-
|
| 254 |
-
/* ---------- Ordered list (1. 2. 3.) ---------- */
|
| 255 |
ol: ({ children }) => <ol className="my-3 space-y-4">{children}</ol>,
|
| 256 |
-
|
| 257 |
li: ({ children, node }) => {
|
| 258 |
const parent = (node as any)?.parent?.tagName;
|
| 259 |
-
|
| 260 |
-
// —— ordered list: number aligned left, text indented
|
| 261 |
if (parent === "ol") {
|
| 262 |
return (
|
| 263 |
<li className="list-none">
|
| 264 |
<div className="flex items-start">
|
| 265 |
-
{/* number column */}
|
| 266 |
<span className="w-6 text-right pr-2 flex-shrink-0 font-medium">
|
| 267 |
{(node as any)?.index + 1}.
|
| 268 |
</span>
|
| 269 |
-
|
| 270 |
-
{/* content column */}
|
| 271 |
<div className="min-w-0">{children}</div>
|
| 272 |
</div>
|
| 273 |
</li>
|
| 274 |
);
|
| 275 |
}
|
| 276 |
-
|
| 277 |
-
/* fallback (unordered handled elsewhere) */
|
| 278 |
return <li>{children}</li>;
|
| 279 |
},
|
| 280 |
-
|
| 281 |
strong: ({ children }) => <strong className="font-semibold">{children}</strong>,
|
| 282 |
-
|
| 283 |
em: ({ children }) => <em className="italic">{children}</em>,
|
| 284 |
}}
|
| 285 |
>
|
|
@@ -347,19 +293,23 @@ export function Message({
|
|
| 347 |
</div>
|
| 348 |
|
| 349 |
{/* Next Question Button for Quiz Mode */}
|
| 350 |
-
{!isUser &&
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
|
| 364 |
{/* References */}
|
| 365 |
{message.references && message.references.length > 0 && (
|
|
@@ -367,7 +317,8 @@ export function Message({
|
|
| 367 |
<CollapsibleTrigger asChild>
|
| 368 |
<Button variant="ghost" size="sm" className="gap-1 h-7 text-xs">
|
| 369 |
{referencesOpen ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
|
| 370 |
-
{message.references.length}
|
|
|
|
| 371 |
</Button>
|
| 372 |
</CollapsibleTrigger>
|
| 373 |
<CollapsibleContent className="space-y-1 mt-1">
|
|
@@ -387,7 +338,9 @@ export function Message({
|
|
| 387 |
variant="ghost"
|
| 388 |
size="icon"
|
| 389 |
className={`h-7 w-7 rounded-md transition-all hover:bg-accent hover:scale-110 active:scale-95 ${
|
| 390 |
-
copied
|
|
|
|
|
|
|
| 391 |
}`}
|
| 392 |
onClick={handleCopy}
|
| 393 |
title="Copy"
|
|
@@ -432,8 +385,15 @@ export function Message({
|
|
| 432 |
{!isUser && showFeedbackArea && feedbackType && (
|
| 433 |
<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">
|
| 434 |
<div className="flex items-start justify-between mb-4">
|
| 435 |
-
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
| 436 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 437 |
<X className="h-4 w-4" />
|
| 438 |
</Button>
|
| 439 |
</div>
|
|
@@ -460,15 +420,15 @@ export function Message({
|
|
| 460 |
/>
|
| 461 |
|
| 462 |
<div className="flex justify-end gap-2">
|
| 463 |
-
<Button variant="outline" size="sm" onClick={handleFeedbackClose}
|
| 464 |
Cancel
|
| 465 |
</Button>
|
| 466 |
<Button
|
| 467 |
size="sm"
|
| 468 |
onClick={handleFeedbackSubmit}
|
| 469 |
-
disabled={
|
| 470 |
>
|
| 471 |
-
|
| 472 |
</Button>
|
| 473 |
</div>
|
| 474 |
</div>
|
|
|
|
| 24 |
import { toast } from "sonner";
|
| 25 |
import clareAvatar from "../assets/dfe44dab3ad8cd93953eac4a3e68bd1a5f999653.png";
|
| 26 |
|
| 27 |
+
// ✅ NEW: real API call
|
| 28 |
import { apiFeedback } from "../lib/api";
|
| 29 |
|
| 30 |
interface MessageProps {
|
|
|
|
| 36 |
onNextQuestion?: () => void; // For quiz mode
|
| 37 |
chatMode?: "ask" | "review" | "quiz"; // Current chat mode
|
| 38 |
|
| 39 |
+
// ✅ NEW: for feedback submission
|
| 40 |
+
currentUserId?: string;
|
|
|
|
| 41 |
docType?: string;
|
| 42 |
+
learningMode?: string;
|
| 43 |
}
|
| 44 |
|
| 45 |
// 反馈标签选项
|
|
|
|
| 61 |
};
|
| 62 |
|
| 63 |
// ---- Markdown list normalization ----
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
function normalizeMarkdownLists(input: string) {
|
| 65 |
if (!input) return input;
|
| 66 |
|
| 67 |
return (
|
| 68 |
input
|
|
|
|
| 69 |
.replace(/(^|\n)(\s*)(\d+\.)\s*\n+\s+/g, "$1$2$3 ")
|
|
|
|
| 70 |
.replace(/(^|\n)(\s*)([-*+])\s*\n+\s+/g, "$1$2$3 ")
|
| 71 |
);
|
| 72 |
}
|
| 73 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
export function Message({
|
| 75 |
message,
|
| 76 |
showSenderInfo = false,
|
|
|
|
| 78 |
showNextButton = false,
|
| 79 |
onNextQuestion,
|
| 80 |
chatMode = "ask",
|
| 81 |
+
|
| 82 |
+
// ✅ NEW
|
| 83 |
currentUserId,
|
| 84 |
+
docType = "Syllabus",
|
| 85 |
+
learningMode = "general",
|
| 86 |
}: MessageProps) {
|
| 87 |
const [feedback, setFeedback] = useState<"helpful" | "not-helpful" | null>(
|
| 88 |
null
|
|
|
|
| 97 |
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
| 98 |
const [nextButtonClicked, setNextButtonClicked] = useState(false);
|
| 99 |
|
|
|
|
|
|
|
|
|
|
| 100 |
const isUser = message.role === "user";
|
| 101 |
const isWelcomeMessage =
|
| 102 |
isFirstGreeting || message.id === "review-1" || message.id === "quiz-1";
|
|
|
|
| 136 |
);
|
| 137 |
};
|
| 138 |
|
| 139 |
+
// ✅ REAL submit to backend -> LangSmith dataset
|
| 140 |
const handleFeedbackSubmit = async () => {
|
|
|
|
|
|
|
| 141 |
if (!currentUserId || !currentUserId.trim()) {
|
| 142 |
toast.error("Missing user_id; cannot submit feedback.");
|
| 143 |
return;
|
| 144 |
}
|
| 145 |
+
if (!feedbackType) {
|
| 146 |
+
toast.error("Please select Helpful / Not helpful.");
|
| 147 |
+
return;
|
| 148 |
+
}
|
| 149 |
|
| 150 |
+
// UI uses "not-helpful" (dash), backend expects "not_helpful" (underscore)
|
| 151 |
const rating = feedbackType === "helpful" ? "helpful" : "not_helpful";
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
try {
|
|
|
|
|
|
|
| 154 |
await apiFeedback({
|
| 155 |
user_id: currentUserId,
|
| 156 |
rating,
|
|
|
|
|
|
|
| 157 |
assistant_message_id: message.id,
|
| 158 |
+
assistant_text: message.content,
|
| 159 |
+
user_text: "", // optional; you can wire last user msg later if you want
|
| 160 |
+
comment: feedbackText || "",
|
|
|
|
| 161 |
tags: selectedTags,
|
| 162 |
+
refs: message.references ?? [],
|
| 163 |
+
learning_mode: chatMode === "ask" ? learningMode : chatMode,
|
|
|
|
| 164 |
doc_type: docType,
|
| 165 |
timestamp_ms: Date.now(),
|
| 166 |
});
|
| 167 |
|
| 168 |
+
toast.success("Thanks — feedback submitted.");
|
| 169 |
handleFeedbackClose();
|
| 170 |
} catch (e: any) {
|
| 171 |
+
// eslint-disable-next-line no-console
|
| 172 |
+
console.error("feedback submit failed:", e);
|
| 173 |
+
toast.error(e?.message || "Failed to submit feedback.");
|
|
|
|
| 174 |
}
|
| 175 |
};
|
| 176 |
|
|
|
|
| 181 |
const markdownClass = useMemo(() => {
|
| 182 |
return [
|
| 183 |
"text-base leading-relaxed break-words",
|
|
|
|
| 184 |
" [&_p]:my-2",
|
| 185 |
" [&_p:first-child]:mt-0",
|
| 186 |
" [&_p:last-child]:mb-0",
|
|
|
|
| 187 |
" [&_code]:px-1 [&_code]:py-0.5 [&_code]:rounded [&_code]:bg-black/5 dark:[&_code]:bg-white/10",
|
| 188 |
" [&_pre]:my-2 [&_pre]:p-3 [&_pre]:rounded-lg [&_pre]:overflow-auto [&_pre]:bg-black/5 dark:[&_pre]:bg-white/10",
|
|
|
|
| 189 |
" [&_a]:underline [&_a]:underline-offset-2",
|
| 190 |
].join("");
|
| 191 |
}, []);
|
|
|
|
| 207 |
p: ({ children }) => (
|
| 208 |
<p className="my-2 whitespace-pre-wrap break-words">{children}</p>
|
| 209 |
),
|
|
|
|
|
|
|
| 210 |
ul: ({ children }) => <ul className="my-3 pl-6 space-y-2">{children}</ul>,
|
|
|
|
|
|
|
| 211 |
ol: ({ children }) => <ol className="my-3 space-y-4">{children}</ol>,
|
|
|
|
| 212 |
li: ({ children, node }) => {
|
| 213 |
const parent = (node as any)?.parent?.tagName;
|
|
|
|
|
|
|
| 214 |
if (parent === "ol") {
|
| 215 |
return (
|
| 216 |
<li className="list-none">
|
| 217 |
<div className="flex items-start">
|
|
|
|
| 218 |
<span className="w-6 text-right pr-2 flex-shrink-0 font-medium">
|
| 219 |
{(node as any)?.index + 1}.
|
| 220 |
</span>
|
|
|
|
|
|
|
| 221 |
<div className="min-w-0">{children}</div>
|
| 222 |
</div>
|
| 223 |
</li>
|
| 224 |
);
|
| 225 |
}
|
|
|
|
|
|
|
| 226 |
return <li>{children}</li>;
|
| 227 |
},
|
|
|
|
| 228 |
strong: ({ children }) => <strong className="font-semibold">{children}</strong>,
|
|
|
|
| 229 |
em: ({ children }) => <em className="italic">{children}</em>,
|
| 230 |
}}
|
| 231 |
>
|
|
|
|
| 293 |
</div>
|
| 294 |
|
| 295 |
{/* Next Question Button for Quiz Mode */}
|
| 296 |
+
{!isUser &&
|
| 297 |
+
showNextButton &&
|
| 298 |
+
!nextButtonClicked &&
|
| 299 |
+
chatMode === "quiz" &&
|
| 300 |
+
onNextQuestion && (
|
| 301 |
+
<div className="mt-2">
|
| 302 |
+
<Button
|
| 303 |
+
onClick={() => {
|
| 304 |
+
setNextButtonClicked(true);
|
| 305 |
+
onNextQuestion();
|
| 306 |
+
}}
|
| 307 |
+
className="bg-primary hover:bg-primary/90"
|
| 308 |
+
>
|
| 309 |
+
Next Question
|
| 310 |
+
</Button>
|
| 311 |
+
</div>
|
| 312 |
+
)}
|
| 313 |
|
| 314 |
{/* References */}
|
| 315 |
{message.references && message.references.length > 0 && (
|
|
|
|
| 317 |
<CollapsibleTrigger asChild>
|
| 318 |
<Button variant="ghost" size="sm" className="gap-1 h-7 text-xs">
|
| 319 |
{referencesOpen ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
|
| 320 |
+
{message.references.length}{" "}
|
| 321 |
+
{message.references.length === 1 ? "reference" : "references"}
|
| 322 |
</Button>
|
| 323 |
</CollapsibleTrigger>
|
| 324 |
<CollapsibleContent className="space-y-1 mt-1">
|
|
|
|
| 338 |
variant="ghost"
|
| 339 |
size="icon"
|
| 340 |
className={`h-7 w-7 rounded-md transition-all hover:bg-accent hover:scale-110 active:scale-95 ${
|
| 341 |
+
copied
|
| 342 |
+
? "bg-green-100 text-green-600 dark:bg-green-900/20 dark:text-green-400"
|
| 343 |
+
: ""
|
| 344 |
}`}
|
| 345 |
onClick={handleCopy}
|
| 346 |
title="Copy"
|
|
|
|
| 385 |
{!isUser && showFeedbackArea && feedbackType && (
|
| 386 |
<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">
|
| 387 |
<div className="flex items-start justify-between mb-4">
|
| 388 |
+
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
| 389 |
+
Tell us more:
|
| 390 |
+
</h4>
|
| 391 |
+
<Button
|
| 392 |
+
variant="ghost"
|
| 393 |
+
size="sm"
|
| 394 |
+
className="h-6 w-6 p-0"
|
| 395 |
+
onClick={handleFeedbackClose}
|
| 396 |
+
>
|
| 397 |
<X className="h-4 w-4" />
|
| 398 |
</Button>
|
| 399 |
</div>
|
|
|
|
| 420 |
/>
|
| 421 |
|
| 422 |
<div className="flex justify-end gap-2">
|
| 423 |
+
<Button variant="outline" size="sm" onClick={handleFeedbackClose}>
|
| 424 |
Cancel
|
| 425 |
</Button>
|
| 426 |
<Button
|
| 427 |
size="sm"
|
| 428 |
onClick={handleFeedbackSubmit}
|
| 429 |
+
disabled={selectedTags.length === 0 && !feedbackText.trim()}
|
| 430 |
>
|
| 431 |
+
Submit
|
| 432 |
</Button>
|
| 433 |
</div>
|
| 434 |
</div>
|