import { memo, useState, useCallback, useRef, useEffect } from "react"; import { trackStudioFeedback } from "../telemetry/events"; const DEFAULT_FEEDBACK_INTERVAL = 10; const AUTO_DISMISS_MS = 20_000; function isFeedbackDisabled(): boolean { try { return import.meta.env.VITE_HYPERFRAMES_NO_FEEDBACK === "1"; } catch { return false; } } // fallow-ignore-next-line complexity function getFeedbackInterval(): number { try { const v = import.meta.env.VITE_HYPERFRAMES_FEEDBACK_INTERVAL as string | undefined; if (v) { const n = parseInt(v, 10); if (Number.isFinite(n) && n > 0) return n; } } catch { // import.meta.env unavailable } return DEFAULT_FEEDBACK_INTERVAL; } const STORAGE_KEYS = { sessionCount: "hyperframes-studio:feedbackSessionCount", lastPromptedAt: "hyperframes-studio:feedbackLastPromptedAt", } as const; // fallow-ignore-next-line complexity function shouldShowFeedback(): boolean { if (isFeedbackDisabled()) return false; try { const count = parseInt(localStorage.getItem(STORAGE_KEYS.sessionCount) || "0", 10) || 0; const lastAt = parseInt(localStorage.getItem(STORAGE_KEYS.lastPromptedAt) || "0", 10) || 0; return count - lastAt >= getFeedbackInterval(); } catch { return false; } } const SESSION_COUNTED_KEY = "hyperframes-studio:feedbackSessionCounted"; // fallow-ignore-next-line complexity function incrementSessionCount(): void { try { if (sessionStorage.getItem(SESSION_COUNTED_KEY)) return; sessionStorage.setItem(SESSION_COUNTED_KEY, "1"); const count = parseInt(localStorage.getItem(STORAGE_KEYS.sessionCount) || "0", 10) || 0; localStorage.setItem(STORAGE_KEYS.sessionCount, String(count + 1)); } catch { // storage unavailable } } function markPrompted(): void { try { const count = localStorage.getItem(STORAGE_KEYS.sessionCount) || "0"; localStorage.setItem(STORAGE_KEYS.lastPromptedAt, count); } catch { // localStorage unavailable } } // fallow-ignore-next-line complexity export const StudioFeedbackBar = memo(function StudioFeedbackBar() { const [visible, setVisible] = useState(false); const [rating, setRating] = useState(null); const [comment, setComment] = useState(""); const [submitted, setSubmitted] = useState(false); const [exiting, setExiting] = useState(false); const inputRef = useRef(null); const dismissTimerRef = useRef | null>(null); // On mount: increment session count, check if we should show useEffect(() => { incrementSessionCount(); // Small delay so the bar doesn't flash on page load const showTimer = setTimeout(() => { if (shouldShowFeedback()) { setVisible(true); } }, 3000); return () => clearTimeout(showTimer); }, []); // Auto-dismiss timer — reset when user interacts (sets rating) useEffect(() => { if (!visible || rating !== null || submitted) return; dismissTimerRef.current = setTimeout(() => { handleDismiss(); }, AUTO_DISMISS_MS); return () => { if (dismissTimerRef.current) clearTimeout(dismissTimerRef.current); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [visible, rating, submitted]); // Focus text input when rating is selected useEffect(() => { if (rating !== null && inputRef.current) { inputRef.current.focus(); } }, [rating]); const handleDismiss = useCallback(() => { setExiting(true); markPrompted(); setTimeout(() => setVisible(false), 300); }, []); const handleSubmit = useCallback(() => { if (rating === null) return; trackStudioFeedback({ rating, comment: comment.trim() || undefined, }); setSubmitted(true); markPrompted(); setTimeout(() => { setExiting(true); setTimeout(() => setVisible(false), 300); }, 1500); }, [rating, comment]); const handleRating = useCallback((n: number) => { setRating(n); // Cancel auto-dismiss — user is engaged if (dismissTimerRef.current) { clearTimeout(dismissTimerRef.current); dismissTimerRef.current = null; } }, []); if (!visible) return null; return (
{submitted ? ( Thanks for the feedback! ) : rating !== null ? ( <> setComment(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleSubmit(); if (e.key === "Escape") handleDismiss(); }} placeholder="Any details? (enter to send, esc to close)" className="flex-1 bg-transparent border-none text-[11px] text-neutral-300 placeholder-neutral-600 outline-none" maxLength={500} /> ) : ( <> How's the Studio experience?
{[1, 2, 3, 4, 5].map((n) => ( ))}
)}
); });