// frontend/components/mail/Triage.tsx — 아리 비서 다이제스트(아리 정리 보기) // 새 메일 중 '챙길 거리'를 발신자 카드로 정리: 요약 + 할 일/일정/답장 액션을 바로 적용. "use client"; import { Icon } from "@/components/Icon"; import type { AiAnalysis, AiReply, EmailRow, ExtractKind, Person } from "@/lib/types"; const PALETTE = ["var(--blue)", "var(--violet)", "var(--coral)", "var(--green)", "var(--amber)"]; const PRIO_RANK: Record = { 높음: 0, 보통: 1, 낮음: 2 }; // 기준 시각(ISO) → 사용자 로컬 표기('6월 18일 오후 2:30'). function fmtSince(iso?: string): string { if (!iso) return ""; const d = new Date(iso); if (Number.isNaN(d.getTime())) return ""; return d.toLocaleString("ko-KR", { month: "long", day: "numeric", hour: "numeric", minute: "2-digit", }); } // 발신자 이름 → 안정적 색(메일마다 같은 발신자는 같은 색). function senderColor(name: string): string { let h = 0; for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) >>> 0; return PALETTE[h % PALETTE.length]; } // 메일 한 통에 달린 액션 수(할 일 + 일정 + 답장 1). function actionCount(ai: AiAnalysis): number { return ai.tasks.length + ai.events.length + (ai.replies.length ? 1 : 0); } // 카드 상태 배지 — 우선순위·액션 종류에서 비서 톤으로 산출. function statusOf(ai: AiAnalysis): { text: string; tone: string } { if (ai.priority === "높음") return { text: "중요 · 액션 필요", tone: "coral" }; if (ai.events.length) return { text: "일정 · 준비 필요", tone: "violet" }; if (ai.tasks.length) return { text: "할 일 · 확인 필요", tone: "blue" }; if (ai.replies.length) return { text: "답장 추천", tone: "green" }; return { text: "검토 필요", tone: "muted" }; } function ActionRow({ icon, kind, title, meta, done, onAdd, }: { icon: "check" | "cal"; kind: "task" | "event"; title: string; meta: string; done: boolean; onAdd: () => void; }) { return (
{title} {meta && {meta}}
); } function SecretaryCard({ m, name, color, added, onOpen, onAdd, onAddAll, onReply, }: { m: EmailRow; name: string; color: string; added: Record; onOpen: (id: string) => void; onAdd: (mailId: string, key: string, kind: ExtractKind, index: number) => void; onAddAll: (m: EmailRow) => void; onReply: (m: EmailRow, idx: number, r: AiReply) => void; }) { const ai = m.ai; if (!ai) return null; const status = statusOf(ai); const n = actionCount(ai); const reply = ai.replies[0]; return (
{name.slice(0, 1).toUpperCase()}
{name} {status.text}

onOpen(m.id)}> {ai.summary || m.subject}

{ai.summary && m.subject !== ai.summary &&

{m.subject}

} {n > 0 && (
{ai.tasks.map((t, i) => ( onAdd(m.id, "t:" + m.id + ":" + i, "task", i)} /> ))} {ai.events.map((e, i) => ( onAdd(m.id, "e:" + m.id + ":" + i, "event", i)} /> ))} {reply && ( )}
)} {n > 1 && ( )}
); } export function Triage({ emails, people, accColor, added, analyzing, since, onAnalyze, onResetBaseline, onOpen, onAdd, onAddAll, onReply, }: { emails: EmailRow[]; people: Record; accColor: (id: string) => string; added: Record; analyzing: boolean; since?: string; onAnalyze: () => void; onResetBaseline: () => void; onOpen: (id: string) => void; onAdd: (mailId: string, key: string, kind: ExtractKind, index: number) => void; onAddAll: (m: EmailRow) => void; onReply: (m: EmailRow, idx: number, r: AiReply) => void; }) { const nameOf = (m: EmailRow) => people[m.from]?.name ?? m.from_name ?? m.from; const colorOf = (m: EmailRow) => people[m.from]?.color ?? senderColor(nameOf(m)) ?? accColor(m.account); // 새 메일 = 기준 시각 이후 도착(서버가 smart 보기로 이미 필터). 읽음 여부와 무관 — // 사용자가 폰/다른 앱에서 먼저 읽어도 아리가 챙길 거리를 정리한다. const fresh = emails.filter((m) => m.ai); // 챙길 거리: 할 일/일정/답장이 있거나 중요(높음)인 새 메일. const cards = fresh .filter((m) => m.ai && (actionCount(m.ai) > 0 || m.ai.priority === "높음")) .sort((a, b) => { const pa = PRIO_RANK[a.ai!.priority] ?? 1; const pb = PRIO_RANK[b.ai!.priority] ?? 1; if (pa !== pb) return pa - pb; return actionCount(b.ai!) - actionCount(a.ai!); }); const tot = cards.reduce( (acc, m) => { acc.tasks += m.ai!.tasks.length; acc.events += m.ai!.events.length; acc.replies += m.ai!.replies.length ? 1 : 0; return acc; }, { tasks: 0, events: 0, replies: 0 }, ); // 아직 아리가 정리하지 않은(분석 전) 새 메일 수. const pending = emails.filter((m) => !m.ai).length; return (
아리 비서 · 새 메일

{cards.length > 0 ? `새 메일 ${fresh.length}통 중 ${cards.length}건을 챙겨뒀어요` : pending > 0 ? `새 메일 ${pending}통, 정리해 드릴까요?` : "지금은 챙길 새 메일이 없어요"}

아리가 기준 시각 이후 도착한 새 메일만 읽고 할 일·일정·답장으로 정리해요. 카드마다 바로 적용하거나 한 번에 처리하면, 결재가 필요한 발송은 결재함으로 올려둘게요.

{cards.length > 0 && (
{tot.tasks} 할 일 {tot.events} 일정 {tot.replies} 답장
)}
{since && (
{fmtSince(since)} 이후 도착한 메일만 정리해요
)}
{cards.length === 0 ? (
0 ? "spark" : "check"} />
{pending > 0 ? ( <>

아직 정리하지 않은 새 메일이 {pending}통 있어요

위 ‘정리 맡기기’를 누르면 아리가 읽고 할 일·일정·답장으로 정리해 드려요.

) : ( <>

새로 도착한 메일이 없어요

기준 시각 이후 새 메일이 오면 아리가 여기에 할 일·일정·답장으로 정리해 둘게요.

)}
) : (
{cards.map((m) => ( ))}
)}
); }