human handoffs
This commit is contained in:
parent
79188e0f59
commit
36a55930f9
35 changed files with 4039 additions and 139 deletions
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue