DocDoeAI / src /components /beta /BetaFeedbackButton.tsx
asnannp's picture
deploy: sync backend to Space root (learn-lesson HF cache fix)
65d68f5
Raw History Blame Contribute Delete
8.49 kB
"use client";
import { useId, useState } from "react";
import { MessageSquarePlus, X } from "lucide-react";
import { cn } from "@/lib/utils";
import { trackBetaEvent } from "@/lib/beta/betaAnalytics";
import {
BETA_FEEDBACK_KIND_LABELS,
MAX_FEEDBACK_MESSAGE_LENGTH,
submitBetaFeedback,
type BetaFeedbackKind,
type BetaFeedbackRating,
} from "@/lib/beta/betaFeedback";
type BetaFeedbackButtonProps = {
route?: string;
subject?: string;
chapterTitle?: string;
missionTitle?: string;
stepLabel?: string;
className?: string;
};
const KIND_ORDER: BetaFeedbackKind[] = [
"helped_understand",
"still_confusing",
"wrong_or_doubtful",
"too_long",
"need_more_examples",
"class_confusing",
"bug",
"content_issue",
"ui_issue",
"too_hard",
"too_easy",
"liked",
"other",
];
export function BetaFeedbackButton({
route,
subject,
chapterTitle,
missionTitle,
stepLabel,
className,
}: BetaFeedbackButtonProps) {
const [open, setOpen] = useState(false);
const [kind, setKind] = useState<BetaFeedbackKind>("class_confusing");
const [rating, setRating] = useState<BetaFeedbackRating | null>(null);
const [message, setMessage] = useState("");
const [submitted, setSubmitted] = useState(false);
const [error, setError] = useState<string | null>(null);
const titleId = useId();
const messageId = useId();
const context = { route, subject, chapterTitle, missionTitle, stepLabel };
function openPanel() {
setOpen(true);
setSubmitted(false);
setError(null);
trackBetaEvent({ name: "feedback_opened", ...context });
}
function closePanel() {
setOpen(false);
}
function handleSubmit() {
const result = submitBetaFeedback({ kind, rating: rating ?? undefined, message, ...context });
if (!result.ok) {
setError(result.error === "empty_message" ? "Please write a short message first." : "Storage unavailable on this device.");
return;
}
setSubmitted(true);
setMessage("");
setRating(null);
}
return (
<div className={cn("relative", className)}>
<button
type="button"
onClick={openPanel}
aria-haspopup="dialog"
aria-expanded={open}
aria-label="Open beta feedback"
className="inline-flex min-h-9 items-center gap-2 rounded-lg border border-[#2A2A2A] bg-[#111111] px-3 text-[12px] font-semibold text-white transition hover:bg-[#1A1A1A] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/35"
>
<MessageSquarePlus className="size-3.5" aria-hidden="true" />
Beta feedback
</button>
{open ? (
<div
role="dialog"
aria-modal="false"
aria-labelledby={titleId}
data-testid="beta-feedback-panel"
className="absolute right-0 z-40 mt-2 w-[min(92vw,340px)] rounded-xl border border-[#2A2A2A] bg-[#0B0B0B] p-4 shadow-[0_24px_80px_rgba(0,0,0,0.5)]"
>
<div className="flex items-start justify-between gap-2">
<div>
<p id={titleId} className="text-[13px] font-bold text-white">
Beta feedback
</p>
<p className="mt-1 text-[11px] leading-4 text-[#8B8B8B]">Tell us what confused you or what broke.</p>
</div>
<button
type="button"
onClick={closePanel}
aria-label="Close beta feedback"
className="rounded-lg border border-[#2A2A2A] bg-[#111111] p-1.5 text-[#A1A1AA] transition hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/35"
>
<X className="size-3.5" aria-hidden="true" />
</button>
</div>
{submitted ? (
<div className="mt-4" data-testid="beta-feedback-confirmation">
<p className="text-[13px] font-semibold text-white">Thanks! Feedback saved.</p>
<p className="mt-1 text-[11px] leading-4 text-[#8B8B8B]">Stored on this device for beta testing.</p>
<button
type="button"
onClick={closePanel}
className="mt-3 inline-flex min-h-9 w-full items-center justify-center rounded-lg bg-white px-3 text-[12px] font-bold text-black transition hover:bg-white/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/35"
>
Close feedback
</button>
</div>
) : (
<div className="mt-3 space-y-3">
<div>
<label htmlFor={`${titleId}-kind`} className="text-[10px] font-semibold uppercase tracking-wider text-[#6B7280]">
What is this about?
</label>
<select
id={`${titleId}-kind`}
aria-label="What is this about?"
value={kind}
onChange={(event) => setKind(event.target.value as BetaFeedbackKind)}
className="mt-1.5 w-full rounded-lg border border-[#2A2A2A] bg-[#050505] p-2 text-[12px] text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/35"
>
{KIND_ORDER.map((value) => (
<option key={value} value={value}>
{BETA_FEEDBACK_KIND_LABELS[value]}
</option>
))}
</select>
</div>
<div>
<fieldset className="mt-1.5 flex gap-1.5" aria-label="Rating (optional)">
<legend className="sr-only">Rating (optional)</legend>
{([1, 2, 3, 4, 5] as BetaFeedbackRating[]).map((value) => (
<button
key={value}
type="button"
aria-label={`Rate ${value} out of 5`}
aria-pressed={rating === value}
onClick={() => setRating((current) => (current === value ? null : value))}
className={cn(
"size-8 rounded-lg border text-[12px] font-bold transition focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/35",
rating === value
? "border-white bg-white text-black"
: "border-[#2A2A2A] bg-[#050505] text-white hover:bg-[#151515]",
)}
>
{value}
</button>
))}
</fieldset>
</div>
<div>
<label htmlFor={messageId} className="text-[10px] font-semibold uppercase tracking-wider text-[#6B7280]">
Message
</label>
<textarea
id={messageId}
aria-label="Message"
value={message}
maxLength={MAX_FEEDBACK_MESSAGE_LENGTH}
onChange={(event) => {
setMessage(event.target.value);
if (error) setError(null);
}}
rows={4}
placeholder="What confused you or what broke?"
className="mt-1.5 w-full resize-y rounded-lg border border-[#2A2A2A] bg-[#050505] p-2.5 text-[12px] leading-5 text-white placeholder:text-[#5F6368] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/35"
/>
<div className="mt-1 flex items-center justify-between">
<p className="text-[10px] text-[#5F6368]">{message.length}/{MAX_FEEDBACK_MESSAGE_LENGTH}</p>
{error ? <p className="text-[10px] font-semibold text-white">{error}</p> : null}
</div>
</div>
<p className="text-[10px] leading-4 text-[#6B7280]">
Beta feedback is saved on this device. Do not include private personal details.
</p>
<button
type="button"
onClick={handleSubmit}
className="inline-flex min-h-9 w-full items-center justify-center rounded-lg bg-white px-3 text-[12px] font-bold text-black transition hover:bg-white/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/35"
>
Submit feedback
</button>
</div>
)}
</div>
) : null}
</div>
);
}