amana / frontend /src /components /HumanControls.tsx
Misbahuddin's picture
feat(ui): first-run welcome modal + Light/Auto/Dark theme
61cdd31
Raw
History Blame Contribute Delete
3.94 kB
import { useState } from "react";
import { Check, HelpCircle, X } from "lucide-react";
import type { GatedResult } from "../types";
const BUTTONS = [
{ label: "Approve", human: "APPROVE", cls: "bg-emerald-600 hover:bg-emerald-700", Icon: Check },
{ label: "Reject", human: "REJECT", cls: "bg-rose-600 hover:bg-rose-700", Icon: X },
{ label: "Request info", human: "ESCALATE", cls: "bg-amber-500 hover:bg-amber-600", Icon: HelpCircle },
];
const MOD_KEY = "amana.moderator";
export function HumanControls({
result,
onDecide,
}: {
result: GatedResult;
onDecide: (human: string, moderator: string, reason: string) => Promise<void>;
}) {
const [moderator, setModerator] = useState(() => localStorage.getItem(MOD_KEY) ?? "");
const [reason, setReason] = useState("");
const [msg, setMsg] = useState<{ kind: "ok" | "warn"; text: string } | null>(null);
const [busy, setBusy] = useState(false);
const aiRec = result.decision.recommendation;
const click = async (human: string) => {
const name = moderator.trim();
if (!name) {
setMsg({ kind: "warn", text: "Add your name first — a human owns every decision." });
return;
}
const isOverride = human !== aiRec;
if (isOverride && !reason.trim()) {
setMsg({ kind: "warn", text: `Overriding the AI's ${aiRec} with ${human} needs a written reason.` });
return;
}
setBusy(true);
setMsg(null);
try {
await onDecide(human, name, reason.trim());
localStorage.setItem(MOD_KEY, name);
setMsg({
kind: "ok",
text: isOverride
? `Recorded — ${name} overrode the AI's ${aiRec} (reason logged).`
: `Recorded: ${human} by ${name} (agreed with the AI).`,
});
setReason("");
} catch (e) {
setMsg({ kind: "warn", text: (e as Error).message });
} finally {
setBusy(false);
}
};
return (
<div className="border-t border-line pt-4">
<div className="text-sm font-semibold text-heading">
Your decision <span className="font-normal text-faint">— final; the AI does not decide</span>
</div>
<div className="mt-3 grid grid-cols-1 sm:grid-cols-[auto,1fr] sm:items-center gap-2">
<label className="text-xs font-medium uppercase tracking-wide text-faint sm:pr-1">
Moderator
</label>
<input
value={moderator}
onChange={(e) => setModerator(e.target.value)}
placeholder="Your name (recorded with the decision)"
className="w-full text-sm border border-line rounded-lg px-3 py-2 bg-surface text-body focus:outline-none focus:ring-2 focus:ring-accent/40 focus:border-accent"
/>
</div>
<textarea
value={reason}
onChange={(e) => setReason(e.target.value)}
rows={2}
placeholder="Reason / notes — required to override the AI's recommendation"
className="mt-2 w-full text-sm border border-line rounded-lg p-2.5 bg-surface text-body focus:outline-none focus:ring-2 focus:ring-accent/40 focus:border-accent"
/>
<div className="grid grid-cols-3 gap-2 mt-2">
{BUTTONS.map(({ label, human, cls, Icon }) => (
<button
key={human}
onClick={() => click(human)}
disabled={busy}
className={`flex items-center justify-center gap-1.5 text-white rounded-lg py-2.5 text-sm font-medium shadow-sm transition disabled:opacity-60 ${cls}`}
>
<Icon size={15} /> {label}
</button>
))}
</div>
{msg && (
<div
className={`mt-2 text-sm px-3 py-2 rounded-lg ${
msg.kind === "ok"
? "bg-emerald-50 text-emerald-800 dark:bg-emerald-500/10 dark:text-emerald-300"
: "bg-amber-50 text-amber-800 dark:bg-amber-500/10 dark:text-amber-300"
}`}
>
{msg.text}
</div>
)}
</div>
);
}