agentic.email/frontend/src/components/HumanHandoffDrawer.jsx
2026-10-06 07:12:55 +05:30

320 lines
12 KiB
JavaScript

import { Mail, UserRound, X } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import AgentSpark from "./AgentSpark";
import {
ALL_MAIL_LABEL,
ALL_TYPES_ID,
buildHandoffSummary,
EMAIL_TYPE_OPTIONS,
HANDOFF_LABELS,
INSTRUCTION_EXAMPLES,
} from "../utils/handoffUtils";
function ToggleSwitch({ checked, onChange, label }) {
return (
<label className="handoff-switch">
<span>{label}</span>
<input type="checkbox" checked={checked} onChange={(event) => onChange(event.target.checked)} />
<span className="handoff-switch-track" aria-hidden="true" />
</label>
);
}
export default function HumanHandoffDrawer({
open,
rule,
saving,
onClose,
onSave,
onTest,
}) {
const [ruleName, setRuleName] = useState("");
const [priority, setPriority] = useState(0);
const [ownerEmail, setOwnerEmail] = useState("");
const [labels, setLabels] = useState([]);
const [emailTypes, setEmailTypes] = useState([]);
const [instructions, setInstructions] = useState("");
const [allowReplies, setAllowReplies] = useState(true);
const [active, setActive] = useState(true);
const [labelQuery, setLabelQuery] = useState("");
useEffect(() => {
if (!open) return;
setRuleName(rule?.name || "");
setPriority(rule?.priority ?? 0);
setOwnerEmail(rule?.owner_email || "");
setLabels(rule?.labels || []);
setEmailTypes(rule?.email_types || []);
setInstructions(rule?.instructions || "");
setAllowReplies(rule?.allow_replies ?? true);
setActive(rule?.active ?? true);
setLabelQuery("");
}, [open, rule]);
const summary = useMemo(
() => buildHandoffSummary({
ownerEmail,
labels,
emailTypes,
instructions,
}),
[ownerEmail, labels, emailTypes, instructions],
);
const filteredLabels = HANDOFF_LABELS.filter((label) =>
label.toLowerCase().includes(labelQuery.trim().toLowerCase()),
);
function toggleLabel(label) {
if (label === ALL_MAIL_LABEL) {
setLabels((current) => (
current.includes(ALL_MAIL_LABEL) ? [] : [ALL_MAIL_LABEL]
));
return;
}
setLabels((current) => {
const withoutAll = current.filter((item) => item !== ALL_MAIL_LABEL);
return withoutAll.includes(label)
? withoutAll.filter((item) => item !== label)
: [...withoutAll, label];
});
}
function toggleEmailType(typeId) {
if (typeId === ALL_TYPES_ID) {
setEmailTypes((current) => (
current.includes(ALL_TYPES_ID) ? [] : [ALL_TYPES_ID]
));
return;
}
setEmailTypes((current) => {
const withoutAll = current.filter((item) => item !== ALL_TYPES_ID);
return withoutAll.includes(typeId)
? withoutAll.filter((item) => item !== typeId)
: [...withoutAll, typeId];
});
}
function insertExample(example) {
setInstructions((current) => {
const trimmed = current.trim();
const addition = example.toLowerCase();
if (!trimmed) return `Hand off when there is a ${addition}.`;
if (trimmed.toLowerCase().includes(addition)) return current;
return `${trimmed.replace(/\.$/, "")}, or when there is a ${addition}.`;
});
}
if (!open) return null;
return (
<div className="handoff-drawer-backdrop" role="presentation" onClick={onClose}>
<aside
className="handoff-drawer"
role="dialog"
aria-labelledby="handoff-drawer-title"
onClick={(event) => event.stopPropagation()}
>
<header className="handoff-drawer-head">
<div>
<h2 id="handoff-drawer-title">{rule ? "Edit Human Handoff" : "Add Human Handoff"}</h2>
<p>Choose who should receive emails that need human attention and define when AGMail should hand them off.</p>
</div>
<button type="button" className="handoff-drawer-close" aria-label="Close" onClick={onClose}>
<X size={18} strokeWidth={1.75} />
</button>
</header>
<div className="handoff-drawer-body">
<section className="handoff-form-section">
<h3>Rule</h3>
<label className="handoff-field-label" htmlFor="handoff-rule-name">Rule name</label>
<input
id="handoff-rule-name"
className="handoff-text-input"
value={ruleName}
maxLength={120}
placeholder="Finance approvals"
onChange={(event) => setRuleName(event.target.value)}
/>
<label className="handoff-field-label" htmlFor="handoff-rule-priority">Priority</label>
<input
id="handoff-rule-priority"
className="handoff-text-input"
type="number"
min="0"
max="10000"
value={priority}
onChange={(event) => setPriority(Number(event.target.value) || 0)}
/>
<p className="handoff-helper">When multiple rules match, the lowest priority number wins.</p>
</section>
<section className="handoff-form-section">
<h3>Human owner</h3>
<label className="handoff-field-label" htmlFor="handoff-owner-email">Owner email address</label>
<div className="handoff-email-input">
<Mail size={16} strokeWidth={1.75} />
<input
id="handoff-owner-email"
type="email"
value={ownerEmail}
placeholder="you@company.com"
onChange={(event) => setOwnerEmail(event.target.value)}
/>
</div>
<p className="handoff-helper">This is the human owner behind this agent. Emails matching your handoff rules will be sent here.</p>
{ownerEmail.includes("@") && (
<div className="handoff-owner-preview">
<span className="handoff-owner-avatar" aria-hidden="true">
<UserRound size={16} strokeWidth={1.75} />
</span>
<div>
<strong>{ownerEmail.split("@")[0].replace(/[._-]/g, " ")}</strong>
<small>{ownerEmail}</small>
</div>
</div>
)}
</section>
<section className="handoff-form-section">
<h3>Hand off emails with these labels</h3>
<p className="handoff-helper">
These are content-based handoff categories, not inbox tabs. Gentic infers them from
subject and body text. Choose <strong>All mail</strong> to hand off every incoming email.
</p>
<div className="handoff-label-toolbar">
<input
type="search"
placeholder="Search labels…"
value={labelQuery}
onChange={(event) => setLabelQuery(event.target.value)}
/>
<button type="button" onClick={() => setLabels([ALL_MAIL_LABEL])}>All mail</button>
<button
type="button"
onClick={() => setLabels(HANDOFF_LABELS.filter((label) => label !== ALL_MAIL_LABEL))}
>
Select categories
</button>
</div>
<div className="handoff-label-grid">
{filteredLabels.map((label) => (
<label key={label} className="handoff-check-pill">
<input
type="checkbox"
checked={labels.includes(label)}
onChange={() => toggleLabel(label)}
/>
<span>{label}</span>
</label>
))}
</div>
</section>
<section className="handoff-form-section">
<h3>Hand off these types of email</h3>
<p className="handoff-helper">
Types are also inferred from email content. Choose <strong>All types</strong> if you
do not want to restrict by category.
</p>
<div className="handoff-type-grid">
{EMAIL_TYPE_OPTIONS.map((type) => (
<label key={type.id} className={`handoff-type-card${emailTypes.includes(type.id) ? " selected" : ""}`}>
<input
type="checkbox"
checked={emailTypes.includes(type.id)}
onChange={() => toggleEmailType(type.id)}
/>
<strong>{type.title}</strong>
<span>{type.description}</span>
</label>
))}
</div>
</section>
<section className="handoff-form-section">
<h3>When should the agent hand off?</h3>
<p className="handoff-helper">Write this naturally, just as you would instruct your agent.</p>
<textarea
className="handoff-instructions"
rows={5}
value={instructions}
placeholder="e.g. Hand off when a sender asks to speak with me, when a payment or approval requires my decision, or whenever the agent is not confident it should respond on my behalf."
onChange={(event) => setInstructions(event.target.value)}
/>
<p className="handoff-helper">Be specific about intent, urgency, topics, amounts, approvals or anything that should require your attention.</p>
<div className="handoff-examples">
<span>Examples:</span>
{INSTRUCTION_EXAMPLES.map((example) => (
<button key={example} type="button" onClick={() => insertExample(example)}>
{example}
</button>
))}
</div>
</section>
<section className="handoff-summary-card">
<div className="handoff-summary-title">
<AgentSpark />
<span>Handoff behavior</span>
</div>
<p>{summary}</p>
</section>
<section className="handoff-form-section">
<h3>Continue the conversation from my inbox</h3>
<ToggleSwitch
label="Allow human replies"
checked={allowReplies}
onChange={setAllowReplies}
/>
<p className="handoff-helper">
A reply from this Human Owner is stored in Handoff Inbox and relayed through the
original Gentic address. Their private address is never exposed.
</p>
</section>
<section className="handoff-form-section">
<ToggleSwitch label="Human Handoff active" checked={active} onChange={setActive} />
<p className="handoff-helper">When disabled, your agent will continue receiving email but will no longer hand new conversations to the human owner.</p>
</section>
</div>
<footer className="handoff-drawer-foot">
<button type="button" className="handoff-btn subtle" onClick={onClose}>Cancel</button>
<button
type="button"
className="handoff-btn subtle"
onClick={() => onTest?.({
owner_email: ownerEmail,
labels,
email_types: emailTypes,
instructions,
})}
>
<AgentSpark /> Test Handoff
</button>
<button
type="button"
className="handoff-btn primary"
disabled={saving || !ruleName.trim() || !ownerEmail.trim()}
onClick={() => onSave({
name: ruleName.trim(),
priority,
owner_email: ownerEmail.trim(),
labels,
email_types: emailTypes,
instructions: instructions.trim(),
allow_replies: allowReplies,
reply_as: "agent",
active,
})}
>
{rule ? "Save Human Handoff" : "Create handoff"}
</button>
</footer>
</aside>
</div>
);
}