112 lines
4.0 KiB
TypeScript
112 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { Check, MessageSquareText, ThumbsDown, ThumbsUp } from "lucide-react";
|
|
|
|
interface AnswerFeedbackProps {
|
|
traceId: string;
|
|
conversationId: string;
|
|
}
|
|
|
|
type Rating = "helpful" | "not_helpful";
|
|
|
|
export function AnswerFeedback({ traceId, conversationId }: AnswerFeedbackProps) {
|
|
const [rating, setRating] = useState<Rating | null>(null);
|
|
const [comment, setComment] = useState("");
|
|
const [state, setState] = useState<"idle" | "saving" | "saved" | "error">("idle");
|
|
|
|
const choose = (next: Rating) => {
|
|
setRating(next);
|
|
setState("idle");
|
|
};
|
|
|
|
const submit = async () => {
|
|
if (!rating || state === "saving") return;
|
|
setState("saving");
|
|
try {
|
|
const response = await fetch("/api/feedback", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ traceId, rating, comment, conversationId }),
|
|
});
|
|
setState(response.ok ? "saved" : "error");
|
|
} catch {
|
|
setState("error");
|
|
}
|
|
};
|
|
|
|
if (state === "saved") {
|
|
return (
|
|
<div className="ml-11 mt-1 flex items-center gap-1.5 text-[0.7rem] text-status-success">
|
|
<Check className="h-3.5 w-3.5" />
|
|
Đã ghi nhận phản hồi. Cảm ơn anh/chị.
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="ml-11 mt-1 max-w-xl text-[0.7rem] text-txt-muted">
|
|
<div className="flex flex-wrap items-center gap-1.5">
|
|
<span>Câu trả lời này có hữu ích không?</span>
|
|
<button
|
|
type="button"
|
|
aria-label="Câu trả lời hữu ích"
|
|
aria-pressed={rating === "helpful"}
|
|
onClick={() => choose("helpful")}
|
|
className={`rounded-lg border p-1.5 transition-colors ${
|
|
rating === "helpful"
|
|
? "border-status-success bg-status-success-bg text-status-success"
|
|
: "border-border-subtle hover:border-border-accent hover:text-txt-primary"
|
|
}`}
|
|
>
|
|
<ThumbsUp className="h-3.5 w-3.5" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
aria-label="Câu trả lời chưa hữu ích"
|
|
aria-pressed={rating === "not_helpful"}
|
|
onClick={() => choose("not_helpful")}
|
|
className={`rounded-lg border p-1.5 transition-colors ${
|
|
rating === "not_helpful"
|
|
? "border-status-danger bg-status-danger-bg text-status-danger"
|
|
: "border-border-subtle hover:border-border-accent hover:text-txt-primary"
|
|
}`}
|
|
>
|
|
<ThumbsDown className="h-3.5 w-3.5" />
|
|
</button>
|
|
</div>
|
|
|
|
{rating && (
|
|
<div className="mt-2 flex flex-col gap-2 rounded-xl border border-border-subtle bg-surface p-2.5">
|
|
<label className="flex items-center gap-1.5 font-medium text-txt-secondary">
|
|
<MessageSquareText className="h-3.5 w-3.5" />
|
|
Góp ý thêm (không bắt buộc)
|
|
</label>
|
|
<textarea
|
|
value={comment}
|
|
maxLength={2000}
|
|
rows={2}
|
|
onChange={(event) => setComment(event.target.value)}
|
|
placeholder="Ví dụ: thiếu lưu ý suy thận, trích dẫn chưa đúng trang…"
|
|
className="resize-y rounded-lg border border-border-subtle bg-surface-elevated px-2.5 py-2 text-xs text-txt-primary outline-none focus:border-border-accent"
|
|
/>
|
|
<div className="flex items-center justify-between gap-2">
|
|
<span>{comment.length}/2000</span>
|
|
<button
|
|
type="button"
|
|
onClick={submit}
|
|
disabled={state === "saving"}
|
|
className="rounded-lg bg-accent-primary px-3 py-1.5 font-bold text-white disabled:opacity-60"
|
|
>
|
|
{state === "saving" ? "Đang gửi…" : "Gửi phản hồi"}
|
|
</button>
|
|
</div>
|
|
{state === "error" && (
|
|
<p className="m-0 text-status-danger">Chưa lưu được phản hồi. Vui lòng thử lại.</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|