Spaces:
Sleeping
Sleeping
Update web/src/components/Message.tsx
Browse files- web/src/components/Message.tsx +143 -92
web/src/components/Message.tsx
CHANGED
|
@@ -3,6 +3,7 @@ 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 |
import pdfIcon from "../assets/file-icons/pdf.png";
|
| 7 |
import pptIcon from "../assets/file-icons/ppt.png";
|
| 8 |
import otherIcon from "../assets/file-icons/other_format.png";
|
|
@@ -33,17 +34,19 @@ import { apiFeedback } from "../lib/api";
|
|
| 33 |
interface MessageProps {
|
| 34 |
key?: React.Key;
|
| 35 |
message: MessageType;
|
| 36 |
-
showSenderInfo?: boolean;
|
| 37 |
-
isFirstGreeting?: boolean;
|
| 38 |
-
showNextButton?: boolean;
|
| 39 |
-
onNextQuestion?: () => void;
|
| 40 |
-
chatMode?: "ask" | "review" | "quiz";
|
| 41 |
|
|
|
|
| 42 |
currentUserId?: string;
|
| 43 |
docType?: string;
|
| 44 |
learningMode?: string;
|
| 45 |
}
|
| 46 |
|
|
|
|
| 47 |
const FEEDBACK_TAGS = {
|
| 48 |
"not-helpful": [
|
| 49 |
"Code was incorrect",
|
|
@@ -61,6 +64,7 @@ const FEEDBACK_TAGS = {
|
|
| 61 |
],
|
| 62 |
};
|
| 63 |
|
|
|
|
| 64 |
function normalizeMarkdownLists(input: string) {
|
| 65 |
if (!input) return input;
|
| 66 |
|
|
@@ -77,18 +81,23 @@ export function Message({
|
|
| 77 |
onNextQuestion,
|
| 78 |
chatMode = "ask",
|
| 79 |
|
|
|
|
| 80 |
currentUserId,
|
| 81 |
docType = "Syllabus",
|
| 82 |
learningMode = "general",
|
| 83 |
}: MessageProps) {
|
| 84 |
-
const [feedback, setFeedback] = useState<"helpful" | "not-helpful" | null>(
|
|
|
|
|
|
|
| 85 |
const [copied, setCopied] = useState(false);
|
| 86 |
|
| 87 |
-
// ✅ References UI state
|
| 88 |
const [referencesOpen, setReferencesOpen] = useState(false);
|
| 89 |
|
| 90 |
const [showFeedbackArea, setShowFeedbackArea] = useState(false);
|
| 91 |
-
const [feedbackType, setFeedbackType] = useState<
|
|
|
|
|
|
|
| 92 |
const [feedbackText, setFeedbackText] = useState("");
|
| 93 |
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
| 94 |
const [nextButtonClicked, setNextButtonClicked] = useState(false);
|
|
@@ -98,6 +107,11 @@ export function Message({
|
|
| 98 |
isFirstGreeting || message.id === "review-1" || message.id === "quiz-1";
|
| 99 |
const shouldShowActions = isUser ? true : !isWelcomeMessage;
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
const handleCopy = async () => {
|
| 102 |
await navigator.clipboard.writeText(message.content);
|
| 103 |
setCopied(true);
|
|
@@ -132,6 +146,7 @@ export function Message({
|
|
| 132 |
);
|
| 133 |
};
|
| 134 |
|
|
|
|
| 135 |
const handleFeedbackSubmit = async () => {
|
| 136 |
if (!currentUserId || !currentUserId.trim()) {
|
| 137 |
toast.error("Missing user_id; cannot submit feedback.");
|
|
@@ -142,6 +157,7 @@ export function Message({
|
|
| 142 |
return;
|
| 143 |
}
|
| 144 |
|
|
|
|
| 145 |
const rating = feedbackType === "helpful" ? "helpful" : "not_helpful";
|
| 146 |
|
| 147 |
try {
|
|
@@ -150,7 +166,7 @@ export function Message({
|
|
| 150 |
rating,
|
| 151 |
assistant_message_id: message.id,
|
| 152 |
assistant_text: message.content,
|
| 153 |
-
user_text: "",
|
| 154 |
comment: feedbackText || "",
|
| 155 |
tags: selectedTags,
|
| 156 |
refs: message.references ?? [],
|
|
@@ -186,7 +202,11 @@ export function Message({
|
|
| 186 |
|
| 187 |
const renderBubbleContent = () => {
|
| 188 |
if (isUser) {
|
| 189 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
| 190 |
}
|
| 191 |
|
| 192 |
return (
|
|
@@ -194,8 +214,12 @@ export function Message({
|
|
| 194 |
remarkPlugins={[remarkGfm]}
|
| 195 |
className={markdownClass}
|
| 196 |
components={{
|
| 197 |
-
p: ({ children }) =>
|
| 198 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
ol: ({ children }) => <ol className="my-3 space-y-4">{children}</ol>,
|
| 200 |
li: ({ children, node }) => {
|
| 201 |
const parent = (node as any)?.parent?.tagName;
|
|
@@ -213,7 +237,9 @@ export function Message({
|
|
| 213 |
}
|
| 214 |
return <li>{children}</li>;
|
| 215 |
},
|
| 216 |
-
strong: ({ children }) =>
|
|
|
|
|
|
|
| 217 |
em: ({ children }) => <em className="italic">{children}</em>,
|
| 218 |
}}
|
| 219 |
>
|
|
@@ -222,17 +248,12 @@ export function Message({
|
|
| 222 |
);
|
| 223 |
};
|
| 224 |
|
| 225 |
-
const refsArr = (message.references || []).filter(Boolean);
|
| 226 |
-
const refsCount = refsArr.length;
|
| 227 |
-
|
| 228 |
const attachments = (message as any).attachments as
|
| 229 |
| Array<{ name: string; kind: string; size: number; fileType?: string }>
|
| 230 |
| undefined;
|
| 231 |
|
| 232 |
const hasAttachments = !!(attachments && attachments.length);
|
| 233 |
|
| 234 |
-
const showReferenceWidget = !isUser; // ✅ assistant always shows the ref widget
|
| 235 |
-
|
| 236 |
return (
|
| 237 |
<div
|
| 238 |
className={`flex gap-2 ${
|
|
@@ -243,7 +264,11 @@ export function Message({
|
|
| 243 |
{showSenderInfo && message.sender ? (
|
| 244 |
<div className="w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 overflow-hidden bg-white">
|
| 245 |
{message.sender.isAI ? (
|
| 246 |
-
<img
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
) : (
|
| 248 |
<img
|
| 249 |
src={
|
|
@@ -259,7 +284,11 @@ export function Message({
|
|
| 259 |
</div>
|
| 260 |
) : !isUser ? (
|
| 261 |
<div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
|
| 262 |
-
<img
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
</div>
|
| 264 |
) : null}
|
| 265 |
|
|
@@ -284,17 +313,20 @@ export function Message({
|
|
| 284 |
{/* Bubble */}
|
| 285 |
<div
|
| 286 |
className={`
|
| 287 |
-
relative
|
| 288 |
rounded-2xl px-4 py-3
|
| 289 |
-
${!isUser ? "pb-10" : ""}
|
| 290 |
${isUser && !showSenderInfo ? "bg-primary text-primary-foreground" : "bg-muted"}
|
| 291 |
`}
|
| 292 |
>
|
| 293 |
-
{/* Attachments */}
|
| 294 |
{hasAttachments && (
|
| 295 |
<div className="mb-3 flex flex-wrap gap-2">
|
| 296 |
{attachments!.map((a, idx) => {
|
| 297 |
-
const icon =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 298 |
|
| 299 |
const label =
|
| 300 |
a.kind === "pdf"
|
|
@@ -328,8 +360,12 @@ export function Message({
|
|
| 328 |
draggable={false}
|
| 329 |
/>
|
| 330 |
<div className="min-w-0 leading-tight">
|
| 331 |
-
<div className="text-sm font-medium truncate max-w-[320px]">
|
| 332 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
</div>
|
| 334 |
</div>
|
| 335 |
);
|
|
@@ -338,74 +374,76 @@ export function Message({
|
|
| 338 |
)}
|
| 339 |
|
| 340 |
{renderBubbleContent()}
|
|
|
|
| 341 |
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
) : (
|
| 364 |
-
<ChevronDown className="h-3 w-3" />
|
| 365 |
-
)}
|
| 366 |
-
<span className="font-medium">References</span>
|
| 367 |
-
<span className="opacity-70">({refsCount})</span>
|
| 368 |
-
</button>
|
| 369 |
-
</CollapsibleTrigger>
|
| 370 |
-
|
| 371 |
-
<CollapsibleContent className="mt-2">
|
| 372 |
-
{refsCount > 0 ? (
|
| 373 |
-
<div className="space-y-1">
|
| 374 |
-
{refsArr.map((ref, index) => (
|
| 375 |
-
<div
|
| 376 |
-
key={index}
|
| 377 |
-
className="rounded-md border border-border bg-background/60 dark:bg-background/20 px-2 py-1 text-xs"
|
| 378 |
-
>
|
| 379 |
-
{ref}
|
| 380 |
-
</div>
|
| 381 |
-
))}
|
| 382 |
-
</div>
|
| 383 |
) : (
|
| 384 |
-
<
|
| 385 |
-
No references returned.
|
| 386 |
-
</div>
|
| 387 |
)}
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 406 |
</div>
|
| 407 |
)}
|
| 408 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
{/* Message Actions */}
|
| 410 |
{shouldShowActions && (
|
| 411 |
<div className="flex items-center gap-1">
|
|
@@ -413,12 +451,18 @@ export function Message({
|
|
| 413 |
variant="ghost"
|
| 414 |
size="icon"
|
| 415 |
className={`h-7 w-7 rounded-md transition-all hover:bg-accent hover:scale-110 active:scale-95 ${
|
| 416 |
-
copied
|
|
|
|
|
|
|
| 417 |
}`}
|
| 418 |
onClick={handleCopy}
|
| 419 |
title="Copy"
|
| 420 |
>
|
| 421 |
-
{copied ?
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
</Button>
|
| 423 |
|
| 424 |
{!isUser && (
|
|
@@ -458,8 +502,15 @@ export function Message({
|
|
| 458 |
{!isUser && showFeedbackArea && feedbackType && (
|
| 459 |
<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">
|
| 460 |
<div className="flex items-start justify-between mb-4">
|
| 461 |
-
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
| 462 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 463 |
<X className="h-4 w-4" />
|
| 464 |
</Button>
|
| 465 |
</div>
|
|
|
|
| 3 |
import { Button } from "./ui/button";
|
| 4 |
import ReactMarkdown from "react-markdown";
|
| 5 |
import remarkGfm from "remark-gfm";
|
| 6 |
+
|
| 7 |
import pdfIcon from "../assets/file-icons/pdf.png";
|
| 8 |
import pptIcon from "../assets/file-icons/ppt.png";
|
| 9 |
import otherIcon from "../assets/file-icons/other_format.png";
|
|
|
|
| 34 |
interface MessageProps {
|
| 35 |
key?: React.Key;
|
| 36 |
message: MessageType;
|
| 37 |
+
showSenderInfo?: boolean; // For group chat mode
|
| 38 |
+
isFirstGreeting?: boolean; // Indicates if this is the first greeting message
|
| 39 |
+
showNextButton?: boolean; // For quiz mode
|
| 40 |
+
onNextQuestion?: () => void; // For quiz mode
|
| 41 |
+
chatMode?: "ask" | "review" | "quiz"; // Current chat mode
|
| 42 |
|
| 43 |
+
// ✅ NEW: for feedback submission
|
| 44 |
currentUserId?: string;
|
| 45 |
docType?: string;
|
| 46 |
learningMode?: string;
|
| 47 |
}
|
| 48 |
|
| 49 |
+
// 反馈标签选项
|
| 50 |
const FEEDBACK_TAGS = {
|
| 51 |
"not-helpful": [
|
| 52 |
"Code was incorrect",
|
|
|
|
| 64 |
],
|
| 65 |
};
|
| 66 |
|
| 67 |
+
// ---- Markdown list normalization ----
|
| 68 |
function normalizeMarkdownLists(input: string) {
|
| 69 |
if (!input) return input;
|
| 70 |
|
|
|
|
| 81 |
onNextQuestion,
|
| 82 |
chatMode = "ask",
|
| 83 |
|
| 84 |
+
// ✅ NEW
|
| 85 |
currentUserId,
|
| 86 |
docType = "Syllabus",
|
| 87 |
learningMode = "general",
|
| 88 |
}: MessageProps) {
|
| 89 |
+
const [feedback, setFeedback] = useState<"helpful" | "not-helpful" | null>(
|
| 90 |
+
null
|
| 91 |
+
);
|
| 92 |
const [copied, setCopied] = useState(false);
|
| 93 |
|
| 94 |
+
// ✅ References UI state
|
| 95 |
const [referencesOpen, setReferencesOpen] = useState(false);
|
| 96 |
|
| 97 |
const [showFeedbackArea, setShowFeedbackArea] = useState(false);
|
| 98 |
+
const [feedbackType, setFeedbackType] = useState<
|
| 99 |
+
"helpful" | "not-helpful" | null
|
| 100 |
+
>(null);
|
| 101 |
const [feedbackText, setFeedbackText] = useState("");
|
| 102 |
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
| 103 |
const [nextButtonClicked, setNextButtonClicked] = useState(false);
|
|
|
|
| 107 |
isFirstGreeting || message.id === "review-1" || message.id === "quiz-1";
|
| 108 |
const shouldShowActions = isUser ? true : !isWelcomeMessage;
|
| 109 |
|
| 110 |
+
// ✅ show reference pill for assistant messages (even if 0)
|
| 111 |
+
const showReferencesPill = !isUser; // assistant only
|
| 112 |
+
const refs = Array.isArray(message.references) ? message.references : [];
|
| 113 |
+
const hasRefs = refs.length > 0;
|
| 114 |
+
|
| 115 |
const handleCopy = async () => {
|
| 116 |
await navigator.clipboard.writeText(message.content);
|
| 117 |
setCopied(true);
|
|
|
|
| 146 |
);
|
| 147 |
};
|
| 148 |
|
| 149 |
+
// ✅ REAL submit to backend -> LangSmith dataset
|
| 150 |
const handleFeedbackSubmit = async () => {
|
| 151 |
if (!currentUserId || !currentUserId.trim()) {
|
| 152 |
toast.error("Missing user_id; cannot submit feedback.");
|
|
|
|
| 157 |
return;
|
| 158 |
}
|
| 159 |
|
| 160 |
+
// UI uses "not-helpful" (dash), backend expects "not_helpful" (underscore)
|
| 161 |
const rating = feedbackType === "helpful" ? "helpful" : "not_helpful";
|
| 162 |
|
| 163 |
try {
|
|
|
|
| 166 |
rating,
|
| 167 |
assistant_message_id: message.id,
|
| 168 |
assistant_text: message.content,
|
| 169 |
+
user_text: "", // optional
|
| 170 |
comment: feedbackText || "",
|
| 171 |
tags: selectedTags,
|
| 172 |
refs: message.references ?? [],
|
|
|
|
| 202 |
|
| 203 |
const renderBubbleContent = () => {
|
| 204 |
if (isUser) {
|
| 205 |
+
return (
|
| 206 |
+
<p className="whitespace-pre-wrap text-base break-words">
|
| 207 |
+
{message.content}
|
| 208 |
+
</p>
|
| 209 |
+
);
|
| 210 |
}
|
| 211 |
|
| 212 |
return (
|
|
|
|
| 214 |
remarkPlugins={[remarkGfm]}
|
| 215 |
className={markdownClass}
|
| 216 |
components={{
|
| 217 |
+
p: ({ children }) => (
|
| 218 |
+
<p className="my-2 whitespace-pre-wrap break-words">{children}</p>
|
| 219 |
+
),
|
| 220 |
+
ul: ({ children }) => (
|
| 221 |
+
<ul className="my-3 pl-6 space-y-2">{children}</ul>
|
| 222 |
+
),
|
| 223 |
ol: ({ children }) => <ol className="my-3 space-y-4">{children}</ol>,
|
| 224 |
li: ({ children, node }) => {
|
| 225 |
const parent = (node as any)?.parent?.tagName;
|
|
|
|
| 237 |
}
|
| 238 |
return <li>{children}</li>;
|
| 239 |
},
|
| 240 |
+
strong: ({ children }) => (
|
| 241 |
+
<strong className="font-semibold">{children}</strong>
|
| 242 |
+
),
|
| 243 |
em: ({ children }) => <em className="italic">{children}</em>,
|
| 244 |
}}
|
| 245 |
>
|
|
|
|
| 248 |
);
|
| 249 |
};
|
| 250 |
|
|
|
|
|
|
|
|
|
|
| 251 |
const attachments = (message as any).attachments as
|
| 252 |
| Array<{ name: string; kind: string; size: number; fileType?: string }>
|
| 253 |
| undefined;
|
| 254 |
|
| 255 |
const hasAttachments = !!(attachments && attachments.length);
|
| 256 |
|
|
|
|
|
|
|
| 257 |
return (
|
| 258 |
<div
|
| 259 |
className={`flex gap-2 ${
|
|
|
|
| 264 |
{showSenderInfo && message.sender ? (
|
| 265 |
<div className="w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 overflow-hidden bg-white">
|
| 266 |
{message.sender.isAI ? (
|
| 267 |
+
<img
|
| 268 |
+
src={clareAvatar}
|
| 269 |
+
alt="Clare"
|
| 270 |
+
className="w-full h-full object-cover"
|
| 271 |
+
/>
|
| 272 |
) : (
|
| 273 |
<img
|
| 274 |
src={
|
|
|
|
| 284 |
</div>
|
| 285 |
) : !isUser ? (
|
| 286 |
<div className="w-10 h-10 rounded-full overflow-hidden bg-white flex items-center justify-center flex-shrink-0">
|
| 287 |
+
<img
|
| 288 |
+
src={clareAvatar}
|
| 289 |
+
alt="Clare"
|
| 290 |
+
className="w-full h-full object-cover"
|
| 291 |
+
/>
|
| 292 |
</div>
|
| 293 |
) : null}
|
| 294 |
|
|
|
|
| 313 |
{/* Bubble */}
|
| 314 |
<div
|
| 315 |
className={`
|
|
|
|
| 316 |
rounded-2xl px-4 py-3
|
|
|
|
| 317 |
${isUser && !showSenderInfo ? "bg-primary text-primary-foreground" : "bg-muted"}
|
| 318 |
`}
|
| 319 |
>
|
| 320 |
+
{/* ✅ Attachments shown “with” the message (neutral card style) */}
|
| 321 |
{hasAttachments && (
|
| 322 |
<div className="mb-3 flex flex-wrap gap-2">
|
| 323 |
{attachments!.map((a, idx) => {
|
| 324 |
+
const icon =
|
| 325 |
+
a.kind === "pdf"
|
| 326 |
+
? pdfIcon
|
| 327 |
+
: a.kind === "ppt"
|
| 328 |
+
? pptIcon
|
| 329 |
+
: otherIcon;
|
| 330 |
|
| 331 |
const label =
|
| 332 |
a.kind === "pdf"
|
|
|
|
| 360 |
draggable={false}
|
| 361 |
/>
|
| 362 |
<div className="min-w-0 leading-tight">
|
| 363 |
+
<div className="text-sm font-medium truncate max-w-[320px]">
|
| 364 |
+
{a.name}
|
| 365 |
+
</div>
|
| 366 |
+
<div className="text-[11px] text-muted-foreground mt-0.5">
|
| 367 |
+
{label}
|
| 368 |
+
</div>
|
| 369 |
</div>
|
| 370 |
</div>
|
| 371 |
);
|
|
|
|
| 374 |
)}
|
| 375 |
|
| 376 |
{renderBubbleContent()}
|
| 377 |
+
</div>
|
| 378 |
|
| 379 |
+
{/* ✅ References block now sits BETWEEN bubble and actions (NOT inside bubble) */}
|
| 380 |
+
{showReferencesPill && (
|
| 381 |
+
<div className={isUser ? "hidden" : ""}>
|
| 382 |
+
<Collapsible open={referencesOpen} onOpenChange={setReferencesOpen}>
|
| 383 |
+
<CollapsibleTrigger asChild>
|
| 384 |
+
<button
|
| 385 |
+
type="button"
|
| 386 |
+
className="
|
| 387 |
+
inline-flex items-center gap-1
|
| 388 |
+
rounded-md border border-border
|
| 389 |
+
bg-background/80 dark:bg-background/30
|
| 390 |
+
px-2 py-1
|
| 391 |
+
text-xs text-foreground
|
| 392 |
+
shadow-sm
|
| 393 |
+
hover:bg-background
|
| 394 |
+
transition
|
| 395 |
+
"
|
| 396 |
+
title="References"
|
| 397 |
+
>
|
| 398 |
+
{referencesOpen ? (
|
| 399 |
+
<ChevronUp className="h-3 w-3" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
) : (
|
| 401 |
+
<ChevronDown className="h-3 w-3" />
|
|
|
|
|
|
|
| 402 |
)}
|
| 403 |
+
<span className="font-medium">References</span>
|
| 404 |
+
<span className="opacity-70">({refs.length})</span>
|
| 405 |
+
</button>
|
| 406 |
+
</CollapsibleTrigger>
|
| 407 |
+
|
| 408 |
+
<CollapsibleContent className="mt-2 space-y-1">
|
| 409 |
+
{hasRefs ? (
|
| 410 |
+
refs.map((ref, index) => (
|
| 411 |
+
<div
|
| 412 |
+
key={index}
|
| 413 |
+
className="rounded-md border border-border bg-background/60 dark:bg-background/20 px-2 py-1 text-xs"
|
| 414 |
+
>
|
| 415 |
+
{ref}
|
| 416 |
+
</div>
|
| 417 |
+
))
|
| 418 |
+
) : (
|
| 419 |
+
<div className="rounded-md border border-border bg-background/60 dark:bg-background/20 px-2 py-1 text-xs opacity-80">
|
| 420 |
+
No references returned.
|
| 421 |
+
</div>
|
| 422 |
+
)}
|
| 423 |
+
</CollapsibleContent>
|
| 424 |
+
</Collapsible>
|
| 425 |
</div>
|
| 426 |
)}
|
| 427 |
|
| 428 |
+
{/* Next Question Button for Quiz Mode */}
|
| 429 |
+
{!isUser &&
|
| 430 |
+
showNextButton &&
|
| 431 |
+
!nextButtonClicked &&
|
| 432 |
+
chatMode === "quiz" &&
|
| 433 |
+
onNextQuestion && (
|
| 434 |
+
<div className="mt-2">
|
| 435 |
+
<Button
|
| 436 |
+
onClick={() => {
|
| 437 |
+
setNextButtonClicked(true);
|
| 438 |
+
onNextQuestion();
|
| 439 |
+
}}
|
| 440 |
+
className="bg-primary hover:bg-primary/90"
|
| 441 |
+
>
|
| 442 |
+
Next Question
|
| 443 |
+
</Button>
|
| 444 |
+
</div>
|
| 445 |
+
)}
|
| 446 |
+
|
| 447 |
{/* Message Actions */}
|
| 448 |
{shouldShowActions && (
|
| 449 |
<div className="flex items-center gap-1">
|
|
|
|
| 451 |
variant="ghost"
|
| 452 |
size="icon"
|
| 453 |
className={`h-7 w-7 rounded-md transition-all hover:bg-accent hover:scale-110 active:scale-95 ${
|
| 454 |
+
copied
|
| 455 |
+
? "bg-green-100 text-green-600 dark:bg-green-900/20 dark:text-green-400"
|
| 456 |
+
: ""
|
| 457 |
}`}
|
| 458 |
onClick={handleCopy}
|
| 459 |
title="Copy"
|
| 460 |
>
|
| 461 |
+
{copied ? (
|
| 462 |
+
<Check className="h-4 w-4" />
|
| 463 |
+
) : (
|
| 464 |
+
<Copy className="h-4 w-4" />
|
| 465 |
+
)}
|
| 466 |
</Button>
|
| 467 |
|
| 468 |
{!isUser && (
|
|
|
|
| 502 |
{!isUser && showFeedbackArea && feedbackType && (
|
| 503 |
<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">
|
| 504 |
<div className="flex items-start justify-between mb-4">
|
| 505 |
+
<h4 className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
| 506 |
+
Tell us more:
|
| 507 |
+
</h4>
|
| 508 |
+
<Button
|
| 509 |
+
variant="ghost"
|
| 510 |
+
size="sm"
|
| 511 |
+
className="h-6 w-6 p-0"
|
| 512 |
+
onClick={handleFeedbackClose}
|
| 513 |
+
>
|
| 514 |
<X className="h-4 w-4" />
|
| 515 |
</Button>
|
| 516 |
</div>
|