responsiveness fix

This commit is contained in:
debu-2 2026-10-06 21:03:37 +05:30
parent 4ad05bf137
commit f7a4753217
9 changed files with 396 additions and 13 deletions

View file

@ -58,6 +58,7 @@ from app.services.reply_extractor import (
)
from app.services.thread_service import (
attach_message_to_thread,
create_thread,
message_event_at,
threading_fields_from_parent,
)
@ -102,8 +103,8 @@ def build_handoff_subject(
del rule_name
subject = " · ".join(
[
_safe_subject_part(sender_name, "unknown sender"),
"handoff",
_safe_subject_part(sender_name, "unknown sender"),
_safe_subject_part(external_sender, "unknown sender"),
_safe_subject_part(external_subject, "(No subject)"),
]
@ -111,6 +112,48 @@ def build_handoff_subject(
return subject[:998].rstrip()
_EMAIL_PATTERN = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
_SUBJECT_PREFIX = re.compile(r"^(?:re|fw|fwd)\s*:\s*", re.IGNORECASE)
_HANDOFF_OPENER = re.compile(
r"^handoff(?:\s*·\s*|\s*\|\s*|\s+-\s+)",
re.IGNORECASE,
)
_HANDOFF_SEPARATOR = re.compile(r"\s*·\s*|\s*\|\s*|\s+-\s+")
def parse_human_initiated_subject(subject: str | None) -> dict | None:
"""Read `handoff · Name · email · subject` from a human-started email.
The word handoff must come first. The three parts after it are the
customer name, the customer email, and the subject the customer should
see. Separate them with ·, |, or a spaced hyphen.
"""
cleaned = re.sub(r"[\r\n]+", " ", subject or "").strip()
while True:
without_prefix = _SUBJECT_PREFIX.sub("", cleaned, count=1).strip()
if without_prefix == cleaned:
break
cleaned = without_prefix
opener = _HANDOFF_OPENER.match(cleaned)
if opener is None:
return None
parts = _HANDOFF_SEPARATOR.split(cleaned[opener.end() :], maxsplit=2)
if len(parts) < 3:
return None
customer_name = parts[0].strip()
customer_email = parts[1].strip().lower()
customer_subject = parts[2].strip()
if not customer_name or not customer_subject:
return None
if _EMAIL_PATTERN.fullmatch(customer_email) is None:
return None
return {
"customer_name": customer_name,
"customer_email": customer_email,
"subject": customer_subject,
}
def redact_human_address(
value: str | None,
*,
@ -304,6 +347,223 @@ async def resolve_handoff_from_inbound(
return None
async def start_human_initiated_handoff(
database,
*,
account_id: str,
agent_email: str,
parsed: dict,
body_text: str,
body_html: str | None,
raw_mime: dict,
delivery_key: str,
) -> bool:
"""Send the first customer email when a Human Owner writes to Gentic.
Subject shape: handoff · Customer name · customer@email · Subject
The customer receives a normal new email. Later replies from that
customer are mirrored back on the same handoff thread.
"""
initiated = parse_human_initiated_subject(parsed.get("subject"))
if initiated is None:
return False
sender = parsed.get("sender") or {}
human_email = (sender.get("address") or "").strip().lower()
if not human_email:
return False
rules = (
await database.human_handoff_rules.find(
{
"account_id": account_id,
"owner_email": human_email,
"active": True,
}
)
.sort([("priority", 1), ("created_at", 1), ("_id", 1)])
.to_list(length=1)
)
if not rules:
return False
rule = rules[0]
delivery_marker = f"handoff-init:{account_id}:{delivery_key}"
if await database.messages.find_one({"handoff_delivery_key": delivery_marker}):
return True
extracted = extract_new_reply_content(body_text=body_text, body_html=body_html)
rich = None
try:
raw = await get_raw_mime_storage().get(raw_mime)
rich = extract_new_reply_mime_content(raw)
except Exception:
logger.exception(
"handoff_initiate_rich_extract_failed delivery=%s",
delivery_key,
)
new_text = (
rich.body_text if rich and rich.body_text else extracted["new_reply_content"]
).strip()
new_html = rich.body_html if rich and rich.body_html else None
new_text = (
redact_human_address(
new_text,
human_email=human_email,
agent_email=agent_email,
)
or ""
).strip()
new_html = redact_human_address(
new_html,
human_email=human_email,
agent_email=agent_email,
)
if not new_text or not validate_external_reply_body(new_text):
logger.info(
"handoff_initiate_not_sent account_id=%s reason=empty_or_private_body",
account_id,
)
return False
_, domain, sender_name = await _account_and_domain(account_id, agent_email)
customer_name = initiated["customer_name"]
customer_email = initiated["customer_email"]
customer_subject = initiated["subject"]
internet_message_id = await deliver_email(
sender=agent_email,
sender_name=sender_name,
recipients=[customer_email],
cc=[],
bcc=[],
subject=customer_subject,
body_text=new_text,
body_html=new_html,
domain=domain,
reply_to=agent_email,
envelope_sender=agent_email,
inline_parts=getattr(rich, "inline_parts", None),
file_attachments=getattr(rich, "file_attachments", None),
)
now = _now()
normalized_id = canonical_message_id(internet_message_id)
external = {
"direction": "outbound",
"message_scope": MESSAGE_SCOPE_EXTERNAL,
"message_origin": MESSAGE_ORIGIN_HUMAN_OWNER,
"generated_by": GENERATED_BY_HANDOFF_BRIDGE,
"account_id": account_id,
"sender": {"name": sender_name, "address": agent_email.lower()},
"to": [{"name": customer_name, "address": customer_email}],
"cc": [],
"bcc": [],
"subject": customer_subject,
"snippet": _snippet(new_text),
"new_reply_content": new_text,
"body_text": new_text,
"body_html": new_html,
"attachments": [],
"created_at": now,
"sent_at": now,
"status": "accepted",
"thread_relation": "new_thread",
"thread_resolution_method": "human_handoff_initiate",
**message_id_fields(normalized_id),
}
inserted = await database.messages.insert_one(external)
external["_id"] = inserted.inserted_id
thread_id = await create_thread(
database,
account_id=account_id,
subject=customer_subject,
message_id=external["_id"],
event_at=now,
)
await database.mailbox_entries.insert_one(
{
"account_id": account_id,
"message_id": external["_id"],
"thread_id": thread_id,
"mailbox": "sent",
"message_scope": MESSAGE_SCOPE_EXTERNAL,
"direction": "outbound",
"sender": external["sender"],
"recipients": external["to"],
"subject": customer_subject,
"snippet": external["snippet"],
"attachments": [],
"status": "accepted",
"is_read": True,
"is_starred": False,
"event_at": now,
"created_at": now,
"trashed_at": None,
"deletion_state": None,
}
)
handoff = await _create_handoff_mapping(
database,
account_id=account_id,
agent_email=agent_email,
external_message={
"_id": external["_id"],
"sender": {"name": customer_name, "address": customer_email},
"subject": customer_subject,
"message_id": normalized_id,
},
external_thread_id=thread_id,
rule=rule,
)
human_message = await store_handoff_message(
database,
account_id=account_id,
thread_id=handoff["handoff_thread_id"],
direction="inbound",
sender={"name": sender.get("name") or "", "address": human_email},
recipients=[{"name": sender_name, "address": agent_email.lower()}],
subject=handoff["private_subject"],
body_text=body_text,
body_html=body_html,
internet_message_id=parsed.get("message_id"),
in_reply_to=parsed.get("in_reply_to"),
references=parsed.get("references") or [],
handoff_id=handoff["_id"],
event_at=parsed.get("sent_at") or now,
raw_mime=raw_mime,
handoff_delivery_key=delivery_marker,
new_reply_content=new_text,
new_reply_html=new_html,
quoted_content=extracted.get("quoted_content", ""),
message_origin=MESSAGE_ORIGIN_HUMAN_OWNER,
generated_by=GENERATED_BY_HUMAN,
relay_status=RELAY_STATUS_SENT,
)
await database.messages.update_one(
{"_id": human_message["_id"]},
{"$set": {"generated_external_message_id": external["_id"]}},
)
await database.messages.update_one(
{"_id": external["_id"]},
{"$set": {"generated_from_handoff_message_id": human_message["_id"]}},
)
await database.human_handoffs.update_one(
{"_id": handoff["_id"]},
{
"$set": {
"status": HANDOFF_STATUS_HUMAN_REPLIED,
"current_owner": OWNER_AGENT,
"last_human_message_at": now,
"updated_at": now,
}
},
)
logger.info(
"handoff_initiated account_id=%s handoff_id=%s customer=%s",
account_id,
handoff["_id"],
customer_email,
)
return True
async def _record_link(
database,
*,

View file

@ -19,6 +19,7 @@ from app.services.human_handoff_service import (
maybe_trigger_handoff,
process_human_reply,
resolve_handoff_from_inbound,
start_human_initiated_handoff,
)
from app.services.message_ids import canonical_message_id, message_id_fields
from app.services.mime_parser import parse_mime
@ -154,6 +155,23 @@ class InboundHandler:
references=parsed.get("references", []),
)
if handoff is None:
started = await start_human_initiated_handoff(
mongo_db,
account_id=account_id,
agent_email=recipient,
parsed=parsed,
body_text=body_text,
body_html=body_html,
raw_mime=raw_mime,
delivery_key=delivery_key,
)
if started:
logger.info(
"handoff_initiated_from_human account_id=%s message_id=%s",
account_id,
internet_message_id,
)
continue
external_recipients.append(recipient)
continue
await process_human_reply(

View file

@ -17,6 +17,9 @@ TEXT_QUOTE_MARKERS = [
PRIVATE_LEAK_MARKERS = [
"human handoff",
" · handoff · ",
"handoff · ",
"handoff - ",
"handoff | ",
"a conversation needs your attention",
"why this was handed off",
"why you're receiving this",

View file

@ -282,7 +282,7 @@ def test_private_subject_stays_off_the_external_plane():
external_sender="customer@example.com",
external_subject="Invoice 42",
)
assert private.startswith("Parth Dixit · handoff ·")
assert private.startswith("handoff · Parth Dixit ·")
assert "Invoice 42" in private
assert "Finance approvals" not in private
@ -332,7 +332,7 @@ async def test_first_matching_rule_creates_a_new_linked_handoff():
assert result["rule_name"] == "Finance approvals"
assert result["external_thread_id"] == external_thread_id
assert result["handoff_thread_id"]
assert result["private_subject"].startswith("Parth Dixit · handoff ·")
assert result["private_subject"].startswith("handoff · Parth Dixit ·")
assert database.handoff_delivery_jobs.items[0]["kind"] == "MIRROR_TO_HUMAN"

View file

@ -7,6 +7,7 @@ from app.services.human_handoff_service import (
_mirror_from_identity,
build_handoff_subject,
maybe_trigger_handoff,
parse_human_initiated_subject,
process_human_reply,
redact_human_address,
resolve_handoff_from_inbound,
@ -97,6 +98,32 @@ def test_mirror_from_stays_on_gentic_address():
assert agent_address == "suyash@gentic.email"
def test_human_initiated_subject_accepts_dot_hyphen_and_bar():
assert parse_human_initiated_subject(
"handoff · Suyash Dixit · suyashdixit091@gmail.com · hello"
) == {
"customer_name": "Suyash Dixit",
"customer_email": "suyashdixit091@gmail.com",
"subject": "hello",
}
assert parse_human_initiated_subject(
"handoff - Suyash Dixit - suyashdixit091@gmail.com - Need a quote"
)["subject"] == "Need a quote"
parsed = parse_human_initiated_subject(
"Re: handoff | Suyash Dixit | SuyashDixit091@gmail.com | hello - there"
)
assert parsed["customer_email"] == "suyashdixit091@gmail.com"
assert parsed["customer_name"] == "Suyash Dixit"
assert parsed["subject"] == "hello - there"
def test_human_initiated_subject_rejects_incomplete_patterns():
assert parse_human_initiated_subject("Suyash Dixit · handoff · a@b.com · hello") is None
assert parse_human_initiated_subject("handoff · Suyash Dixit · not-an-email · hello") is None
assert parse_human_initiated_subject("handoff-Suyash Dixit-a@b.com-hello") is None
assert parse_human_initiated_subject("hello from the customer") is None
def test_private_subject_contains_customer_name_and_external_subject():
subject = build_handoff_subject(
sender_name="Parth Dixit",
@ -104,7 +131,7 @@ def test_private_subject_contains_customer_name_and_external_subject():
external_subject="Invoice 42",
)
assert subject == (
"Parth Dixit · handoff · parthdixit091@gmail.com · Invoice 42"
"handoff · Parth Dixit · parthdixit091@gmail.com · Invoice 42"
)

View file

@ -12,6 +12,7 @@ import {
LogOut,
Mail,
Menu,
PanelLeftOpen,
HandHelping,
Plug,
Send,
@ -173,10 +174,16 @@ export default function DashboardPage() {
window.location.assign("/login");
}
function closeMobileMenu() {
if (window.matchMedia("(max-width: 767px)").matches) {
setMenuOpen(false);
}
}
function selectCompanyItem(label) {
setCompanyActive(label);
setAgentNav(null);
setMenuOpen(false);
closeMobileMenu();
setSheet(null);
}
@ -193,7 +200,7 @@ export default function DashboardPage() {
setActive(item);
setLens("all");
}
setMenuOpen(false);
closeMobileMenu();
setSheet(null);
}
@ -202,7 +209,7 @@ export default function DashboardPage() {
setAgentNav(null);
setActive("Inbox");
setLens(name);
setMenuOpen(false);
closeMobileMenu();
setSheet(null);
}
@ -211,7 +218,7 @@ export default function DashboardPage() {
setActive("Inbox");
setLens("all");
setAgentNav(actionKey);
setMenuOpen(false);
closeMobileMenu();
const compact = window.matchMedia("(max-width: 1199px)").matches;
setSheet(actionKey === "handoff" || !compact ? null : "agent");
}
@ -221,7 +228,7 @@ export default function DashboardPage() {
setActive("Inbox");
setLens("all");
setAgentNav(actionKey);
setMenuOpen(false);
closeMobileMenu();
setSheet("agent");
}
@ -229,7 +236,7 @@ export default function DashboardPage() {
setCompanyActive(null);
setActive("Inbox");
setLens("all");
setMenuOpen(false);
closeMobileMenu();
}
function openSheet(next) {
@ -326,7 +333,13 @@ export default function DashboardPage() {
<aside className={menuOpen ? "dashboard-sidebar open" : "dashboard-sidebar"}>
<div className="dashboard-brand-row">
<Brand />
<button aria-label="Close navigation" type="button" onClick={() => setMenuOpen(false)}><X size={19} /></button>
<button
aria-label={menuOpen ? "Collapse navigation" : "Expand navigation"}
type="button"
onClick={() => setMenuOpen((open) => !open)}
>
{menuOpen ? <X size={19} /> : <PanelLeftOpen size={19} />}
</button>
</div>
{!isOrgOnboarding && (

View file

@ -20,6 +20,20 @@ export default function HumanHandoff({ onOpenExternal }) {
<p className="handoff-page-lead">
Manage conversations that your Agent has handed over to a Human Owner.
</p>
<div className="handoff-start-help">
<strong>Start the first email yourself</strong>
<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 |.
</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.
</p>
</div>
</div>
</header>
<nav className="human-handoff-tabs" aria-label="Human Handoff">

View file

@ -19,13 +19,13 @@
padding:16px 8px 12px;
z-index:12;
}
.dashboard-brand-row { justify-content:center; padding:0; }
.dashboard-brand-row { flex-direction:column; justify-content:center; padding:0; gap:6px; }
.dashboard-brand-row .brand > span:not(.brand-mark),
.dashboard-compose span,
.dashboard-sidebar-section h2,
.dashboard-nav-item em,
.dashboard-profile > div { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; }
.dashboard-brand-row > button,
.dashboard-brand-row > button { display:grid; width:32px; height:32px; color:#c8c3d4; }
.dashboard-profile > button { display:none; }
.dashboard-compose { width:44px; min-height:40px; margin:16px auto 0; padding:0; }
.dashboard-nav-item { justify-content:center; padding:0; gap:0; }
@ -111,6 +111,50 @@
max-width:calc(100vw - 48px);
height:min(78vh,calc(100vh - 32px));
}
.dashboard:has(.dashboard-sidebar.open) { grid-template-columns:228px minmax(0,1fr); }
.dashboard-sidebar.open {
width:228px;
padding:var(--space-4) var(--space-3) var(--space-3);
}
.dashboard-sidebar.open .dashboard-brand-row { flex-direction:row; justify-content:space-between; padding:0 6px 2px; }
.dashboard-sidebar.open .dashboard-brand-row .brand > span:not(.brand-mark),
.dashboard-sidebar.open .dashboard-compose span,
.dashboard-sidebar.open .dashboard-sidebar-section h2,
.dashboard-sidebar.open .dashboard-nav-item span,
.dashboard-sidebar.open .dashboard-profile > div {
position:static;
width:auto;
height:auto;
clip:auto;
overflow:visible;
margin:0;
white-space:nowrap;
}
.dashboard-sidebar.open .dashboard-nav-item em {
position:static;
width:auto;
min-width:18px;
height:18px;
clip:auto;
overflow:visible;
margin-left:auto;
padding:0 6px;
}
.dashboard-sidebar.open .dashboard-sidebar-section h2 { margin:0 0 var(--space-2); padding:0 var(--space-2); }
.dashboard-sidebar.open .dashboard-compose {
width:calc(100% - 8px);
margin:var(--space-4) var(--space-1) 0;
padding:0 var(--space-4);
}
.dashboard-sidebar.open .dashboard-nav-item { justify-content:flex-start; padding:0 var(--space-3); gap:var(--space-3); }
.dashboard-sidebar.open .dashboard-nav-item span { position:static; width:auto; height:auto; clip:auto; overflow:visible; flex:1; min-width:0; }
.dashboard-sidebar.open .dashboard-profile {
grid-template-columns:34px minmax(0,1fr) 31px;
padding-left:5px;
padding-right:5px;
}
.dashboard-sidebar.open .dashboard-profile > button { display:grid; }
}
@media (max-width:767px) {

View file

@ -560,6 +560,10 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
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-start-example { color:#3d3948; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12px; }
.human-handoff-tabs {
margin-top:24px;
height:38px;