handoff ui improvement
This commit is contained in:
parent
f7a4753217
commit
d4bb4df40d
4 changed files with 151 additions and 126 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue