You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
185 lines
5.9 KiB
TypeScript
185 lines
5.9 KiB
TypeScript
// frontend/components/mail/AriPanel.tsx — 아리 분석 패널 (요약 + 할일/일정/답장/정리 + 모두 적용)
|
|
"use client";
|
|
import { Icon } from "@/components/Icon";
|
|
import type { AiReply, EmailDetail, EmailRow, ExtractKind } from "@/lib/types";
|
|
import { Sug } from "./Sug";
|
|
|
|
type Mail = EmailRow | EmailDetail;
|
|
|
|
export function AriPanel({
|
|
m,
|
|
added,
|
|
onAdd,
|
|
onReply,
|
|
onAddAll,
|
|
}: {
|
|
m: Mail;
|
|
added: Record<string, boolean>;
|
|
onAdd: (key: string, kind: ExtractKind, index: number) => void;
|
|
onReply: (m: Mail, replyIndex: number, r: AiReply) => void;
|
|
onAddAll: (m: Mail) => void;
|
|
}) {
|
|
const ai = m.ai;
|
|
if (!ai) return null;
|
|
|
|
const keys: string[] = [];
|
|
ai.tasks.forEach((_, i) => keys.push("t:" + m.id + ":" + i));
|
|
ai.events.forEach((_, i) => keys.push("e:" + m.id + ":" + i));
|
|
if (ai.file) keys.push("f:" + m.id);
|
|
const allDone = keys.length > 0 && keys.every((k) => added[k]);
|
|
|
|
return (
|
|
<div className="ari-panel">
|
|
<div className="ap-head">
|
|
<div className="ap-avatar">
|
|
<Icon name="spark" />
|
|
</div>
|
|
<div className="ap-h">
|
|
<b>아리가 분석했어요</b>
|
|
<span>{ai.category} · 새 메일을 읽고 필요한 일을 정리했어요</span>
|
|
</div>
|
|
<span className={"ap-prio " + ai.priority}>{ai.priority}</span>
|
|
</div>
|
|
|
|
<div
|
|
className="ap-summary"
|
|
dangerouslySetInnerHTML={{ __html: "<b>요약 — </b>" + ai.summary }}
|
|
/>
|
|
|
|
{ai.tasks.length > 0 && (
|
|
<div className="ap-section">
|
|
<div className="ap-sec-title">
|
|
<Icon name="check" />할 일로 추가<span className="n">{ai.tasks.length}건</span>
|
|
</div>
|
|
{ai.tasks.map((t, i) => {
|
|
const k = "t:" + m.id + ":" + i;
|
|
return (
|
|
<Sug
|
|
key={k}
|
|
kind="task"
|
|
text={t.text}
|
|
added={!!added[k]}
|
|
onAdd={() => onAdd(k, "task", i)}
|
|
meta={
|
|
<>
|
|
<span className="chip2">
|
|
<Icon name="folder" />
|
|
{t.project}
|
|
</span>
|
|
<span className="dotsep" />
|
|
<span className="chip2">
|
|
<Icon name="cal" />
|
|
{t.due}
|
|
</span>
|
|
<span className="dotsep" />
|
|
<span className="chip2">
|
|
<Icon name="flag" />
|
|
{t.prio}
|
|
</span>
|
|
</>
|
|
}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{ai.events.length > 0 && (
|
|
<div className="ap-section">
|
|
<div className="ap-sec-title">
|
|
<Icon name="cal" />
|
|
일정에 추가<span className="n">{ai.events.length}건</span>
|
|
</div>
|
|
{ai.events.map((ev, i) => {
|
|
const k = "e:" + m.id + ":" + i;
|
|
return (
|
|
<Sug
|
|
key={k}
|
|
kind="event"
|
|
text={ev.title}
|
|
added={!!added[k]}
|
|
onAdd={() => onAdd(k, "event", i)}
|
|
meta={
|
|
<>
|
|
<span className="chip2">
|
|
<Icon name="cal" />
|
|
{ev.date} ({ev.day})
|
|
</span>
|
|
<span className="dotsep" />
|
|
<span className="chip2">
|
|
<Icon name="clock" />
|
|
{ev.time}
|
|
{ev.dur ? " · " + ev.dur : ""}
|
|
</span>
|
|
{ev.place && (
|
|
<>
|
|
<span className="dotsep" />
|
|
<span className="chip2">{ev.place}</span>
|
|
</>
|
|
)}
|
|
</>
|
|
}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
|
|
{ai.replies.length > 0 && (
|
|
<div className="ap-section">
|
|
<div className="ap-sec-title">
|
|
<Icon name="reply" />
|
|
답장 추천<span className="n">{ai.replies.length}개 초안</span>
|
|
</div>
|
|
<div className="replies">
|
|
{ai.replies.map((r, i) => (
|
|
<button className="rep" key={i} onClick={() => onReply(m, i, r)}>
|
|
<div className="rep-top">
|
|
<span className="rep-tone">{r.tone}</span>
|
|
<span className="rep-go">
|
|
<Icon name="arrow" />
|
|
</span>
|
|
</div>
|
|
<div className="rep-prev">{r.preview}</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{ai.file &&
|
|
(() => {
|
|
const k = "f:" + m.id;
|
|
return (
|
|
<div className={"ap-file" + (added[k] ? " added" : "")}>
|
|
<div className="fi-ico">
|
|
<Icon name="layers" />
|
|
</div>
|
|
<div className="fi-main">
|
|
<b>{ai.file.project}에 정리</b>
|
|
<span>{ai.file.reason}</span>
|
|
</div>
|
|
<button
|
|
className={"sug-add" + (added[k] ? " done" : "")}
|
|
onClick={() => onAdd(k, "file", 0)}
|
|
>
|
|
<Icon name={added[k] ? "tick" : "plus"} w={added[k] ? 3 : 1.9} />
|
|
{added[k] ? "정리됨" : "정리"}
|
|
</button>
|
|
</div>
|
|
);
|
|
})()}
|
|
|
|
{keys.length > 1 && (
|
|
<div className="ap-allbar">
|
|
<span className="txt">아리가 준비한 {keys.length}개 작업을 한 번에 적용할 수 있어요</span>
|
|
<button className={"ap-all-btn" + (allDone ? " done" : "")} onClick={() => onAddAll(m)}>
|
|
<Icon name={allDone ? "tick" : "zap"} w={allDone ? 3 : 1.9} />
|
|
{allDone ? "모두 적용됨" : "모두 적용"}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|