handoff ui improvement

This commit is contained in:
debu-2 2026-10-06 21:22:41 +05:30
parent f7a4753217
commit d4bb4df40d
4 changed files with 151 additions and 126 deletions

View file

@ -1,4 +1,4 @@
import { ArrowDown, ArrowUp, MoreHorizontal, Plus, UserRound } from "lucide-react";
import { MoreHorizontal, UserRound } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import {
createHandoffRule,
@ -10,13 +10,13 @@ import {
} from "../api";
import AgentSpark from "./AgentSpark";
import HumanHandoffDrawer from "./HumanHandoffDrawer";
import { buildRuleCardChips, EMAIL_TYPE_OPTIONS } from "../utils/handoffUtils";
import { buildRuleCardChips } from "../utils/handoffUtils";
function typeLabel(id) {
return EMAIL_TYPE_OPTIONS.find((item) => item.id === id)?.title || id;
function ruleScope(rule) {
return buildRuleCardChips(rule).filter((chip) => chip !== rule.name).join(" · ");
}
export default function HandoffSettings() {
export default function HandoffSettings({ createToken = 0 }) {
const token = sessionStorage.getItem("agmail_access_token");
const [rules, setRules] = useState([]);
const [loading, setLoading] = useState(true);
@ -50,10 +50,11 @@ export default function HandoffSettings() {
loadRules();
}, [loadRules]);
function openCreate() {
useEffect(() => {
if (!createToken) return;
setEditingRule(null);
setDrawerOpen(true);
}
}, [createToken]);
function openEdit(rule) {
setEditingRule(rule);
@ -145,10 +146,9 @@ export default function HandoffSettings() {
return (
<div className="handoff-settings">
<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 className="handoff-section-head">
<h2>Handoff rules</h2>
<p>Choose who can take over conversations from your Agent.</p>
</div>
{error && <p className="form-error" role="alert">{error}</p>}
@ -159,48 +159,32 @@ export default function HandoffSettings() {
) : rules.length === 0 ? (
<div className="human-handoff-empty">
<div className="human-handoff-empty-icon" aria-hidden="true">
<UserRound size={24} strokeWidth={1.75} />
<UserRound size={20} 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>
<p>Add a human owner so your Agent knows who can take over a conversation.</p>
</div>
) : (
<div className="human-handoff-list">
{rules.map((rule) => (
{rules.map((rule, index) => (
<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} />
<UserRound size={15} strokeWidth={1.75} />
</span>
<div className="human-handoff-card-identity">
<strong className="human-handoff-card-name">{rule.name}</strong>
<small className="human-handoff-card-email">
{index === 0 ? "Primary human owner" : "Human owner"}
{" · "}
{rule.owner_email}
</small>
</div>
<p className="human-handoff-card-scope">{ruleScope(rule)}</p>
<div className="human-handoff-card-actions">
<span className={`human-handoff-status${rule.active ? " active" : ""}`}>
{rule.active ? "Active" : "Paused"}
</span>
<small className="human-handoff-card-email">
Priority {rule.priority} · {rule.owner_name} · {rule.owner_email}
</small>
<div className="human-handoff-card-actions">
<button
type="button"
className="handoff-btn icon quiet"
aria-label="Move rule up"
disabled={rules[0]?.id === rule.id}
onClick={() => moveRule(rule.id, -1)}
>
<ArrowUp size={15} strokeWidth={1.75} />
</button>
<button
type="button"
className="handoff-btn icon quiet"
aria-label="Move rule down"
disabled={rules[rules.length - 1]?.id === rule.id}
onClick={() => moveRule(rule.id, 1)}
>
<ArrowDown size={15} strokeWidth={1.75} />
</button>
<button type="button" className="handoff-btn subtle" onClick={() => openEdit(rule)}>
Edit
</button>
@ -215,6 +199,8 @@ export default function HandoffSettings() {
</button>
{menuOpenId === rule.id && (
<div className="handoff-card-menu">
<button type="button" disabled={index === 0} onClick={() => moveRule(rule.id, -1)}>Move up</button>
<button type="button" disabled={index === rules.length - 1} onClick={() => moveRule(rule.id, 1)}>Move down</button>
<button type="button" onClick={() => openTest({
owner_email: rule.owner_email,
labels: rule.labels,
@ -227,11 +213,6 @@ export default function HandoffSettings() {
</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>

View file

@ -1,3 +1,4 @@
import { ChevronDown, Plus } from "lucide-react";
import { useState } from "react";
import HandoffConversation from "./HandoffConversation";
import HandoffInbox from "./HandoffInbox";
@ -6,36 +7,65 @@ import HandoffSettings from "./HandoffSettings";
export default function HumanHandoff({ onOpenExternal }) {
const [tab, setTab] = useState("inbox");
const [selectedHandoffId, setSelectedHandoffId] = useState(null);
const [howOpen, setHowOpen] = useState(false);
const [createToken, setCreateToken] = useState(0);
function selectTab(value) {
setTab(value);
setSelectedHandoffId(null);
}
function addRule() {
setTab("rules");
setSelectedHandoffId(null);
setCreateToken((token) => token + 1);
}
return (
<section className="human-handoff-page">
<header className="human-handoff-head">
<div>
<h1 className="handoff-page-title">Human Handoff</h1>
<p className="handoff-page-lead">
Manage conversations that your Agent has handed over to a Human Owner.
When a customer emails your Gentic address, your Agent can hand that conversation to a person.
The person replies from their own inbox. The customer still only sees mail from Gentic.
</p>
<div className="handoff-start-help">
<strong>Start the first email yourself</strong>
<button
type="button"
className={`handoff-how-toggle${howOpen ? " open" : ""}`}
aria-expanded={howOpen}
onClick={() => setHowOpen((open) => !open)}
>
How it works
<ChevronDown size={14} strokeWidth={1.75} />
</button>
{howOpen && (
<div className="handoff-how">
<section>
<h2>When a customer writes to you</h2>
<p>
From the human mailbox, send a new email to your Gentic address.
Put the word handoff first. Then add the customer’s name, their email address, and the subject they should see.
Separate those four parts with a dot · , a hyphen - , or a bar |.
A customer emails your Gentic address. If one of your rules matches, Gentic sends that conversation to the human owner.
The owner replies to that email. Gentic then sends the reply to the customer as a normal message from your Gentic address.
The customer never sees the human’s own email address.
</p>
</section>
<section>
<h2>When you write first</h2>
<p>
Only the human owner on an active rule can start a conversation. From that mailbox, send a new email to your Gentic address.
Put handoff first, then the customer’s name, their email, and the subject they should see.
Separate the parts with a middle dot (·), a hyphen (-), or a bar (|).
</p>
<p className="handoff-start-example">handoff · Suyash Dixit · suyashdixit091@gmail.com · hello</p>
<p>
Gentic sends that as a normal first email. The customer only sees “hello”, from your Gentic address.
Your From address must be the human email saved on an active handoff rule.
When the customer replies to Gentic, that reply is mirrored back to you the same way as any other handoff.
Gentic sends a normal first email. The customer only sees “hello”, from your Gentic address.
When they reply, it comes back to the human owner the same way as any other handoff.
</p>
</section>
</div>
</div>
)}
</header>
<div className="handoff-toolbar-row">
<nav className="human-handoff-tabs" aria-label="Human Handoff">
<button
type="button"
@ -59,8 +89,13 @@ export default function HumanHandoff({ onOpenExternal }) {
Rules
</button>
</nav>
<button type="button" className="handoff-btn primary" onClick={addRule}>
<Plus size={16} strokeWidth={1.75} /> Add handoff rule
</button>
</div>
{tab === "rules" ? (
<HandoffSettings />
<HandoffSettings createToken={createToken} />
) : selectedHandoffId ? (
<HandoffConversation
handoffId={selectedHandoffId}

View file

@ -198,12 +198,14 @@
.human-handoff-page { max-width:none; margin:0; padding:0; }
.handoff-page-title,
.human-handoff-head h1.handoff-page-title { white-space:normal; font-size:22px; }
.handoff-toolbar-row { align-items:stretch; flex-direction:column; }
.handoff-toolbar-row .handoff-btn.primary { align-self:flex-start; }
.human-handoff-card-body {
grid-template-areas:"avatar name status" "avatar email email" "actions actions actions";
grid-template-columns:36px minmax(0,1fr) auto;
grid-template-areas:"avatar identity" "scope scope" "actions actions";
grid-template-columns:32px minmax(0,1fr);
row-gap:8px;
}
.human-handoff-card-actions { margin-top:8px; flex-wrap:wrap; justify-content:flex-end; }
.human-handoff-card-chips { padding-left:0; }
.human-handoff-card-actions { justify-self:start; }
.handoff-drawer,.handoff-test-modal,.people-drawer { width:100%; max-width:100%; }
.mailbox-workspace { gap:0; }

View file

@ -533,7 +533,7 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
.security-toggle span { position:absolute; inset:0; border-radius:999px; background:#D9D5E0; transition:background .2s; cursor:pointer; }
.security-toggle span::after { content:""; position:absolute; top:3px; left:3px; width:18px; height:18px; border-radius:50%; background:#fff; box-shadow:0 1px 3px rgba(20,18,30,0.18); transition:transform .2s; }
.security-toggle input:checked + span { background:#7357F6; }
.human-handoff-page { max-width:900px; margin:0; padding:8px 4px 32px; }
.human-handoff-page { max-width:960px; width:100%; margin:0; padding:4px 0 32px; }
.human-handoff-head { margin:0; }
.handoff-page-title,
.human-handoff-head h1.handoff-page-title {
@ -553,55 +553,60 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
}
.handoff-page-lead,
.human-handoff-head p.handoff-page-lead {
margin:5px 0 0;
max-width:640px;
font-size:12px;
line-height:1.45;
margin:8px 0 0;
max-width:none;
font-size:13px;
line-height:1.5;
font-weight:400;
color:#77758A;
}
.handoff-start-help { max-width:640px; margin-top:14px; padding:12px 14px; border:1px solid #e8e5ec; border-radius:12px; background:#fff; }
.handoff-start-help strong { display:block; color:#171424; font-size:13px; }
.handoff-start-help p { margin:6px 0 0; color:#77758a; font-size:12px; line-height:1.5; }
.handoff-how-toggle { margin:8px 0 0; padding:0; display:inline-flex; align-items:center; gap:4px; border:0; background:transparent; color:#6d5bd0; font-size:12px; font-weight:650; }
.handoff-how-toggle svg { transition:transform .15s ease; }
.handoff-how-toggle.open svg { transform:rotate(180deg); }
.handoff-how { margin-top:10px; display:grid; gap:12px; }
.handoff-how section { padding:12px 14px; border:1px solid #E7E4EA; border-radius:10px; background:#fff; }
.handoff-how h2 { margin:0; font:650 13px/1.3 "DM Sans"; color:#171424; }
.handoff-how p { margin:4px 0 0; color:#77758a; font-size:12px; line-height:1.5; }
.handoff-start-example { color:#3d3948; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12px; }
.handoff-toolbar-row { margin-top:20px; display:flex; align-items:center; justify-content:space-between; gap:12px; }
.human-handoff-tabs {
margin-top:24px;
height:38px;
margin-top:0;
height:36px;
padding:3px;
display:inline-flex;
align-items:center;
gap:0;
border:1px solid #E6E2EA;
border:1px solid #E7E4EA;
border-radius:9px;
background:#FAF9FB;
}
.human-handoff-tabs button {
height:30px;
height:28px;
padding:0 14px;
border:0;
border-radius:6px;
background:transparent;
color:#777286;
font-size:11px;
font-size:12px;
font-weight:550;
}
.human-handoff-tabs button.active {
background:#FFFFFF;
color:#252130;
box-shadow:0 1px 2px rgba(20,18,30,.06);
box-shadow:none;
}
.handoff-inbox, .handoff-settings, .handoff-conversation { margin-top:22px; }
.handoff-inbox, .handoff-settings, .handoff-conversation { margin-top:20px; }
.handoff-section-head { margin:0 0 12px; }
.handoff-section-head h2 {
margin:0;
font-size:13px;
font-size:14px;
font-weight:650;
line-height:1.3;
color:#292536;
}
.handoff-section-head p {
margin:3px 0 0;
font-size:10.5px;
margin:2px 0 0;
font-size:12px;
font-weight:400;
line-height:1.4;
color:#8A8696;
@ -725,28 +730,30 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
.handoff-settings-notice p { margin:0; color:#77758a; font-size:11.5px; line-height:1.45; }
.handoff-flow-diagram { margin:14px 0 10px; padding:14px; display:grid; justify-items:center; gap:4px; border:1px dashed #ddd4ff; border-radius:10px; background:#fff; color:#4a4652; font-size:12px; font-weight:600; }
.handoff-flow-arrow { color:#9a96a5; font-weight:400; }
.handoff-settings-toolbar { margin:0 0 14px; display:flex; justify-content:flex-end; }
.handoff-settings-toolbar { display:none; }
.human-handoff-empty,.human-handoff-list { margin-top:0; }
.human-handoff-empty { padding:40px 28px; display:grid; justify-items:center; text-align:center; border:1px solid #e8e5ec; border-radius:12px; background:#fff; box-shadow:0 1px 2px rgba(20,18,30,.025); }
.human-handoff-empty-icon { width:52px; height:52px; margin-bottom:14px; display:grid; place-items:center; border-radius:14px; background:#f3f0fa; color:#6d5bd0; }
.human-handoff-empty h2 { margin:0 0 8px; font:700 18px/1.2 "DM Sans"; }
.human-handoff-empty p { margin:0 0 18px; max-width:420px; color:#77758a; font-size:12px; line-height:1.55; }
.human-handoff-list { display:grid; gap:10px; }
.human-handoff-card { padding:14px 16px; border:1px solid #e8e5ec; border-radius:12px; background:#fff; box-shadow:0 1px 2px rgba(20,18,30,.025); }
.human-handoff-card-body { display:grid; grid-template-areas:"avatar name status" "avatar email actions"; grid-template-columns:36px minmax(0,1fr) auto; column-gap:12px; row-gap:2px; align-items:center; }
.human-handoff-card-avatar { grid-area:avatar; width:36px; height:36px; display:grid; place-items:center; border-radius:50%; background:#f3f0fa; color:#5f4db8; align-self:center; }
.human-handoff-card-name { grid-area:name; font:650 14px/1.25 "DM Sans"; color:#171424; }
.human-handoff-card-email { grid-area:email; color:#77758a; font-size:11px; line-height:1.3; }
.human-handoff-status { grid-area:status; height:20px; padding:0 7px; display:inline-flex; align-items:center; justify-self:end; border-radius:999px; background:#f1eff3; color:#716d7a; font-size:9.5px; font-weight:600; }
.human-handoff-empty { padding:28px 20px; display:grid; justify-items:center; text-align:center; border:1px solid #E7E4EA; border-radius:10px; background:#fff; box-shadow:none; }
.human-handoff-empty-icon { width:40px; height:40px; margin-bottom:10px; display:grid; place-items:center; border-radius:12px; background:#f6f4fb; color:#6d5bd0; }
.human-handoff-empty h2 { margin:0 0 4px; font:650 15px/1.3 "DM Sans"; }
.human-handoff-empty p { margin:0; max-width:420px; color:#77758a; font-size:12px; line-height:1.5; }
.human-handoff-list { display:grid; gap:8px; }
.human-handoff-card { padding:10px 12px; border:1px solid #E7E4EA; border-radius:10px; background:#fff; box-shadow:none; }
.human-handoff-card-body { display:grid; grid-template-areas:"avatar identity scope actions"; grid-template-columns:32px minmax(160px,1fr) minmax(140px,1.2fr) auto; column-gap:12px; align-items:center; }
.human-handoff-card-avatar { grid-area:avatar; width:32px; height:32px; display:grid; place-items:center; border-radius:50%; background:#f6f4fb; color:#5f4db8; }
.human-handoff-card-identity { grid-area:identity; min-width:0; display:grid; gap:1px; }
.human-handoff-card-name { font:650 13px/1.25 "DM Sans"; color:#171424; }
.human-handoff-card-email { color:#77758a; font-size:11px; line-height:1.3; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.human-handoff-card-scope { grid-area:scope; margin:0; color:#6f6b7c; font-size:12px; line-height:1.35; }
.human-handoff-status { height:22px; padding:0 8px; display:inline-flex; align-items:center; border-radius:999px; background:#f4f2f6; color:#716d7a; font-size:11px; font-weight:600; }
.human-handoff-status.active { background:#edf9f4; color:#11966a; }
.human-handoff-card-actions { grid-area:actions; display:flex; align-items:center; justify-self:end; gap:4px; }
.human-handoff-card-chips { display:flex; flex-wrap:wrap; gap:6px; margin-top:10px; padding-left:48px; }
.human-handoff-card-actions { grid-area:actions; display:flex; align-items:center; justify-self:end; gap:6px; }
.handoff-meta-chip { height:21px; padding:0 8px; display:inline-flex; align-items:center; border-radius:6px; background:#f4f2f8; color:#6f6b7c; font-size:10.5px; font-weight:500; white-space:nowrap; }
.human-handoff-card-quote { margin:0 0 14px; padding-left:12px; border-left:2px solid #ece7ff; color:#4a4652; font-size:12px; line-height:1.55; }
.handoff-card-menu-wrap { position:relative; }
.handoff-card-menu { position:absolute; right:0; top:calc(100% + 4px); min-width:160px; padding:6px; display:grid; gap:2px; border:1px solid #ece9f2; border-radius:10px; background:#fff; box-shadow:0 8px 24px rgba(20,18,30,0.12); z-index:5; }
.handoff-card-menu button { width:100%; min-height:34px; padding:0 10px; border:0; border-radius:8px; background:transparent; text-align:left; font-size:12px; font-weight:600; color:#4a4652; }
.handoff-card-menu button:hover { background:#f7f5fa; }
.handoff-card-menu button:disabled { color:#b7b3be; cursor:not-allowed; }
.handoff-btn { height:36px; padding:0 14px; display:inline-flex; align-items:center; justify-content:center; gap:7px; border:1px solid #dfdde5; border-radius:9px; background:#fff; color:#241f2b; font-size:12px; font-weight:600; cursor:pointer; transition:background .12s ease; }
.handoff-btn:hover:not(:disabled) { background:#f7f5fa; }
.handoff-btn.primary { border-color:#6d5bd0; background:#7357f6; color:#fff; }