Add production condition retrieval smoke test

This commit is contained in:
2026-08-11 14:58:28 +07:00
parent 59e6ad2d0d
commit 7ebbe1f309
38 changed files with 3752 additions and 121 deletions
+111
View File
@@ -0,0 +1,111 @@
"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 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>
);
}
+23 -16
View File
@@ -4,6 +4,7 @@ import React, { useState, useEffect, useRef } from "react";
import type { ChatMessage, Citation, SendMessageResponse } from "@duoc-thu/shared-types";
import { ChatBubble, CitationBeamOverlay, useTheme } from "@duoc-thu/ui";
import { Composer } from "./Composer";
import { AnswerFeedback } from "./AnswerFeedback";
import {
Sparkles,
Pill,
@@ -370,22 +371,28 @@ export function ChatPanel({
? [...messages.slice(0, msgIdx)].reverse().find((m) => m.role === "user")?.content
: undefined;
return (
<ChatBubble
key={msg.id}
message={msg}
onCitationClick={(citation, idx, allCitations) =>
onCitationClick?.(citation, idx, allCitations)
}
activeCitationIndex={activeCitationIndex}
onRetry={retryQuery ? () => handleSendMessage(retryQuery) : undefined}
onQuickReply={
msgIdx === messages.length - 1 &&
msg.decision === "clarify" &&
!isLoading
? (text) => handleSendMessage(text)
: undefined
}
/>
<React.Fragment key={msg.id}>
<ChatBubble
message={msg}
onCitationClick={(citation, idx, allCitations) =>
onCitationClick?.(citation, idx, allCitations)
}
activeCitationIndex={activeCitationIndex}
onRetry={retryQuery ? () => handleSendMessage(retryQuery) : undefined}
onQuickReply={
msgIdx === messages.length - 1 &&
msg.decision === "clarify" &&
!isLoading
? (text) => handleSendMessage(text)
: undefined
}
/>
{msg.role === "assistant" &&
msg.traceId &&
!msg.traceId.startsWith("fallback-") && (
<AnswerFeedback traceId={msg.traceId} conversationId={sessionId} />
)}
</React.Fragment>
);
})
)}