human handoffs

This commit is contained in:
debu-2 2026-10-06 01:51:23 +05:30
parent 79188e0f59
commit 36a55930f9
35 changed files with 4039 additions and 139 deletions

View file

@ -19,7 +19,8 @@ import {
import { useCallback, useEffect, useMemo, useState } from "react";
import {
getEmail,
listEmails,
getThread,
listThreads,
permanentlyDeleteEmail,
restoreEmail,
setEmailRead,
@ -62,6 +63,41 @@ function resultPage(result) {
};
}
function threadToRow(thread) {
const primarySender = thread.senders?.[0] || thread.participants?.[0] || { name: "Unknown", address: "" };
return {
id: thread.latest_message_id,
thread_id: thread.id,
mailbox: thread.mailbox,
sender: primarySender,
recipients: thread.participants || [],
subject: thread.subject,
snippet: thread.snippet,
created_at: thread.latest_at,
is_read: thread.is_read,
is_starred: thread.is_starred,
attachments: thread.has_attachments ? [{ filename: "attachment" }] : [],
message_count: thread.message_count,
unread_count: thread.unread_count,
};
}
function buildReplyContext(message, replyAll = false) {
const sender = message?.sender || { name: "", address: "" };
const subject = (message?.subject || "").trim();
const replySubject = /^re:/i.test(subject) ? subject : `Re: ${subject || "(No subject)"}`;
const quote = (message?.body_text || message?.snippet || "").trim();
return {
replyToMessageId: message.id,
replyAll,
to: replyAll ? [] : [{ email: sender.address, name: sender.name }],
subject: replySubject,
bodyHtml: quote
? `<p><br></p><blockquote>${quote.replace(/\n/g, "<br>")}</blockquote>`
: "<p><br></p>",
};
}
export default function MailboxView({
mailbox,
title,
@ -83,6 +119,7 @@ export default function MailboxView({
const [checked, setChecked] = useState({});
const [hidden, setHidden] = useState({});
const [detail, setDetail] = useState(null);
const [threadDetail, setThreadDetail] = useState(null);
const [status, setStatus] = useState({ loading: true, error: "" });
const [detailStatus, setDetailStatus] = useState({ loading: false, error: "", busy: false });
const [filtersOpen, setFiltersOpen] = useState(false);
@ -101,7 +138,7 @@ export default function MailboxView({
const load = useCallback(async () => {
setStatus({ loading: true, error: "" });
try {
const result = await listEmails(
const result = await listThreads(
{
mailbox,
limit: PAGE_SIZE,
@ -115,7 +152,7 @@ export default function MailboxView({
token,
);
const page = resultPage(result);
setEmails(page.items);
setEmails(page.items.map(threadToRow));
setNextCursor(page.nextCursor);
setStatus({ loading: false, error: "" });
} catch (error) {
@ -193,17 +230,26 @@ export default function MailboxView({
}
async function openEmail(email) {
setSelected(email.id);
setSelected(email.thread_id || email.id);
setDetail(email);
setThreadDetail(null);
setDetailStatus({ loading: true, error: "", busy: false });
if (email.is_read === false) {
setEmails((current) =>
current.map((item) => item.id === email.id ? { ...item, is_read: true } : item),
current.map((item) => (
item.thread_id === email.thread_id || item.id === email.id
? { ...item, is_read: true }
: item
)),
);
setDetail((current) => ({ ...current, is_read: true }));
setEmailRead(email.id, true, token).catch(() => {
setEmails((current) =>
current.map((item) => item.id === email.id ? { ...item, is_read: false } : item),
current.map((item) => (
item.thread_id === email.thread_id || item.id === email.id
? { ...item, is_read: false }
: item
)),
);
setDetail((current) =>
current?.id === email.id ? { ...current, is_read: false } : current,
@ -211,8 +257,24 @@ export default function MailboxView({
});
}
try {
const completeEmail = await getEmail(email.id, token);
setDetail((current) => ({ ...current, ...completeEmail, is_read: current?.is_read ?? completeEmail.is_read }));
if (email.thread_id) {
const thread = await getThread(email.thread_id, token);
const messages = thread.messages || [];
const latest = messages[messages.length - 1] || email;
setThreadDetail(thread);
setDetail((current) => ({
...current,
...latest,
is_read: current?.is_read ?? latest.is_read,
}));
} else {
const completeEmail = await getEmail(email.id, token);
setDetail((current) => ({
...current,
...completeEmail,
is_read: current?.is_read ?? completeEmail.is_read,
}));
}
setDetailStatus({ loading: false, error: "", busy: false });
} catch (error) {
setDetailStatus({ loading: false, error: error.message, busy: false });
@ -694,13 +756,14 @@ export default function MailboxView({
<MailDetail
email={detail}
thread={threadDetail}
messages={threadDetail?.messages}
loading={detailStatus.loading}
error={detailStatus.error}
busy={detailStatus.busy}
mailbox={mailbox}
viewerEmail={account?.email}
threadSize={1}
onClose={() => { setSelected(null); setDetail(null); }}
onClose={() => { setSelected(null); setDetail(null); setThreadDetail(null); }}
onToggleRead={toggleRead}
onToggleStar={(starred) => detail && toggleStar(detail, starred)}
onTrash={() => mutateSelected(trashEmail)}
@ -710,8 +773,17 @@ export default function MailboxView({
onSpam={() => detail && hideForSession(detail, "Marked as spam in this view until you refresh.")}
onSnooze={() => detail && hideForSession(detail, "Snoozed in this view until you refresh.")}
onMove={() => setAgentFocus("Move and labels — coming soon.")}
onReply={() => { onCompose?.(); setAgentFocus(detail?.subject ? `Reply: ${detail.subject}` : "Reply"); }}
onForward={() => { onCompose?.(); setAgentFocus(detail?.subject ? `Forward: ${detail.subject}` : "Forward"); }}
onReply={(replyAll = false) => {
const target = threadDetail?.messages?.[threadDetail.messages.length - 1] || detail;
onCompose?.(buildReplyContext(target, replyAll));
}}
onForward={() => {
const target = threadDetail?.messages?.[threadDetail.messages.length - 1] || detail;
onCompose?.({
subject: `Fwd: ${target?.subject || ""}`,
bodyHtml: `<p><br></p><blockquote>${(target?.body_text || target?.snippet || "").replace(/\n/g, "<br>")}</blockquote>`,
});
}}
onExtractActions={() => detail && extractActions(detail)}
onSummarizeThread={() => detail && summarizeThread(detail)}
onAnalyzeAttachment={() => detail && analyzeAttachment(detail)}
@ -725,6 +797,7 @@ export default function MailboxView({
account={account}
emails={listedEmails}
contextEmail={detail}
contextThread={threadDetail}
focus={agentFocus}
insight={insight}
loading={status.loading}