agentic.email/frontend/src/components/HandoffSettings.jsx
2026-10-06 04:26:32 +05:30

279 lines
10 KiB
JavaScript

import { Info, MoreHorizontal, Plus, UserRound } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import {
createHandoffRule,
deleteHandoffRule,
listHandoffRules,
testHandoffRule,
updateHandoffRule,
} from "../api";
import AgentSpark from "./AgentSpark";
import HumanHandoffDrawer from "./HumanHandoffDrawer";
import { buildRuleCardChips, EMAIL_TYPE_OPTIONS } from "../utils/handoffUtils";
function typeLabel(id) {
return EMAIL_TYPE_OPTIONS.find((item) => item.id === id)?.title || id;
}
export default function HandoffSettings() {
const token = sessionStorage.getItem("agmail_access_token");
const [rules, setRules] = useState([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
const [notice, setNotice] = useState("");
const [drawerOpen, setDrawerOpen] = useState(false);
const [editingRule, setEditingRule] = useState(null);
const [menuOpenId, setMenuOpenId] = useState(null);
const [testOpen, setTestOpen] = useState(false);
const [testDraft, setTestDraft] = useState(null);
const [testSubject, setTestSubject] = useState("");
const [testBody, setTestBody] = useState("");
const [testResult, setTestResult] = useState(null);
const [testing, setTesting] = useState(false);
const loadRules = useCallback(async () => {
setLoading(true);
setError("");
try {
const data = await listHandoffRules(token);
setRules(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, [token]);
useEffect(() => {
loadRules();
}, [loadRules]);
function openCreate() {
setEditingRule(null);
setDrawerOpen(true);
}
function openEdit(rule) {
setEditingRule(rule);
setDrawerOpen(true);
setMenuOpenId(null);
}
async function handleSave(payload) {
setSaving(true);
setError("");
try {
if (editingRule) {
await updateHandoffRule(editingRule.id, payload, token);
setNotice("Human Handoff updated.");
} else {
await createHandoffRule(payload, token);
setNotice("Human Handoff created.");
}
setDrawerOpen(false);
setEditingRule(null);
await loadRules();
window.setTimeout(() => setNotice(""), 2500);
} catch (err) {
setError(err.message);
} finally {
setSaving(false);
}
}
async function handleDelete(rule) {
if (!window.confirm("Delete this handoff rule?")) return;
try {
await deleteHandoffRule(rule.id, token);
setMenuOpenId(null);
await loadRules();
} catch (err) {
setError(err.message);
}
}
function openTest(draft) {
setTestDraft(draft);
setTestSubject("");
setTestBody("");
setTestResult(null);
setTestOpen(true);
}
async function runTest() {
if (!testDraft) return;
setTesting(true);
setError("");
try {
const result = await testHandoffRule({
rule_id: editingRule?.id,
owner_email: testDraft.owner_email,
labels: testDraft.labels,
email_types: testDraft.email_types,
instructions: testDraft.instructions,
email: {
subject: testSubject,
snippet: testBody,
body_text: testBody,
},
}, token);
setTestResult(result);
} catch (err) {
setError(err.message);
} finally {
setTesting(false);
}
}
return (
<div className="handoff-settings">
<div className="handoff-settings-notice">
<Info size={14} strokeWidth={2} className="handoff-settings-notice-icon" aria-hidden="true" />
<div>
<strong>Configuration only</strong>
<p>Handoff rules are saved as configuration only. Email routing remains unchanged.</p>
</div>
</div>
<div className="handoff-settings-toolbar">
<button type="button" className="handoff-btn primary" onClick={openCreate}>
<Plus size={16} strokeWidth={1.75} /> Add handoff rule
</button>
</div>
{error && <p className="form-error" role="alert">{error}</p>}
{notice && <p className="security-notice" role="status">{notice}</p>}
{loading ? (
<p className="people-table-empty">Loading settings…</p>
) : rules.length === 0 ? (
<div className="human-handoff-empty">
<div className="human-handoff-empty-icon" aria-hidden="true">
<UserRound size={24} strokeWidth={1.75} />
</div>
<h2>No handoff rules yet</h2>
<p>Configure when your Agent should involve a Human Owner.</p>
<button type="button" className="handoff-btn primary" onClick={openCreate}>
<Plus size={16} strokeWidth={1.75} /> Add handoff rule
</button>
</div>
) : (
<div className="human-handoff-list">
{rules.map((rule) => (
<article key={rule.id} className="human-handoff-card">
<div className="human-handoff-card-body">
<span className="human-handoff-card-avatar" aria-hidden="true">
<UserRound size={17} strokeWidth={1.75} />
</span>
<strong className="human-handoff-card-name">{rule.owner_name}</strong>
<span className={`human-handoff-status${rule.active ? " active" : ""}`}>
{rule.active ? "Active" : "Paused"}
</span>
<small className="human-handoff-card-email">{rule.owner_email}</small>
<div className="human-handoff-card-actions">
<button type="button" className="handoff-btn subtle" onClick={() => openEdit(rule)}>
Edit
</button>
<div className="handoff-card-menu-wrap">
<button
type="button"
className="handoff-btn icon quiet"
aria-label="More actions"
onClick={() => setMenuOpenId((current) => (current === rule.id ? null : rule.id))}
>
<MoreHorizontal size={16} strokeWidth={1.75} />
</button>
{menuOpenId === rule.id && (
<div className="handoff-card-menu">
<button type="button" onClick={() => openTest({
owner_email: rule.owner_email,
labels: rule.labels,
email_types: rule.email_types,
instructions: rule.instructions,
})}>Test with an email</button>
<button type="button" onClick={() => handleDelete(rule)}>Delete</button>
</div>
)}
</div>
</div>
</div>
<div className="human-handoff-card-chips">
{buildRuleCardChips(rule).map((chip) => (
<span key={chip} className="handoff-meta-chip">{chip}</span>
))}
</div>
</article>
))}
</div>
)}
<HumanHandoffDrawer
open={drawerOpen}
rule={editingRule}
saving={saving}
onClose={() => { setDrawerOpen(false); setEditingRule(null); }}
onSave={handleSave}
onTest={(draft) => openTest(draft)}
/>
{testOpen && (
<div className="handoff-drawer-backdrop" role="presentation" onClick={() => setTestOpen(false)}>
<aside className="handoff-test-modal" role="dialog" aria-labelledby="handoff-test-title" onClick={(e) => e.stopPropagation()}>
<header className="handoff-drawer-head">
<div>
<h2 id="handoff-test-title">Test Human Handoff</h2>
<p>Check whether your current rules would match a particular email.</p>
</div>
</header>
<div className="handoff-drawer-body">
<label className="handoff-field-label" htmlFor="handoff-test-subject">Subject</label>
<input
id="handoff-test-subject"
className="handoff-text-input"
value={testSubject}
onChange={(event) => setTestSubject(event.target.value)}
placeholder="Payment approval required"
/>
<label className="handoff-field-label" htmlFor="handoff-test-body">Email content</label>
<textarea
id="handoff-test-body"
className="handoff-instructions"
rows={6}
value={testBody}
onChange={(event) => setTestBody(event.target.value)}
placeholder="Paste or describe the email you want to test…"
/>
{testResult && (
<div className="handoff-test-result">
<h3>Handoff test</h3>
<ul>
<li className={testResult.label_matched ? "pass" : "fail"}>
{testResult.label_matched ? "✓" : "✕"} Label matched
{testResult.matched_labels?.length ? ` — ${testResult.matched_labels.join(", ")}` : ""}
</li>
<li className={testResult.type_matched ? "pass" : "fail"}>
{testResult.type_matched ? "✓" : "✕"} Type matched
{testResult.matched_types?.length ? ` — ${testResult.matched_types.map(typeLabel).join(", ")}` : ""}
</li>
<li className={testResult.instruction_matched ? "pass" : "fail"}>
{testResult.instruction_matched ? "✓" : "✕"} Handoff instruction matched
</li>
</ul>
<p><strong>Result:</strong> {testResult.result_path}</p>
<p><strong>Reason:</strong> {testResult.reason}</p>
</div>
)}
</div>
<footer className="handoff-drawer-foot">
<button type="button" className="handoff-btn subtle" onClick={() => setTestOpen(false)}>Close</button>
<button type="button" className="handoff-btn primary" disabled={testing} onClick={runTest}>
<AgentSpark /> {testing ? "Testing…" : "Run test"}
</button>
</footer>
</aside>
</div>
)}
</div>
);
}