import { useActionMutation, useReconciledState, } from "@agent-native/core/client/hooks"; import { useT } from "@agent-native/core/client/i18n"; import { IconLock, IconClock, IconMessage, IconDownload, IconPhoto, IconX, IconMoodSmile, } from "@tabler/icons-react"; import { useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; import { Switch } from "@/components/ui/switch"; import { SPEED_OPTIONS } from "./player-controls"; export interface SettingsPanelProps { recording: { id: string; /** * Whether a password is currently set on the recording. The plaintext * password is never sent to the client — the editor sees `hasPassword` * and can either replace or clear the password, but never read it. */ hasPassword: boolean; expiresAt: string | null; enableComments: boolean; enableReactions: boolean; enableDownloads: boolean; defaultSpeed: string; animatedThumbnailEnabled: boolean; }; visibility: "private" | "org" | "public"; ctas: { id: string; label: string; url: string; color: string; placement: "end" | "throughout"; }[]; onClose: () => void; onRefetch?: () => void; showHeader?: boolean; } export function SettingsPanel(props: SettingsPanelProps) { const t = useT(); const { recording, visibility, ctas, onClose, onRefetch, showHeader = true, } = props; const update = useActionMutation("update-recording", { onSuccess: () => onRefetch?.(), }); const setVis = useActionMutation("set-resource-visibility", { onSuccess: () => onRefetch?.(), }); const createCta = useActionMutation("create-cta", { onSuccess: () => onRefetch?.(), }); const updateCta = useActionMutation("update-cta", { onSuccess: () => onRefetch?.(), }); const deleteCta = useActionMutation("delete-cta", { onSuccess: () => onRefetch?.(), }); // The plaintext password is never sent to the client (see action // `get-recording-player-data`). Start empty; the placeholder communicates // whether one is currently set. Saving an empty value clears it, saving a // non-empty value replaces it. const [password, setPassword] = useState(""); // Re-adopt the server/agent expiry when the user isn't actively editing the // field, so an agent change to `expiresAt` shows up live in the open panel. const expiresFocused = useRef(false); const [expiresAt, setExpiresAt] = useReconciledState( recording.expiresAt ?? "", { active: expiresFocused.current }, ); function patch(fields: Record) { update.mutate({ id: recording.id, ...fields } as any); } return (
{showHeader ? (

{t("playerSettings.title")}

) : null}
{/* Privacy */}

{t("playerSettings.privacy")}

setPassword(e.target.value)} placeholder={ recording.hasPassword ? t("playerSettings.passwordSetPlaceholder") : t("playerSettings.noPasswordPlaceholder") } /> {password.length > 0 && !password.trim() ? (

{t("playerSettings.passwordWhitespaceOnly")}

) : null}
{recording.hasPassword ? ( ) : null}
{ expiresFocused.current = true; }} onBlur={() => { expiresFocused.current = false; }} onChange={(e) => setExpiresAt(fromDatetimeLocal(e.target.value)) } className="flex-1" />
{/* Toggles */}

{t("playerSettings.viewerOptions")}

} label={t("playerSettings.comments")} checked={recording.enableComments} onChange={(v) => patch({ enableComments: v })} /> } label={t("playerSettings.reactions")} checked={recording.enableReactions} onChange={(v) => patch({ enableReactions: v })} /> } label={t("playerSettings.allowDownloads")} checked={recording.enableDownloads} onChange={(v) => patch({ enableDownloads: v })} /> } label={t("playerSettings.animatedThumbnail")} checked={recording.animatedThumbnailEnabled} onChange={(v) => patch({ animatedThumbnailEnabled: v })} />
{/* Default speed */}

{t("playerSettings.defaultPlaybackSpeed")}

{/* CTA editor */}

{t("playerSettings.callToAction")}

{ctas.length === 0 ? (

{t("playerSettings.noCtas")}

) : (
{ctas.map((cta) => ( updateCta.mutate({ id: cta.id, ...fields } as any) } onDelete={() => deleteCta.mutate({ id: cta.id } as any)} /> ))}
)}
); } function ToggleRow({ icon, label, checked, onChange, }: { icon: React.ReactNode; label: string; checked: boolean; onChange: (v: boolean) => void; }) { return (
{icon} {label}
); } function CtaEditor({ cta, t, onSave, onDelete, }: { cta: { id: string; label: string; url: string; color: string; placement: "end" | "throughout"; }; t: ReturnType; onSave: (fields: Record) => void; onDelete: () => void; }) { // Re-adopt the server/agent CTA fields whenever the user isn't actively // editing this card, so an agent edit to the CTA shows up live. `editing` // flips true while focus is anywhere inside the card. const editing = useRef(false); const [collapsed, setCollapsed] = useState(true); const [label, setLabel] = useReconciledState(cta.label, { active: editing.current, }); const [url, setUrl] = useReconciledState(cta.url, { active: editing.current, }); const [color, setColor] = useReconciledState(cta.color, { active: editing.current, }); const [placement, setPlacement] = useReconciledState(cta.placement, { active: editing.current, }); return (
{ editing.current = true; }} onBlurCapture={(e) => { // Only clear when focus leaves the card entirely. if (!e.currentTarget.contains(e.relatedTarget as Node | null)) { editing.current = false; } }} > {collapsed ? (

{cta.label}

{cta.placement === "throughout" ? t("playerSettings.placementThroughout") : t("playerSettings.placementEnd")}

) : ( <> setLabel(e.target.value)} placeholder={t("playerSettings.buttonLabelPlaceholder")} /> setUrl(e.target.value)} placeholder="https://…" />
setColor(e.target.value)} className="h-9 w-12 rounded cursor-pointer border border-border" />
)}
); } function toDatetimeLocal(iso: string | null): string { if (!iso) return ""; const d = new Date(iso); if (isNaN(d.getTime())) return ""; const pad = (n: number) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`; } function fromDatetimeLocal(v: string): string { if (!v) return ""; return new Date(v).toISOString(); }