diff --git a/backend/app/routers/agent_handoff.py b/backend/app/routers/agent_handoff.py
index adaf7eb..e54b5de 100644
--- a/backend/app/routers/agent_handoff.py
+++ b/backend/app/routers/agent_handoff.py
@@ -264,13 +264,23 @@ async def get_handoff_conversation(
sender_address = (message.get("sender") or {}).get("address", "").lower()
role = "human" if sender_address == human_email else "agent"
event_at = row.get("event_at") or message.get("received_at") or message.get("sent_at") or message["created_at"]
+ raw_body = message.get("raw_body")
+ new_reply_content = message.get("new_reply_content")
+ display_body = new_reply_content or message.get("body_text", "")
messages.append(
HandoffMessageRead(
id=str(message["_id"]),
role=role,
subject=message.get("subject", ""),
- snippet=message.get("snippet", ""),
- body_text=message.get("body_text", ""),
+ snippet=message.get("snippet", "") or display_body[:240],
+ body_text=display_body,
+ new_reply_content=new_reply_content,
+ raw_body=raw_body,
+ has_quoted_text=bool(
+ raw_body
+ and new_reply_content
+ and raw_body.strip() != new_reply_content.strip()
+ ),
event_at=event_at,
)
)
diff --git a/backend/app/schemas.py b/backend/app/schemas.py
index 756c055..3b6b684 100644
--- a/backend/app/schemas.py
+++ b/backend/app/schemas.py
@@ -314,6 +314,9 @@ class HandoffMessageRead(BaseModel):
subject: str
snippet: str
body_text: str
+ new_reply_content: str | None = None
+ raw_body: str | None = None
+ has_quoted_text: bool = False
event_at: datetime
diff --git a/backend/app/services/handoff_email_templates.py b/backend/app/services/handoff_email_templates.py
new file mode 100644
index 0000000..73f7b7e
--- /dev/null
+++ b/backend/app/services/handoff_email_templates.py
@@ -0,0 +1,198 @@
+from html import escape
+
+DEBUG_REASON_MARKERS = (
+ "step passes",
+ "classifier",
+ "rule evaluation",
+ "no handoff instructions were set",
+)
+
+
+def _clean_external_subject(subject: str) -> str:
+ value = (subject or "(No subject)").strip()
+ for prefix in ("Re: ", "RE: ", "re: "):
+ if value.startswith(prefix):
+ value = value[len(prefix) :].strip()
+ for prefix in ("Human Handoff · ", "Human Handoff: "):
+ if value.lower().startswith(prefix.lower()):
+ value = value[len(prefix) :].strip()
+ return value or "(No subject)"
+
+
+def handoff_email_subject(external_subject: str) -> str:
+ return f"Human Handoff · {_clean_external_subject(external_subject)}"
+
+
+def external_reply_subject(external_subject: str) -> str:
+ cleaned = _clean_external_subject(external_subject)
+ return f"Re: {cleaned}"
+
+
+def format_reason_for_human(reason: str | None, *, is_update: bool = False) -> str | None:
+ if is_update:
+ return None
+ if not reason or not reason.strip():
+ return None
+ lower = reason.strip().lower()
+ if any(marker in lower for marker in DEBUG_REASON_MARKERS):
+ return "This conversation matched your Human Handoff settings."
+ if lower in {
+ "matched handoff rules.",
+ "new message in the linked conversation.",
+ }:
+ return "This conversation requires human attention."
+ return reason.strip()
+
+
+def build_initial_handoff_text(
+ *,
+ external_sender: str,
+ external_subject: str,
+ external_body: str,
+ agent_email: str,
+ handoff_reason: str | None,
+) -> str:
+ reason = format_reason_for_human(handoff_reason)
+ lines = [
+ "GENTIC — HUMAN HANDOFF",
+ "",
+ "This conversation needs your attention.",
+ "",
+ f"From: {external_sender}",
+ f"Subject: {_clean_external_subject(external_subject)}",
+ "",
+ "LATEST MESSAGE",
+ "",
+ external_body.strip(),
+ "",
+ ]
+ if reason:
+ lines.extend(["WHY YOU'RE RECEIVING THIS", "", reason, ""])
+ lines.extend(
+ [
+ "REPLY NORMALLY",
+ "",
+ f"Reply to this email and Gentic will send only your new reply to the "
+ f"original sender through {agent_email}.",
+ "",
+ "This private handoff conversation and quoted history will not be shared "
+ "with the sender.",
+ "",
+ "—",
+ "Gentic Human Handoff",
+ "Private Agent ↔ Human conversation",
+ ]
+ )
+ return "\n".join(lines)
+
+
+def build_initial_handoff_html(
+ *,
+ external_sender: str,
+ external_subject: str,
+ external_body: str,
+ agent_email: str,
+ handoff_reason: str | None,
+) -> str:
+ reason = format_reason_for_human(handoff_reason)
+ reason_block = ""
+ if reason:
+ reason_block = f"""
+
+ Why you're receiving this
+ {escape(reason)}
+ """
+
+ return f"""
+
+
+
+
+
+ ✦ Gentic
+ Human Handoff
+ This conversation needs your attention.
+
+
+
+
+ From
+ {escape(external_sender)}
+ Subject
+ {escape(_clean_external_subject(external_subject))}
+
+
+
+
+ Latest message
+ {escape(external_body.strip())}
+
+ {reason_block}
+
+
+
+ ↩ Reply normally
+
+ Reply to this email and Gentic will send your response to the original sender as
+ {escape(agent_email)} . Only the new text you write will be sent.
+ This handoff email and quoted history stay private.
+
+
+
+
+
+ Gentic Human Handoff Private Agent ↔ Human conversation
+
+
+
+
+"""
+
+
+def build_update_handoff_text(
+ *,
+ external_sender: str,
+ external_body: str,
+ agent_email: str,
+) -> str:
+ return "\n".join(
+ [
+ "Customer replied",
+ "",
+ external_sender,
+ "",
+ external_body.strip(),
+ "",
+ f"Reply normally to respond through {agent_email}.",
+ ]
+ )
+
+
+def build_update_handoff_html(
+ *,
+ external_sender: str,
+ external_body: str,
+ agent_email: str,
+) -> str:
+ return f"""
+
+
+
+
+
+ ✦ Gentic
+ Customer replied
+ {escape(external_sender)}
+ {escape(external_body.strip())}
+
+ Reply normally to respond through {escape(agent_email)} .
+
+
+
+
+
+"""
+
+
+def build_relay_confirmation_text(customer_body: str) -> str:
+ return f"Sent to customer:\n\n{customer_body.strip()}"
diff --git a/backend/app/services/human_handoff_service.py b/backend/app/services/human_handoff_service.py
index 6fa04a8..0155097 100644
--- a/backend/app/services/human_handoff_service.py
+++ b/backend/app/services/human_handoff_service.py
@@ -4,9 +4,19 @@ from datetime import datetime, timezone
from bson import ObjectId
+from app.services.handoff_email_templates import (
+ build_initial_handoff_html,
+ build_initial_handoff_text,
+ build_relay_confirmation_text,
+ build_update_handoff_html,
+ build_update_handoff_text,
+ external_reply_subject,
+ handoff_email_subject,
+)
from app.services.handoff_evaluator import evaluate_handoff
from app.services.mail_sender import deliver_email
-from app.services.message_ids import build_references_header, normalize_message_id, parse_message_id_list
+from app.services.message_ids import normalize_message_id, parse_message_id_list
+from app.services.reply_extractor import extract_new_reply_content, validate_external_reply_body
from app.services.thread_service import (
attach_message_to_thread,
message_event_at,
@@ -42,36 +52,8 @@ def _snippet(text: str, limit: int = 240) -> str:
return re.sub(r"\s+", " ", (text or "")).strip()[:limit]
-def _handoff_subject(external_subject: str) -> str:
- subject = (external_subject or "(No subject)").strip()
- if subject.lower().startswith("human handoff:"):
- return subject
- return f"Human Handoff: {subject}"
-
-
-def _build_handoff_body(
- *,
- external_sender: str,
- external_subject: str,
- external_body: str,
- handoff_reason: str,
- agent_email: str,
-) -> str:
- return (
- "Human Handoff\n\n"
- "A conversation needs your attention.\n\n"
- f"From\n{external_sender}\n\n"
- f"Subject\n{external_subject}\n\n"
- "Latest message\n\n"
- f"\"{external_body.strip()}\"\n\n"
- "Why this was handed off\n\n"
- f"{handoff_reason}\n\n"
- "────────────────────────────────────────\n\n"
- f"Reply to this email to respond to the original sender through {agent_email}.\n\n"
- "Your reply will be sent to the customer.\n\n"
- "────────────────────────────────────────\n\n"
- "Gentic Human Handoff"
- )
+def _handoff_reply_subject(external_subject: str) -> str:
+ return f"Re: {handoff_email_subject(external_subject)}"
async def create_empty_handoff_thread(
@@ -114,6 +96,47 @@ async def _record_message_link(
)
+async def _latest_external_customer_message(
+ database,
+ account_id: str,
+ external_thread_id: ObjectId,
+ customer_address: str,
+) -> dict | None:
+ customer = customer_address.lower()
+ pipeline = [
+ {
+ "$match": {
+ "account_id": account_id,
+ "thread_id": external_thread_id,
+ "message_scope": MESSAGE_SCOPE_EXTERNAL,
+ "deletion_state": {"$in": [None, ""]},
+ }
+ },
+ {"$sort": {"event_at": -1, "_id": -1}},
+ {
+ "$lookup": {
+ "from": "messages",
+ "localField": "message_id",
+ "foreignField": "_id",
+ "as": "message",
+ }
+ },
+ {"$unwind": "$message"},
+ {
+ "$match": {
+ "$or": [
+ {"message.direction": "inbound"},
+ {"message.sender.address": customer},
+ ]
+ }
+ },
+ {"$limit": 1},
+ ]
+ cursor = await database.mailbox_entries.aggregate(pipeline)
+ rows = [row async for row in cursor]
+ return rows[0]["message"] if rows else None
+
+
async def _latest_external_message(database, account_id: str, external_thread_id: ObjectId) -> dict | None:
pipeline = [
{
@@ -250,8 +273,12 @@ async def store_handoff_message(
link_relationship: str | None = None,
external_message_id: ObjectId | None = None,
event_at: datetime | None = None,
+ raw_body: str | None = None,
+ new_reply_content: str | None = None,
+ body_html: str | None = None,
) -> dict:
now = event_at or _now()
+ display_body = new_reply_content if new_reply_content is not None else body_text
document = {
"direction": direction,
"message_scope": MESSAGE_SCOPE_HANDOFF,
@@ -259,9 +286,11 @@ async def store_handoff_message(
"sender": sender,
"to": recipients,
"subject": subject,
- "snippet": _snippet(body_text),
- "body_text": body_text,
- "body_html": None,
+ "snippet": _snippet(display_body),
+ "body_text": display_body,
+ "raw_body": raw_body,
+ "new_reply_content": new_reply_content,
+ "body_html": body_html,
"attachments": [],
"created_at": now,
"sent_at": now if direction == "outbound" else None,
@@ -331,17 +360,21 @@ async def send_handoff_to_human(
external_subject = external_message.get("subject", "")
external_body = external_message.get("body_text") or external_message.get("snippet") or ""
+ stable_subject = handoff_email_subject(external_subject)
if existing_handoff:
handoff_id = existing_handoff["_id"]
handoff_thread_id = existing_handoff["handoff_thread_id"]
parent = await _latest_handoff_message(database, account_id, handoff_thread_id)
threading = threading_fields_from_parent(parent) if parent else {}
- subject = _handoff_subject(external_subject)
- body = _build_handoff_body(
+ subject = _handoff_reply_subject(external_subject)
+ body_text = build_update_handoff_text(
+ external_sender=external_sender_text,
+ external_body=external_body,
+ agent_email=agent_email,
+ )
+ body_html = build_update_handoff_html(
external_sender=external_sender_text,
- external_subject=external_subject,
external_body=external_body,
- handoff_reason="New message in the linked conversation.",
agent_email=agent_email,
)
relationship = LINK_EXTERNAL_UPDATE_RELAYED
@@ -349,11 +382,18 @@ async def send_handoff_to_human(
handoff_thread_id = await create_empty_handoff_thread(
database,
account_id=account_id,
- subject=_handoff_subject(external_subject),
+ subject=stable_subject,
)
threading = {}
- subject = _handoff_subject(external_subject)
- body = _build_handoff_body(
+ subject = stable_subject
+ body_text = build_initial_handoff_text(
+ external_sender=external_sender_text,
+ external_subject=external_subject,
+ external_body=external_body,
+ handoff_reason=handoff_reason,
+ agent_email=agent_email,
+ )
+ body_html = build_initial_handoff_html(
external_sender=external_sender_text,
external_subject=external_subject,
external_body=external_body,
@@ -386,8 +426,8 @@ async def send_handoff_to_human(
cc=[],
bcc=[],
subject=subject,
- body_text=body,
- body_html=None,
+ body_text=body_text,
+ body_html=body_html,
domain=domain,
in_reply_to=threading.get("in_reply_to"),
references=threading.get("references"),
@@ -402,7 +442,8 @@ async def send_handoff_to_human(
sender={"name": "Gentic Agent", "address": agent_email},
recipients=[{"name": human_email.split("@")[0], "address": human_email}],
subject=subject,
- body_text=body,
+ body_text=body_text,
+ body_html=body_html,
internet_message_id=internet_message_id,
in_reply_to=threading.get("in_reply_to"),
references=threading.get("references"),
@@ -458,6 +499,13 @@ async def process_human_reply(
rule = await _handoff_rule_for_human(database, account_id, human_email)
allow_replies = rule.get("allow_replies", True) if rule else True
+ extracted = extract_new_reply_content(
+ body_text=inbound_body,
+ body_html=inbound_parsed.get("body_html"),
+ )
+ new_reply_content = extracted["new_reply_content"].strip()
+ raw_body = extracted["raw_body"]
+
human_message = await store_handoff_message(
database,
account_id=account_id,
@@ -467,7 +515,10 @@ async def process_human_reply(
sender={"name": human_email.split("@")[0], "address": human_email},
recipients=[{"name": "Gentic Agent", "address": agent_email}],
subject=inbound_parsed.get("subject", ""),
- body_text=inbound_body,
+ body_text=raw_body,
+ raw_body=raw_body,
+ new_reply_content=new_reply_content,
+ body_html=inbound_parsed.get("body_html"),
internet_message_id=inbound_parsed.get("message_id"),
in_reply_to=inbound_parsed.get("in_reply_to"),
references=inbound_parsed.get("references", []),
@@ -490,21 +541,56 @@ async def process_human_reply(
)
return
- external_parent = await _latest_external_message(database, account_id, external_thread_id)
+ if not new_reply_content:
+ logger.warning(
+ "handoff_reply_empty_extract handoff_id=%s account=%s",
+ handoff["_id"],
+ account_id,
+ )
+ await database.human_handoffs.update_one(
+ {"_id": handoff["_id"]},
+ {
+ "$set": {
+ "status": HANDOFF_STATUS_HUMAN_REPLIED,
+ "current_owner": OWNER_AGENT,
+ "updated_at": now,
+ "last_human_message_at": now,
+ }
+ },
+ )
+ return
+
+ if not validate_external_reply_body(new_reply_content):
+ logger.error(
+ "handoff_reply_leakage_guard handoff_id=%s account=%s",
+ handoff["_id"],
+ account_id,
+ )
+ return
+
+ external_recipient = handoff.get("external_sender") or ""
+ external_parent = await _latest_external_customer_message(
+ database,
+ account_id,
+ external_thread_id,
+ external_recipient,
+ )
+ if external_parent is None:
+ external_parent = await _latest_external_message(database, account_id, external_thread_id)
if external_parent is None:
logger.warning("handoff_reply_missing_external_parent handoff_id=%s", handoff["_id"])
return
threading = threading_fields_from_parent(external_parent)
- external_subject = handoff.get("external_subject") or external_parent.get("subject", "")
- if not re.match(r"^re:\s*", external_subject, flags=re.IGNORECASE):
- external_subject = f"Re: {external_subject}"
+ external_subject = external_reply_subject(
+ handoff.get("external_subject") or external_parent.get("subject", "")
+ )
- external_recipient = handoff.get("external_sender") or external_parent.get("sender", {}).get("address")
+ external_recipient = external_recipient or external_parent.get("sender", {}).get("address")
if not external_recipient:
return
- customer_body = inbound_body.strip()
+ customer_body = new_reply_content
internet_message_id = await deliver_email(
sender=agent_email,
recipients=[external_recipient],
@@ -572,8 +658,8 @@ async def process_human_reply(
}
)
- relay_body = f"Sent to customer:\n\n\"{customer_body}\""
- relay_subject = f"Re: {_handoff_subject(handoff.get('external_subject', ''))}"
+ relay_body = build_relay_confirmation_text(customer_body)
+ relay_subject = _handoff_reply_subject(handoff.get("external_subject", ""))
relay_threading = threading_fields_from_parent(human_message)
relay_internet_id = await deliver_email(
sender=agent_email,
@@ -656,7 +742,7 @@ async def maybe_trigger_handoff(
existing = await find_active_handoff_for_external_thread(
database, account_id, external_thread_id
)
- reason = evaluation.get("instruction_reason") or rule.get("instructions") or "Matched handoff rules."
+ reason = (rule.get("instructions") or "").strip() or "This conversation requires human attention."
await send_handoff_to_human(
database,
account_id=account_id,
diff --git a/backend/app/services/reply_extractor.py b/backend/app/services/reply_extractor.py
new file mode 100644
index 0000000..0780f45
--- /dev/null
+++ b/backend/app/services/reply_extractor.py
@@ -0,0 +1,119 @@
+import re
+from html import unescape
+
+# Patterns that mark the start of quoted previous-message content.
+TEXT_QUOTE_MARKERS = [
+ re.compile(r"\n-{2,}\s*Original Message\s*-{2,}", re.IGNORECASE),
+ re.compile(
+ r"\nOn .+? wrote:\s*\n",
+ re.IGNORECASE,
+ ),
+ re.compile(
+ r"\nOn .+?, .+? at .+? .+? wrote:\s*\n",
+ re.IGNORECASE,
+ ),
+ re.compile(
+ r"\nFrom:\s*.+\nSent:\s*.+\nTo:\s*.+\nSubject:\s*.+",
+ re.IGNORECASE,
+ ),
+ re.compile(r"\n_{5,}\n"),
+]
+
+HTML_QUOTE_PATTERNS = [
+ re.compile(r"]*>.*? ", re.IGNORECASE | re.DOTALL),
+ re.compile(r']+class="[^"]*gmail_quote[^"]*"[^>]*>.*?
', re.IGNORECASE | re.DOTALL),
+ re.compile(r']+class="[^"]*gmail_attr[^"]*"[^>]*>.*?
', re.IGNORECASE | re.DOTALL),
+]
+
+PRIVATE_LEAK_MARKERS = [
+ "human handoff",
+ "a conversation needs your attention",
+ "why this was handed off",
+ "why you're receiving this",
+ "gentic human handoff",
+ "no handoff instructions were set",
+ "step passes",
+ "classifier returned",
+ "rule evaluation succeeded",
+ "reply normally to respond through",
+ "latest message",
+ "private agent ↔ human conversation",
+]
+
+
+def _normalize_newlines(text: str) -> str:
+ return text.replace("\r\n", "\n").replace("\r", "\n")
+
+
+def _html_to_text(html: str) -> str:
+ cleaned = html
+ for pattern in HTML_QUOTE_PATTERNS:
+ cleaned = pattern.sub("", cleaned)
+ cleaned = re.sub(r" ", "\n", cleaned, flags=re.IGNORECASE)
+ cleaned = re.sub(r"", "\n", cleaned, flags=re.IGNORECASE)
+ cleaned = re.sub(r"<[^>]+>", "", cleaned)
+ return unescape(cleaned)
+
+
+def _strip_trailing_quoted_lines(text: str) -> str:
+ lines = text.split("\n")
+ kept: list[str] = []
+ for line in lines:
+ if re.match(r"^\s*>", line):
+ break
+ kept.append(line)
+ return "\n".join(kept).strip()
+
+
+def _split_at_quote_markers(text: str) -> tuple[str, str]:
+ normalized = _normalize_newlines(text)
+ earliest_index = None
+ for pattern in TEXT_QUOTE_MARKERS:
+ match = pattern.search(normalized)
+ if match and (earliest_index is None or match.start() < earliest_index):
+ earliest_index = match.start()
+ if earliest_index is not None:
+ new_content = normalized[:earliest_index].strip()
+ quoted = normalized[earliest_index:].strip()
+ return new_content, quoted
+ return _strip_trailing_quoted_lines(normalized), ""
+
+
+def extract_new_reply_content(
+ *,
+ body_text: str | None,
+ body_html: str | None = None,
+) -> dict[str, str]:
+ raw_text = _normalize_newlines((body_text or "").strip())
+ if body_html and not raw_text:
+ raw_text = _html_to_text(body_html).strip()
+
+ if body_html:
+ html_stripped = body_html
+ for pattern in HTML_QUOTE_PATTERNS:
+ html_stripped = pattern.sub("", html_stripped)
+ html_text = _html_to_text(html_stripped).strip()
+ if html_text:
+ new_from_html, quoted_from_html = _split_at_quote_markers(html_text)
+ if new_from_html:
+ return {
+ "raw_body": raw_text or html_text,
+ "new_reply_content": new_from_html,
+ "quoted_content": quoted_from_html,
+ }
+
+ new_content, quoted_content = _split_at_quote_markers(raw_text)
+ return {
+ "raw_body": raw_text,
+ "new_reply_content": new_content,
+ "quoted_content": quoted_content,
+ }
+
+
+def validate_external_reply_body(body: str) -> bool:
+ normalized = (body or "").strip()
+ if not normalized:
+ return False
+ lower = normalized.lower()
+ hits = sum(1 for marker in PRIVATE_LEAK_MARKERS if marker in lower)
+ return hits == 0
diff --git a/backend/tests/test_reply_extractor.py b/backend/tests/test_reply_extractor.py
new file mode 100644
index 0000000..fba95eb
--- /dev/null
+++ b/backend/tests/test_reply_extractor.py
@@ -0,0 +1,63 @@
+from app.services.handoff_email_templates import external_reply_subject, handoff_email_subject
+from app.services.reply_extractor import extract_new_reply_content, validate_external_reply_body
+
+SCREENSHOT_REPLY = """ahan, did you receive a response?
+
+On Tue, Oct 6, 2026 at 2:11 AM wrote:
+
+> Human Handoff
+>
+> A conversation needs your attention.
+>
+> From
+> 091rabbytes@gmail.com
+>
+> Subject
+> are you ready?
+>
+> Latest message
+> "lets start a threaddddd"
+>
+> Why this was handed off
+> No handoff instructions were set, so this step passes.
+>
+> Reply to this email to respond to the original sender through suyash@gentic.email.
+>
+> Your reply will be sent to the customer.
+>
+> Gentic Human Handoff"""
+
+
+def test_extracts_new_reply_from_gmail_quote():
+ result = extract_new_reply_content(body_text=SCREENSHOT_REPLY)
+ assert result["new_reply_content"] == "ahan, did you receive a response?"
+ assert "Human Handoff" in result["quoted_content"]
+ assert "091rabbytes@gmail.com" in result["quoted_content"]
+
+
+def test_extracted_reply_passes_leakage_guard():
+ result = extract_new_reply_content(body_text=SCREENSHOT_REPLY)
+ assert validate_external_reply_body(result["new_reply_content"]) is True
+
+
+def test_leakage_guard_blocks_private_template():
+ assert validate_external_reply_body(SCREENSHOT_REPLY) is False
+
+
+def test_external_reply_subject_uses_customer_subject():
+ assert external_reply_subject("are you ready?") == "Re: are you ready?"
+ assert external_reply_subject("Human Handoff · are you ready?") == "Re: are you ready?"
+
+
+def test_handoff_subject_format():
+ assert handoff_email_subject("are you ready?") == "Human Handoff · are you ready?"
+
+
+def test_outlook_original_message_split():
+ body = """Thanks for the update.
+
+-----Original Message-----
+From: support@gentic.email
+Subject: Human Handoff"""
+ result = extract_new_reply_content(body_text=body)
+ assert result["new_reply_content"] == "Thanks for the update."
diff --git a/frontend/src/components/HandoffConversation.jsx b/frontend/src/components/HandoffConversation.jsx
index e87fdc3..019b121 100644
--- a/frontend/src/components/HandoffConversation.jsx
+++ b/frontend/src/components/HandoffConversation.jsx
@@ -2,6 +2,29 @@ import { ArrowLeft } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { getHandoffConversation } from "../api";
+function HandoffMessageBody({ message }) {
+ const [showQuoted, setShowQuoted] = useState(false);
+ const displayText = message.new_reply_content || message.body_text || message.snippet;
+
+ return (
+ <>
+ {displayText}
+ {message.has_quoted_text && message.raw_body && (
+ setShowQuoted((current) => !current)}
+ >
+ {showQuoted ? "Hide quoted text" : "Show quoted text"}
+
+ )}
+ {showQuoted && message.raw_body && (
+ {message.raw_body}
+ )}
+ >
+ );
+}
+
function formatTime(dateValue) {
if (!dateValue) return "";
return new Date(dateValue).toLocaleString(undefined, {
@@ -63,9 +86,7 @@ export default function HandoffConversation({ handoffId, onBack }) {
{message.role === "human" ? "HUMAN OWNER" : "AGENT"}
{formatTime(message.event_at)}
-
- {message.body_text || message.snippet}
-
+
))}
diff --git a/frontend/src/styles.css b/frontend/src/styles.css
index 92c60cd..bbcab18 100644
--- a/frontend/src/styles.css
+++ b/frontend/src/styles.css
@@ -559,6 +559,8 @@ h1 em { color:var(--coral); font-style:normal; font-weight:500; }
.handoff-message-human header strong { color:#11966a; }
.handoff-message header time { color:#9a96a5; font-size:11px; }
.handoff-message-body { color:#292536; font-size:13px; line-height:1.6; white-space:pre-wrap; }
+.handoff-show-quoted { margin-top:8px; padding:0; border:0; background:transparent; color:#6d5bd0; font-size:11px; font-weight:600; cursor:pointer; }
+.handoff-quoted-body { margin:8px 0 0; padding:10px 12px; border:1px solid #ece9f2; border-radius:8px; background:#faf9fc; color:#77758a; font-size:11px; line-height:1.45; white-space:pre-wrap; overflow:auto; }
.handoff-settings-intro { margin-bottom:18px; padding:18px 20px; border:1px solid #ece7ff; border-radius:14px; background:#fbf9ff; }
.handoff-settings-intro p { margin:0 0 10px; color:#4a4652; font-size:12px; line-height:1.6; }
.handoff-settings-intro p strong { color:#171424; }